rack_dav 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *~
2
+ doc/*
3
+ pkg/*
4
+ test/repo
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2009 Matthias Georgi <http://www.matthias-georgi.de>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,108 @@
1
+ ---
2
+ title: RackDAV - Web Authoring for Rack
3
+ ---
4
+
5
+ RackDAV is Handler for [Rack][1], which allows content authoring over
6
+ HTTP. RackDAV brings its own file backend, but other backends are
7
+ possible by subclassing RackDAV::Resource.
8
+
9
+ ## Install
10
+
11
+ Just install the gem from github:
12
+
13
+ $ gem sources -a http://gems.github.com
14
+ $ sudo gem install georgi-rack_dav
15
+
16
+ ## Quickstart
17
+
18
+ If you just want to share a folder over WebDAV, you can just start a
19
+ simple server with:
20
+
21
+ $ rack_dav
22
+
23
+ This will start a WEBrick server on port 3000, which you can connect
24
+ to without authentication.
25
+
26
+ ## Rack Handler
27
+
28
+ Using RackDAV inside a rack application is quite easy. A simple rackup
29
+ script looks like this:
30
+
31
+ @@ruby
32
+
33
+ require 'rubygems'
34
+ require 'rack_dav'
35
+
36
+ use Rack::CommonLogger
37
+
38
+ run RackDAV::Handler.new('/path/to/docs')
39
+
40
+ ## Implementing your own WebDAV resource
41
+
42
+ RackDAV::Resource is an abstract base class and defines an interface
43
+ for accessing resources.
44
+
45
+ Each resource will be initialized with a path, which should be used to
46
+ find the real resource.
47
+
48
+ RackDAV::Handler needs to be initialized with the actual resource class:
49
+
50
+ @@ruby
51
+
52
+ RackDAV::Handler.new(:resource_class => MyResource)
53
+
54
+ RackDAV needs some information about the resources, so you have to
55
+ implement following methods:
56
+
57
+ * __children__: If this is a collection, return the child resources.
58
+
59
+ * __collection?__: Is this resource a collection?
60
+
61
+ * __exist?__: Does this recource exist?
62
+
63
+ * __creation\_date__: Return the creation time.
64
+
65
+ * __last\_modified__: Return the time of last modification.
66
+
67
+ * __last\_modified=(time)__: Set the time of last modification.
68
+
69
+ * __etag__: Return an Etag, an unique hash value for this resource.
70
+
71
+ * __content_type__: Return the mime type of this resource.
72
+
73
+ * __content\_length__: Return the size in bytes for this resource.
74
+
75
+
76
+ Most importantly you have to implement the actions, which are called
77
+ to retrieve and change the resources:
78
+
79
+ * __get(request, response)__: Write the content of the resource to the response.body.
80
+
81
+ * __put(request, response)__: Save the content of the request.body.
82
+
83
+ * __post(request, response)__: Usually forbidden.
84
+
85
+ * __delete__: Delete this resource.
86
+
87
+ * __copy(dest)__: Copy this resource to given destination resource.
88
+
89
+ * __move(dest)__: Move this resource to given destination resource.
90
+
91
+ * __make\_collection__: Create this resource as collection.
92
+
93
+
94
+ Note, that it is generally possible, that a resource object is
95
+ instantiated for a not yet existing resource.
96
+
97
+ For inspiration you should have a look at the FileResource
98
+ implementation. Please let me now, if you are going to implement a new
99
+ type of resource.
100
+
101
+
102
+ ### RackDAV on GitHub
103
+
104
+ Download or fork the project on its [Github page][2]
105
+
106
+
107
+ [1]: http://github.com/chneukirchen/rack
108
+ [2]: http://github.com/georgi/rack_dav
data/bin/rack_dav ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'rack_dav'
5
+
6
+ app = Rack::Builder.new do
7
+ use Rack::ShowExceptions
8
+ use Rack::CommonLogger
9
+ use Rack::Reloader
10
+ use Rack::Lint
11
+
12
+ run RackDAV::Handler.new
13
+
14
+ end.to_app
15
+
16
+ begin
17
+ Rack::Handler::Mongrel.run(app, :Port => 3000)
18
+ rescue
19
+ Rack::Handler::WEBrick.run(app, :Port => 3000)
20
+ end
@@ -0,0 +1,22 @@
1
+
2
+ module Builder
3
+
4
+ class XmlBase
5
+ def namespace(ns)
6
+ old_namespace = @namespace
7
+ @namespace = ns
8
+ yield
9
+ @namespace = old_namespace
10
+ self
11
+ end
12
+
13
+ alias_method :method_missing_without_namespace, :method_missing
14
+
15
+ def method_missing(sym, *args, &block)
16
+ sym = "#{@namespace}:#{sym}" if @namespace
17
+ method_missing_without_namespace(sym, *args, &block)
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,410 @@
1
+ module RackDAV
2
+
3
+ class Controller
4
+ include RackDAV::HTTPStatus
5
+
6
+ attr_reader :request, :response, :resource
7
+
8
+ def initialize(request, response, options)
9
+ @request = request
10
+ @response = response
11
+ @options = options
12
+ @resource = resource_class.new(url_unescape(request.path_info), @options)
13
+ raise Forbidden if request.path_info.include?('../')
14
+ end
15
+
16
+ def url_escape(s)
17
+ s.gsub(/([^\/a-zA-Z0-9_.-]+)/n) do
18
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
19
+ end.tr(' ', '+')
20
+ end
21
+
22
+ def url_unescape(s)
23
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) do
24
+ [$1.delete('%')].pack('H*')
25
+ end
26
+ end
27
+
28
+ def options
29
+ response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
30
+ response["Dav"] = "1,2"
31
+ response["Ms-Author-Via"] = "DAV"
32
+ end
33
+
34
+ def head
35
+ raise NotFound if not resource.exist?
36
+ response['Etag'] = resource.etag
37
+ response['Content-Type'] = resource.content_type
38
+ response['Last-Modified'] = resource.last_modified.httpdate
39
+ end
40
+
41
+ def get
42
+ raise NotFound if not resource.exist?
43
+ response['Etag'] = resource.etag
44
+ response['Content-Type'] = resource.content_type
45
+ response['Content-Length'] = resource.content_length.to_s
46
+ response['Last-Modified'] = resource.last_modified.httpdate
47
+ map_exceptions do
48
+ resource.get(request, response)
49
+ end
50
+ end
51
+
52
+ def put
53
+ raise Forbidden if resource.collection?
54
+ map_exceptions do
55
+ resource.put(request, response)
56
+ end
57
+ end
58
+
59
+ def post
60
+ map_exceptions do
61
+ resource.post(request, response)
62
+ end
63
+ end
64
+
65
+ def delete
66
+ delete_recursive(resource, errors = [])
67
+
68
+ if errors.empty?
69
+ response.status = NoContent
70
+ else
71
+ multistatus do |xml|
72
+ response_errors(xml, errors)
73
+ end
74
+ end
75
+ end
76
+
77
+ def mkcol
78
+ map_exceptions do
79
+ resource.make_collection
80
+ end
81
+ response.status = Created
82
+ end
83
+
84
+ def copy
85
+ raise NotFound if not resource.exist?
86
+
87
+ dest_uri = URI.parse(env['HTTP_DESTINATION'])
88
+ destination = url_unescape(dest_uri.path)
89
+
90
+ raise BadGateway if dest_uri.host and dest_uri.host != request.host
91
+ raise Forbidden if destination == resource.path
92
+
93
+ dest = resource_class.new(destination, @options)
94
+ dest = dest.child(resource.name) if dest.collection?
95
+
96
+ dest_existed = dest.exist?
97
+
98
+ copy_recursive(resource, dest, depth, errors = [])
99
+
100
+ if errors.empty?
101
+ response.status = dest_existed ? NoContent : Created
102
+ else
103
+ multistatus do |xml|
104
+ response_errors(xml, errors)
105
+ end
106
+ end
107
+ rescue URI::InvalidURIError => e
108
+ raise BadRequest.new(e.message)
109
+ end
110
+
111
+ def move
112
+ raise NotFound if not resource.exist?
113
+
114
+ dest_uri = URI.parse(env['HTTP_DESTINATION'])
115
+ destination = url_unescape(dest_uri.path)
116
+
117
+ raise BadGateway if dest_uri.host and dest_uri.host != request.host
118
+ raise Forbidden if destination == resource.path
119
+
120
+ dest = resource_class.new(destination, @options)
121
+ dest = dest.child(resource.name) if dest.collection?
122
+
123
+ dest_existed = dest.exist?
124
+
125
+ raise Conflict if depth <= 1
126
+
127
+ copy_recursive(resource, dest, depth, errors = [])
128
+ delete_recursive(resource, errors)
129
+
130
+ if errors.empty?
131
+ response.status = dest_existed ? NoContent : Created
132
+ else
133
+ multistatus do |xml|
134
+ response_errors(xml, errors)
135
+ end
136
+ end
137
+ rescue URI::InvalidURIError => e
138
+ raise BadRequest.new(e.message)
139
+ end
140
+
141
+ def propfind
142
+ raise NotFound if not resource.exist?
143
+
144
+ if not request_match("/propfind/allprop").empty?
145
+ names = resource.property_names
146
+ else
147
+ names = request_match("/propfind/prop/*").map { |e| e.name }
148
+ names = resource.property_names
149
+ raise BadRequest if names.empty?
150
+ end
151
+
152
+ multistatus do |xml|
153
+ for resource in find_resources
154
+ xml.response do
155
+ xml.href "http://#{host}#{url_escape resource.path}"
156
+ propstats xml, get_properties(resource, names)
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ def proppatch
163
+ raise NotFound if not resource.exist?
164
+
165
+ prop_rem = request_match("/propertyupdate/remove/prop/*").map { |e| [e.name] }
166
+ prop_set = request_match("/propertyupdate/set/prop/*").map { |e| [e.name, e.text] }
167
+
168
+ multistatus do |xml|
169
+ for resource in find_resources
170
+ xml.response do
171
+ xml.href "http://#{host}#{resource.path}"
172
+ propstats xml, set_properties(resource, prop_set)
173
+ end
174
+ end
175
+ end
176
+
177
+ resource.save
178
+ end
179
+
180
+ def lock
181
+ raise NotFound if not resource.exist?
182
+
183
+ lockscope = request_match("/lockinfo/lockscope/*")[0].name
184
+ locktype = request_match("/lockinfo/locktype/*")[0].name
185
+ owner = request_match("/lockinfo/owner/href")[0]
186
+ locktoken = "opaquelocktoken:" + sprintf('%x-%x-%s', Time.now.to_i, Time.now.sec, resource.etag)
187
+
188
+ response['Lock-Token'] = locktoken
189
+
190
+ render_xml do |xml|
191
+ xml.prop('xmlns:D' => "DAV:") do
192
+ xml.lockdiscovery do
193
+ xml.activelock do
194
+ xml.lockscope { xml.tag! lockscope }
195
+ xml.locktype { xml.tag! locktype }
196
+ xml.depth 'Infinity'
197
+ if owner
198
+ xml.owner { xml.href owner.text }
199
+ end
200
+ xml.timeout "Second-60"
201
+ xml.locktoken do
202
+ xml.href locktoken
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+
210
+ def unlock
211
+ raise NoContent
212
+ end
213
+
214
+ # ************************************************************
215
+ # private methods
216
+
217
+ private
218
+
219
+ def env
220
+ @request.env
221
+ end
222
+
223
+ def host
224
+ env['HTTP_HOST']
225
+ end
226
+
227
+ def resource_class
228
+ @options[:resource_class]
229
+ end
230
+
231
+ def depth
232
+ case env['HTTP_DEPTH']
233
+ when '0' then 0
234
+ when '1' then 1
235
+ else 100
236
+ end
237
+ end
238
+
239
+ def overwrite
240
+ env['HTTP_OVERWRITE'].to_s.upcase != 'F'
241
+ end
242
+
243
+ def find_resources
244
+ case env['HTTP_DEPTH']
245
+ when '0'
246
+ [resource]
247
+ when '1'
248
+ [resource] + resource.children
249
+ else
250
+ [resource] + resource.descendants
251
+ end
252
+ end
253
+
254
+ def delete_recursive(res, errors)
255
+ for child in res.children
256
+ delete_recursive(child, errors)
257
+ end
258
+
259
+ begin
260
+ map_exceptions { res.delete } if errors.empty?
261
+ rescue Status
262
+ errors << [res.path, $!]
263
+ end
264
+ end
265
+
266
+ def copy_recursive(res, dest, depth, errors)
267
+ map_exceptions do
268
+ if dest.exist?
269
+ if overwrite
270
+ delete_recursive(dest, errors)
271
+ else
272
+ raise PreconditionFailed
273
+ end
274
+ end
275
+ res.copy(dest)
276
+ end
277
+ rescue Status
278
+ errors << [res.path, $!]
279
+ else
280
+ if depth > 0
281
+ for child in res.children
282
+ dest_child = dest.child(child.name)
283
+ copy_recursive(child, dest_child, depth - 1, errors)
284
+ end
285
+ end
286
+ end
287
+
288
+ def map_exceptions
289
+ yield
290
+ rescue
291
+ case $!
292
+ when URI::InvalidURIError then raise BadRequest
293
+ when Errno::EACCES then raise Forbidden
294
+ when Errno::ENOENT then raise Conflict
295
+ when Errno::EEXIST then raise Conflict
296
+ when Errno::ENOSPC then raise InsufficientStorage
297
+ else
298
+ raise
299
+ end
300
+ end
301
+
302
+ def request_document
303
+ @request_document ||= REXML::Document.new(request.body.read)
304
+ rescue REXML::ParseException
305
+ raise BadRequest
306
+ end
307
+
308
+ def request_match(pattern)
309
+ REXML::XPath::match(request_document, pattern, '' => 'DAV:')
310
+ end
311
+
312
+ def render_xml
313
+ xml = Builder::XmlMarkup.new(:indent => 2)
314
+ xml.instruct! :xml, :version => "1.0", :encoding => "UTF-8"
315
+
316
+ xml.namespace('D') do
317
+ yield xml
318
+ end
319
+
320
+ response.body = xml.target!
321
+ response["Content-Type"] = 'text/xml; charset="utf-8"'
322
+ response["Content-Length"] = response.body.size.to_s
323
+ end
324
+
325
+ def multistatus
326
+ render_xml do |xml|
327
+ xml.multistatus('xmlns:D' => "DAV:") do
328
+ yield xml
329
+ end
330
+ end
331
+
332
+ response.status = MultiStatus
333
+ end
334
+
335
+ def response_errors(xml, errors)
336
+ for path, status in errors
337
+ xml.response do
338
+ xml.href "http://#{host}#{path}"
339
+ xml.status "#{request.env['HTTP_VERSION']} #{status.status_line}"
340
+ end
341
+ end
342
+ end
343
+
344
+ def get_properties(resource, names)
345
+ stats = Hash.new { |h, k| h[k] = [] }
346
+ for name in names
347
+ begin
348
+ map_exceptions do
349
+ stats[OK] << [name, resource.get_property(name)]
350
+ end
351
+ rescue Status
352
+ stats[$!] << name
353
+ end
354
+ end
355
+ stats
356
+ end
357
+
358
+ def set_properties(resource, pairs)
359
+ stats = Hash.new { |h, k| h[k] = [] }
360
+ for name, value in pairs
361
+ begin
362
+ map_exceptions do
363
+ stats[OK] << [name, resource.set_property(name, value)]
364
+ end
365
+ rescue Status
366
+ stats[$!] << name
367
+ end
368
+ end
369
+ stats
370
+ end
371
+
372
+ def propstats(xml, stats)
373
+ return if stats.empty?
374
+ for status, props in stats
375
+ xml.propstat do
376
+ xml.prop do
377
+ for name, value in props
378
+ if value.is_a?(REXML::Element)
379
+ xml.tag!(name) do
380
+ rexml_convert(xml, value)
381
+ end
382
+ else
383
+ xml.tag!(name, value)
384
+ end
385
+ end
386
+ end
387
+ xml.status "#{request.env['HTTP_VERSION']} #{status.status_line}"
388
+ end
389
+ end
390
+ end
391
+
392
+ def rexml_convert(xml, element)
393
+ if element.elements.empty?
394
+ if element.text
395
+ xml.tag!(element.name, element.text, element.attributes)
396
+ else
397
+ xml.tag!(element.name, element.attributes)
398
+ end
399
+ else
400
+ xml.tag!(element.name, element.attributes) do
401
+ element.elements.each do |child|
402
+ rexml_convert(xml, child)
403
+ end
404
+ end
405
+ end
406
+ end
407
+
408
+ end
409
+
410
+ end