georgi-rack_dav 0.1.1

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
data/lib/rack_dav.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'builder'
3
+ require 'time'
4
+ require 'uri'
5
+ require 'rexml/document'
6
+ require 'webrick/httputils'
7
+
8
+ require 'rack'
9
+ require 'rack_dav/builder_namespace'
10
+ require 'rack_dav/http_status'
11
+ require 'rack_dav/resource'
12
+ require 'rack_dav/file_resource'
13
+ require 'rack_dav/handler'
14
+ require 'rack_dav/controller'
15
+
@@ -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,409 @@
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"] = "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
+ raise BadRequest if names.empty?
149
+ end
150
+
151
+ multistatus do |xml|
152
+ for resource in find_resources
153
+ xml.response do
154
+ xml.href "http://#{host}#{url_escape resource.path}"
155
+ propstats xml, get_properties(resource, names)
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ def proppatch
162
+ raise NotFound if not resource.exist?
163
+
164
+ prop_rem = request_match("/propertyupdate/remove/prop/*").map { |e| [e.name] }
165
+ prop_set = request_match("/propertyupdate/set/prop/*").map { |e| [e.name, e.text] }
166
+
167
+ multistatus do |xml|
168
+ for resource in find_resources
169
+ xml.response do
170
+ xml.href "http://#{host}#{resource.path}"
171
+ propstats xml, set_properties(resource, prop_set)
172
+ end
173
+ end
174
+ end
175
+
176
+ resource.save
177
+ end
178
+
179
+ def lock
180
+ raise NotFound if not resource.exist?
181
+
182
+ lockscope = request_match("/lockinfo/lockscope/*")[0].name
183
+ locktype = request_match("/lockinfo/locktype/*")[0].name
184
+ owner = request_match("/lockinfo/owner/href")[0]
185
+ locktoken = "opaquelocktoken:" + sprintf('%x-%x-%s', Time.now.to_i, Time.now.sec, resource.etag)
186
+
187
+ response['Lock-Token'] = locktoken
188
+
189
+ render_xml do |xml|
190
+ xml.prop('xmlns:D' => "DAV:") do
191
+ xml.lockdiscovery do
192
+ xml.activelock do
193
+ xml.lockscope { xml.tag! lockscope }
194
+ xml.locktype { xml.tag! locktype }
195
+ xml.depth 'Infinity'
196
+ if owner
197
+ xml.owner { xml.href owner.text }
198
+ end
199
+ xml.timeout "Second-60"
200
+ xml.locktoken do
201
+ xml.href locktoken
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ def unlock
210
+ raise NoContent
211
+ end
212
+
213
+ # ************************************************************
214
+ # private methods
215
+
216
+ private
217
+
218
+ def env
219
+ @request.env
220
+ end
221
+
222
+ def host
223
+ env['HTTP_HOST']
224
+ end
225
+
226
+ def resource_class
227
+ @options[:resource_class]
228
+ end
229
+
230
+ def depth
231
+ case env['HTTP_DEPTH']
232
+ when '0' then 0
233
+ when '1' then 1
234
+ else 100
235
+ end
236
+ end
237
+
238
+ def overwrite
239
+ env['HTTP_OVERWRITE'].to_s.upcase != 'F'
240
+ end
241
+
242
+ def find_resources
243
+ case env['HTTP_DEPTH']
244
+ when '0'
245
+ [resource]
246
+ when '1'
247
+ [resource] + resource.children
248
+ else
249
+ [resource] + resource.descendants
250
+ end
251
+ end
252
+
253
+ def delete_recursive(res, errors)
254
+ for child in res.children
255
+ delete_recursive(child, errors)
256
+ end
257
+
258
+ begin
259
+ map_exceptions { res.delete } if errors.empty?
260
+ rescue Status
261
+ errors << [res.path, $!]
262
+ end
263
+ end
264
+
265
+ def copy_recursive(res, dest, depth, errors)
266
+ map_exceptions do
267
+ if dest.exist?
268
+ if overwrite
269
+ delete_recursive(dest, errors)
270
+ else
271
+ raise PreconditionFailed
272
+ end
273
+ end
274
+ res.copy(dest)
275
+ end
276
+ rescue Status
277
+ errors << [res.path, $!]
278
+ else
279
+ if depth > 0
280
+ for child in res.children
281
+ dest_child = dest.child(child.name)
282
+ copy_recursive(child, dest_child, depth - 1, errors)
283
+ end
284
+ end
285
+ end
286
+
287
+ def map_exceptions
288
+ yield
289
+ rescue
290
+ case $!
291
+ when URI::InvalidURIError then raise BadRequest
292
+ when Errno::EACCES then raise Forbidden
293
+ when Errno::ENOENT then raise Conflict
294
+ when Errno::EEXIST then raise Conflict
295
+ when Errno::ENOSPC then raise InsufficientStorage
296
+ else
297
+ raise
298
+ end
299
+ end
300
+
301
+ def request_document
302
+ @request_document ||= REXML::Document.new(request.body.read)
303
+ rescue REXML::ParseException
304
+ raise BadRequest
305
+ end
306
+
307
+ def request_match(pattern)
308
+ REXML::XPath::match(request_document, pattern, '' => 'DAV:')
309
+ end
310
+
311
+ def render_xml
312
+ xml = Builder::XmlMarkup.new(:indent => 2)
313
+ xml.instruct! :xml, :version => "1.0", :encoding => "UTF-8"
314
+
315
+ xml.namespace('D') do
316
+ yield xml
317
+ end
318
+
319
+ response.body = xml.target!
320
+ response["Content-Type"] = 'text/xml; charset="utf-8"'
321
+ response["Content-Length"] = response.body.size.to_s
322
+ end
323
+
324
+ def multistatus
325
+ render_xml do |xml|
326
+ xml.multistatus('xmlns:D' => "DAV:") do
327
+ yield xml
328
+ end
329
+ end
330
+
331
+ response.status = MultiStatus
332
+ end
333
+
334
+ def response_errors(xml, errors)
335
+ for path, status in errors
336
+ xml.response do
337
+ xml.href "http://#{host}#{path}"
338
+ xml.status "#{request.env['HTTP_VERSION']} #{status.status_line}"
339
+ end
340
+ end
341
+ end
342
+
343
+ def get_properties(resource, names)
344
+ stats = Hash.new { |h, k| h[k] = [] }
345
+ for name in names
346
+ begin
347
+ map_exceptions do
348
+ stats[OK] << [name, resource.get_property(name)]
349
+ end
350
+ rescue Status
351
+ stats[$!] << name
352
+ end
353
+ end
354
+ stats
355
+ end
356
+
357
+ def set_properties(resource, pairs)
358
+ stats = Hash.new { |h, k| h[k] = [] }
359
+ for name, value in pairs
360
+ begin
361
+ map_exceptions do
362
+ stats[OK] << [name, resource.set_property(name, value)]
363
+ end
364
+ rescue Status
365
+ stats[$!] << name
366
+ end
367
+ end
368
+ stats
369
+ end
370
+
371
+ def propstats(xml, stats)
372
+ return if stats.empty?
373
+ for status, props in stats
374
+ xml.propstat do
375
+ xml.prop do
376
+ for name, value in props
377
+ if value.is_a?(REXML::Element)
378
+ xml.tag!(name) do
379
+ rexml_convert(xml, value)
380
+ end
381
+ else
382
+ xml.tag!(name, value)
383
+ end
384
+ end
385
+ end
386
+ xml.status "#{request.env['HTTP_VERSION']} #{status.status_line}"
387
+ end
388
+ end
389
+ end
390
+
391
+ def rexml_convert(xml, element)
392
+ if element.elements.empty?
393
+ if element.text
394
+ xml.tag!(element.name, element.text, element.attributes)
395
+ else
396
+ xml.tag!(element.name, element.attributes)
397
+ end
398
+ else
399
+ xml.tag!(element.name, element.attributes) do
400
+ element.elements.each do |child|
401
+ rexml_convert(xml, child)
402
+ end
403
+ end
404
+ end
405
+ end
406
+
407
+ end
408
+
409
+ end