rack_dav 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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