georgi-rack_dav 0.1.1

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
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