dav4rack 0.0.1 → 0.1.0
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/README.rdoc +98 -10
- data/bin/dav4rack +11 -1
- data/dav4rack.gemspec +8 -3
- data/lib/dav4rack.rb +1 -1
- data/lib/dav4rack/controller.rb +54 -31
- data/lib/dav4rack/handler.rb +8 -2
- data/lib/dav4rack/interceptor_resource.rb +2 -2
- data/lib/dav4rack/lock.rb +40 -0
- data/lib/dav4rack/lock_store.rb +61 -0
- data/lib/dav4rack/remote_file.rb +57 -23
- data/lib/dav4rack/resource.rb +170 -10
- metadata +41 -8
data/README.rdoc
CHANGED
@@ -19,9 +19,10 @@ simple server with:
|
|
19
19
|
|
20
20
|
dav4rack
|
21
21
|
|
22
|
-
This will start a WEBrick server on port 3000, which you can connect
|
23
|
-
to without authentication.
|
24
|
-
|
22
|
+
This will start a Unicorn, Mongrel or WEBrick server on port 3000, which you can connect
|
23
|
+
to without authentication. Unicorn and Mongrel will be much more responsive than WEBrick,
|
24
|
+
so if you are having slowness issues, install one of them and restart the dav4rack process.
|
25
|
+
The simple file resource allows very basic authentication which is used for an example. To enable it:
|
25
26
|
|
26
27
|
dav4rack --username=user --password=pass
|
27
28
|
|
@@ -43,8 +44,8 @@ an example of how to use a custom resource:
|
|
43
44
|
run DAV4Rack::Handler.new(:resource_class => CustomResource, :custom => 'options', :passed => 'to resource')
|
44
45
|
|
45
46
|
Next, lets venture into mapping a path for our WebDAV access. In this example, we
|
46
|
-
will use default FileResource like in the first example, but instead of
|
47
|
-
|
47
|
+
will use default FileResource like in the first example, but instead of the WebDAV content
|
48
|
+
being available at the root directory, we will map it to a specific directory: /webdav/share/
|
48
49
|
|
49
50
|
require 'rubygems'
|
50
51
|
require 'dav4rack'
|
@@ -58,7 +59,7 @@ directly to the host, we will have to connect to: http://host/webdav/share/
|
|
58
59
|
}.to_app
|
59
60
|
run app
|
60
61
|
|
61
|
-
Aside from the #map block, notice the new option passed to the Handler's initialization, :root_uri_path. When
|
62
|
+
Aside from the Builder#map block, notice the new option passed to the Handler's initialization, :root_uri_path. When
|
62
63
|
DAV4Rack receives a request, it will automatically convert the request to the proper path and pass it to
|
63
64
|
the resource.
|
64
65
|
|
@@ -107,14 +108,13 @@ virtualized resources.
|
|
107
108
|
|
108
109
|
There are some helpers worth mentioning that make things a little easier. DAV4Rack::Resource#accept_redirect? method is available to Resources.
|
109
110
|
If true, the currently connected client will accept and properly use a 302 redirect for a GET request. Most clients do not properly
|
110
|
-
support this, which can be a real pain when working with virtualized files that may located some where else, like S3. To deal with
|
111
|
+
support this, which can be a real pain when working with virtualized files that may be located some where else, like S3. To deal with
|
111
112
|
those clients that don't support redirects, a helper has been provided so resources don't have to deal with proxying themselves. The
|
112
|
-
DAV4Rack::RemoteFile allows the resource to
|
113
|
+
DAV4Rack::RemoteFile allows the resource to simply tell Rack to download and send the file from the provided resource and go away, allowing the
|
113
114
|
process to be freed up to deal with other waiters. A very simple example:
|
114
115
|
|
115
116
|
class MyResource < DAV4Rack::Resource
|
116
|
-
def
|
117
|
-
super(*args)
|
117
|
+
def setup
|
118
118
|
@item = method_to_fill_this_properly
|
119
119
|
end
|
120
120
|
|
@@ -128,8 +128,96 @@ process to be freed up to deal with other waiters. A very simple example:
|
|
128
128
|
end
|
129
129
|
end
|
130
130
|
|
131
|
+
|
132
|
+
== Authentication
|
133
|
+
|
134
|
+
Authentication is performed on a per Resource basis. The Controller object will check the Resource for a Resource#authenticate method. If it exists,
|
135
|
+
any authentication information will be passed to the method. Depending on the result, the Controller will either continue on with the request, or send
|
136
|
+
a 401 Unauthorized response.
|
137
|
+
|
138
|
+
As a nicety, Resource#authentication_realm will be checked for existence and the returning string will be used as the Realm. Resource#authentication_error_msg
|
139
|
+
will also be checked for existence and the returning string will be passed in the response upon authentication failure.
|
140
|
+
|
141
|
+
Authentication can also be implemented using callbacks, as discussed below.
|
142
|
+
|
143
|
+
== Callbacks
|
144
|
+
|
145
|
+
Resources can make use of callbacks to easily apply permissions, authentication or any other action that needs to be performed before or after any or all
|
146
|
+
actions. Callbacks are applied to all publicly available methods. This is important for methods used internally within the resource. Methods not meant
|
147
|
+
to be called by the Controller, or anyone else, should be scoped protected or private to reduce the interaction with callbacks.
|
148
|
+
|
149
|
+
Callbacks can be called before or after a method call. For example:
|
150
|
+
|
151
|
+
class MyResource < DAV4Rack::Resource
|
152
|
+
before do |resource, method_name|
|
153
|
+
my_authentication_method
|
154
|
+
end
|
155
|
+
|
156
|
+
after do |resource, method_name|
|
157
|
+
puts "#{Time.now} -> Completed: #{resource}##{method_name}"
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def my_authentication_method
|
163
|
+
true
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
In this example MyResource#my_authentication_method will be called before any public method is called. After any method has been called a status
|
168
|
+
line will be printed to STDOUT. Running callbacks before/after every method call is a bit much in most cases, so callbacks can be applied to specific
|
169
|
+
methods:
|
170
|
+
|
171
|
+
class MyResource < DAV4Rack::Resource
|
172
|
+
before_get do |resource|
|
173
|
+
puts "#{Time.now} -> Received GET request from resource: #{resource}"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
In this example, a simple status line will be printed to STDOUT before the MyResource#get method is called. The current resource object is always
|
178
|
+
provided to callbacks. The method name is only provided to the generic before/after callbacks.
|
179
|
+
|
180
|
+
Something very handy for dealing with the mess of files OS X leaves on the system:
|
181
|
+
|
182
|
+
class MyResource < DAV4Rack::Resource
|
183
|
+
after_unlock do |resource|
|
184
|
+
resource.delete if resource.name[0,1] == '.'
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
Because OS X implements locking correctly, we can wait until it releases the lock on the file, and remove it if it's a hidden file.
|
189
|
+
|
190
|
+
Callbacks are called in the order they are defined, so you can easily build callbacks off each other. Like this example:
|
191
|
+
|
192
|
+
class MyResource < DAV4Rack::Resource
|
193
|
+
before do |resource, method_name|
|
194
|
+
resource.DAV_authenticate unless resource.user.is_a?(User)
|
195
|
+
raise Unauthorized unless resource.user.is_a?(User)
|
196
|
+
end
|
197
|
+
before do |resource, method_name|
|
198
|
+
resource.user.allowed?(method_name)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
In this example, the second block checking User#allowed? can count on Resource#user being defined because the blocks are called in
|
203
|
+
order, and if the Resource#user is not a User type, an exception is raised.
|
204
|
+
|
205
|
+
=== Avoiding callbacks
|
206
|
+
|
207
|
+
Something special to notice in the last example is the DAV_ prefix on authenticate. Providing the DAV_ prefix will prevent
|
208
|
+
any callbacks being applied to the given method. This allows us to provide a public method that the callback can access on the resource
|
209
|
+
without getting stuck in a loop.
|
210
|
+
|
131
211
|
== Issues/Bugs/Questions
|
132
212
|
|
213
|
+
=== Known Issues
|
214
|
+
|
215
|
+
* OS X Finder PUT fails when using NGINX (this is due to NGINX's lack of chunked transfer encoding)
|
216
|
+
* Windows WebDAV mini-redirector fails (this client is very broken. patches welcome.)
|
217
|
+
* Lots of unimplemented parts of the webdav spec (patches always welcome)
|
218
|
+
|
219
|
+
=== Unknown Issues
|
220
|
+
|
133
221
|
Please use the issues at github: http://github.com/chrisroberts/dav4rack
|
134
222
|
|
135
223
|
== License
|
data/bin/dav4rack
CHANGED
@@ -62,4 +62,14 @@ app = Rack::Builder.new do
|
|
62
62
|
|
63
63
|
end.to_app
|
64
64
|
|
65
|
-
|
65
|
+
begin
|
66
|
+
puts 'Attempting to start unicorn...'
|
67
|
+
require 'unicorn'
|
68
|
+
Unicorn.run(app, :listeners => ["0.0.0.0:3000"])
|
69
|
+
rescue => e
|
70
|
+
puts "Failed to start unicorn (#{e}). Starting mongrel..."
|
71
|
+
Rack::Handler::Mongrel.run(app, :Port => 3000)
|
72
|
+
rescue => e
|
73
|
+
puts "Failed to start mongrel (#{e}). Falling back to WEBrick."
|
74
|
+
Rack::Handler::WEBrick.run(app, :Port => 3000)
|
75
|
+
end
|
data/dav4rack.gemspec
CHANGED
@@ -1,16 +1,19 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + '/lib/'
|
1
2
|
require 'dav4rack'
|
2
3
|
Gem::Specification.new do |s|
|
3
4
|
s.name = 'dav4rack'
|
4
5
|
s.version = DAV4Rack::VERSION
|
5
6
|
s.summary = 'WebDAV handler for Rack'
|
6
7
|
s.author = 'Chris Roberts'
|
7
|
-
s.email = '
|
8
|
-
s.homepage = 'http://github.com/dav4rack'
|
8
|
+
s.email = 'chrisroberts.code@gmail.com'
|
9
|
+
s.homepage = 'http://github.com/chrisroberts/dav4rack'
|
9
10
|
s.description = 'WebDAV handler for Rack'
|
10
11
|
s.require_path = 'lib'
|
11
12
|
s.executables << 'dav4rack'
|
12
13
|
s.has_rdoc = true
|
13
|
-
s.extra_rdoc_files = ['README.rdoc']
|
14
|
+
s.extra_rdoc_files = ['README.rdoc']
|
15
|
+
s.add_dependency 'nokogiri', '~> 1.4.2'
|
16
|
+
s.add_dependency 'uuidtools', '~> 2.1.1'
|
14
17
|
s.files = %w{
|
15
18
|
.gitignore
|
16
19
|
LICENSE
|
@@ -24,6 +27,8 @@ lib/dav4rack/resource.rb
|
|
24
27
|
lib/dav4rack/interceptor.rb
|
25
28
|
lib/dav4rack/interceptor_resource.rb
|
26
29
|
lib/dav4rack/remote_file.rb
|
30
|
+
lib/dav4rack/lock.rb
|
31
|
+
lib/dav4rack/lock_store.rb
|
27
32
|
bin/dav4rack
|
28
33
|
spec/handler_spec.rb
|
29
34
|
README.rdoc
|
data/lib/dav4rack.rb
CHANGED
data/lib/dav4rack/controller.rb
CHANGED
@@ -11,12 +11,11 @@
|
|
11
11
|
# Create a new Controller.
|
12
12
|
# NOTE: options will be passed to Resource
|
13
13
|
def initialize(request, response, options={})
|
14
|
+
raise Forbidden if request.path_info.include?('../')
|
14
15
|
@request = request
|
15
16
|
@response = response
|
16
17
|
@options = options
|
17
|
-
@resource = resource_class.new(actual_path, implied_path, @request, @options)
|
18
|
-
authenticate
|
19
|
-
raise Forbidden if request.path_info.include?('../')
|
18
|
+
@resource = resource_class.new(actual_path, implied_path, @request, @response, @options)
|
20
19
|
end
|
21
20
|
|
22
21
|
# s:: string
|
@@ -56,7 +55,7 @@
|
|
56
55
|
def get
|
57
56
|
raise NotFound unless resource.exist?
|
58
57
|
res = resource.get(request, response)
|
59
|
-
if(
|
58
|
+
if(res == OK && !resource.collection?)
|
60
59
|
response['Etag'] = resource.etag
|
61
60
|
response['Content-Type'] = resource.content_type
|
62
61
|
response['Content-Length'] = resource.content_length.to_s
|
@@ -68,7 +67,14 @@
|
|
68
67
|
# Return response to PUT
|
69
68
|
def put
|
70
69
|
raise Forbidden if resource.collection?
|
71
|
-
resource.
|
70
|
+
resource.lock_check
|
71
|
+
status = resource.put(request, response)
|
72
|
+
multistatus do |xml|
|
73
|
+
xml.response do
|
74
|
+
xml.href resource.path
|
75
|
+
xml.status "#{http_version} #{status.status_line}"
|
76
|
+
end
|
77
|
+
end
|
72
78
|
end
|
73
79
|
|
74
80
|
# Return response to POST
|
@@ -79,12 +85,20 @@
|
|
79
85
|
# Return response to DELETE
|
80
86
|
def delete
|
81
87
|
raise NotFound unless resource.exist?
|
88
|
+
resource.lock_check
|
82
89
|
resource.delete
|
83
90
|
end
|
84
91
|
|
85
92
|
# Return response to MKCOL
|
86
93
|
def mkcol
|
87
|
-
resource.
|
94
|
+
resource.lock_check
|
95
|
+
status = resource.make_collection
|
96
|
+
multistatus do |xml|
|
97
|
+
xml.response do
|
98
|
+
xml.href resource.path
|
99
|
+
xml.status "#{http_version} #{status.status_line}"
|
100
|
+
end
|
101
|
+
end
|
88
102
|
end
|
89
103
|
|
90
104
|
# Return response to COPY
|
@@ -97,16 +111,25 @@
|
|
97
111
|
# Resource will be copied (implementation ease)
|
98
112
|
def move(*args)
|
99
113
|
raise NotFound unless resource.exist?
|
114
|
+
resource.lock_check unless args.include?(:copy)
|
100
115
|
dest_uri = URI.parse(env['HTTP_DESTINATION'])
|
101
116
|
destination = url_unescape(dest_uri.path)
|
102
117
|
raise BadGateway if dest_uri.host and dest_uri.host != request.host
|
103
118
|
raise Forbidden if destination == resource.public_path
|
104
|
-
dest = resource_class.new(destination, clean_path(destination), @request, @options)
|
119
|
+
dest = resource_class.new(destination, clean_path(destination), @request, @response, @options.merge(:user => resource.user))
|
120
|
+
status = nil
|
105
121
|
if(args.include?(:copy))
|
106
|
-
resource.copy(dest, overwrite)
|
122
|
+
status = resource.copy(dest, overwrite)
|
107
123
|
else
|
108
124
|
raise Conflict unless depth.is_a?(Symbol) || depth > 1
|
109
|
-
resource.move(dest)
|
125
|
+
status = resource.move(dest)
|
126
|
+
end
|
127
|
+
response['Location'] = "#{scheme}://#{host}:#{port}#{dest.public_path}" if status == Created
|
128
|
+
multistatus do |xml|
|
129
|
+
xml.response do
|
130
|
+
xml.href "#{scheme}://#{host}:#{port}#{dest.public_path}"
|
131
|
+
xml.status = "#{http_version} #{status.status_line}"
|
132
|
+
end
|
110
133
|
end
|
111
134
|
end
|
112
135
|
|
@@ -132,6 +155,7 @@
|
|
132
155
|
# Return response to PROPPATCH
|
133
156
|
def proppatch
|
134
157
|
raise NotFound unless resource.exist?
|
158
|
+
resource.lock_check
|
135
159
|
prop_rem = request_match('/propertyupdate/remove/prop').children.map{|n| [n.name] }
|
136
160
|
prop_set = request_match('/propertyupdate/set/prop').children.map{|n| [n.name, n.text] }
|
137
161
|
multistatus do |xml|
|
@@ -191,7 +215,6 @@
|
|
191
215
|
end
|
192
216
|
end
|
193
217
|
end
|
194
|
-
response.status = MultiStatus
|
195
218
|
end
|
196
219
|
end
|
197
220
|
|
@@ -200,6 +223,23 @@
|
|
200
223
|
resource.unlock(lock_token)
|
201
224
|
end
|
202
225
|
|
226
|
+
# Perform authentication
|
227
|
+
# NOTE: Authentication will only be performed if the Resource
|
228
|
+
# has defined an #authenticate method
|
229
|
+
def authenticate
|
230
|
+
authed = true
|
231
|
+
if(resource.respond_to?(:authenticate))
|
232
|
+
authed = false
|
233
|
+
if(request.env['HTTP_AUTHORIZATION'])
|
234
|
+
auth = Rack::Auth::Basic::Request.new(request.env)
|
235
|
+
if(auth.basic? && auth.credentials)
|
236
|
+
authed = resource.authenticate(auth.credentials[0], auth.credentials[1])
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
raise Unauthorized unless authed
|
241
|
+
end
|
242
|
+
|
203
243
|
# ************************************************************
|
204
244
|
# private methods
|
205
245
|
|
@@ -363,6 +403,8 @@
|
|
363
403
|
begin
|
364
404
|
val = resource.get_property(name)
|
365
405
|
stats[OK].push [name, val] if val
|
406
|
+
rescue Unauthorized => u
|
407
|
+
raise u
|
366
408
|
rescue Status
|
367
409
|
stats[$!] << name
|
368
410
|
end
|
@@ -378,6 +420,8 @@
|
|
378
420
|
for name, value in pairs
|
379
421
|
begin
|
380
422
|
stats[OK] << [name, resource.set_property(name, value)]
|
423
|
+
rescue Unauthorized => u
|
424
|
+
raise u
|
381
425
|
rescue Status
|
382
426
|
stats[$!] << name
|
383
427
|
end
|
@@ -431,27 +475,6 @@
|
|
431
475
|
end
|
432
476
|
end
|
433
477
|
|
434
|
-
# Perform authentication
|
435
|
-
# NOTE: Authentication will only be performed if the Resource
|
436
|
-
# has defined an #authenticate method
|
437
|
-
def authenticate
|
438
|
-
authed = true
|
439
|
-
if(resource.respond_to?(:authenticate))
|
440
|
-
authed = false
|
441
|
-
if(request.env['HTTP_AUTHORIZATION'])
|
442
|
-
auth = Rack::Auth::Basic::Request.new(request.env)
|
443
|
-
if(auth.basic? && auth.credentials)
|
444
|
-
authed = resource.authenticate(auth.credentials[0], auth.credentials[1])
|
445
|
-
end
|
446
|
-
end
|
447
|
-
end
|
448
|
-
unless(authed)
|
449
|
-
response.body = resource.respond_to?(:authentication_error_msg) ? resource.authentication_error_msg : 'Not Authorized'
|
450
|
-
response['WWW-Authenticate'] = "Basic realm=\"#{resource.respond_to?(:authentication_realm) ? resource.authentication_realm : 'Locked content'}\""
|
451
|
-
raise Unauthorized.new unless authed
|
452
|
-
end
|
453
|
-
end
|
454
|
-
|
455
478
|
end
|
456
479
|
|
457
480
|
end
|
data/lib/dav4rack/handler.rb
CHANGED
@@ -16,10 +16,16 @@ module DAV4Rack
|
|
16
16
|
request = Rack::Request.new(env)
|
17
17
|
response = Rack::Response.new
|
18
18
|
|
19
|
+
controller = nil
|
19
20
|
begin
|
20
21
|
controller = Controller.new(request, response, @options.dup)
|
22
|
+
controller.authenticate
|
21
23
|
res = controller.send(request.request_method.downcase)
|
22
24
|
response.status = res.code if res.respond_to?(:code)
|
25
|
+
rescue HTTPStatus::Unauthorized => status
|
26
|
+
response.body = controller.resource.respond_to?(:authentication_error_msg) ? controller.resource.authentication_error_msg : 'Not Authorized'
|
27
|
+
response['WWW-Authenticate'] = "Basic realm=\"#{controller.resource.respond_to?(:authentication_realm) ? controller.resource.authentication_realm : 'Locked content'}\""
|
28
|
+
response.status = status.code
|
23
29
|
rescue HTTPStatus::Status => status
|
24
30
|
response.status = status.code
|
25
31
|
end
|
@@ -27,11 +33,11 @@ module DAV4Rack
|
|
27
33
|
# Strings in Ruby 1.9 are no longer enumerable. Rack still expects the response.body to be
|
28
34
|
# enumerable, however.
|
29
35
|
|
30
|
-
response['Content-Length'] = response.body.to_s.length unless response['Content-Length'] || response.body.
|
36
|
+
response['Content-Length'] = response.body.to_s.length unless response['Content-Length'] || !response.body.is_a?(String)
|
31
37
|
response.body = [response.body] if not response.body.respond_to? :each
|
32
38
|
response.status = response.status ? response.status.to_i : 200
|
33
39
|
response.headers.each_pair{|k,v| response[k] = v.to_s}
|
34
|
-
|
40
|
+
|
35
41
|
# Apache wants the body dealt with, so just read it and junk it
|
36
42
|
buf = true
|
37
43
|
buf = request.body.read(8192) while buf
|
@@ -99,9 +99,9 @@ module DAV4Rack
|
|
99
99
|
new_public.slice!(-1) if new_public[-1,1] == '/'
|
100
100
|
new_public = "#{new_public}#{name}"
|
101
101
|
if(key = @root_paths.find{|x| new_path =~ /^#{Regexp.escape(x.downcase)}\/?/})
|
102
|
-
@mappings[key][:resource_class].new(new_public, new_path.gsub(key, ''), request, {:root_uri_path => key}.merge(@mappings[key][:options] ? @mappings[key][:options] : options))
|
102
|
+
@mappings[key][:resource_class].new(new_public, new_path.gsub(key, ''), request, response, {:root_uri_path => key, :user => @user}.merge(@mappings[key][:options] ? @mappings[key][:options] : options))
|
103
103
|
else
|
104
|
-
self.class.new(new_public, new_path, request, options)
|
104
|
+
self.class.new(new_public, new_path, request, response, {:user => @user}.merge(options))
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module DAV4Rack
|
2
|
+
class Lock
|
3
|
+
|
4
|
+
def initialize(args={})
|
5
|
+
@args = args
|
6
|
+
@store = nil
|
7
|
+
@args[:created_at] = Time.now
|
8
|
+
@args[:updated_at] = Time.now
|
9
|
+
end
|
10
|
+
|
11
|
+
def store
|
12
|
+
@store
|
13
|
+
end
|
14
|
+
|
15
|
+
def store=(s)
|
16
|
+
raise TypeError.new 'Expecting LockStore' unless s.respond_to? :remove
|
17
|
+
@store = s
|
18
|
+
end
|
19
|
+
|
20
|
+
def destroy
|
21
|
+
if(@store)
|
22
|
+
@store.remove(self)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def remaining_timeout
|
27
|
+
@args[:timeout].to_i - (Time.now.to_i - @args[:created_at].to_i)
|
28
|
+
end
|
29
|
+
|
30
|
+
def method_missing(*args)
|
31
|
+
if(@args.has_key?(args.first.to_sym))
|
32
|
+
@args[args.first.to_sym]
|
33
|
+
elsif(args.first.to_s[-1,1] == '=')
|
34
|
+
@args[args.first.to_s[0, args.first.to_s.length - 1].to_sym] = args[1]
|
35
|
+
else
|
36
|
+
raise NoMethodError.new "Undefined method #{args.first} for #{self}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'dav4rack/lock'
|
2
|
+
module DAV4Rack
|
3
|
+
class LockStore
|
4
|
+
class << self
|
5
|
+
def create
|
6
|
+
@locks_by_path = {}
|
7
|
+
@locks_by_token = {}
|
8
|
+
end
|
9
|
+
def add(lock)
|
10
|
+
@locks_by_path[lock.path] = lock
|
11
|
+
@locks_by_token[lock.token] = lock
|
12
|
+
end
|
13
|
+
|
14
|
+
def remove(lock)
|
15
|
+
@locks_by_path.delete(lock.path)
|
16
|
+
@locks_by_token.delete(lock.token)
|
17
|
+
end
|
18
|
+
|
19
|
+
def find_by_path(path)
|
20
|
+
@locks_by_path.map do |lpath, lock|
|
21
|
+
lpath == path && lock.remaining_timeout > 0 ? lock : nil
|
22
|
+
end.compact.first
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_by_token(token)
|
26
|
+
@locks_by_token.map do |ltoken, lock|
|
27
|
+
ltoken == token && lock.remaining_timeout > 0 ? lock : nil
|
28
|
+
end.compact.first
|
29
|
+
end
|
30
|
+
|
31
|
+
def explicit_locks(path)
|
32
|
+
@locks_by_path.map do |lpath, lock|
|
33
|
+
lpath == path && lock.remaining_timeout > 0 ? lock : nil
|
34
|
+
end.compact
|
35
|
+
end
|
36
|
+
|
37
|
+
def implicit_locks(path)
|
38
|
+
@locks_by_path.map do |lpath, lock|
|
39
|
+
lpath =~ /^#{Regexp.escape(path)}/ && lock.remaining_timeout > 0 && lock.depth > 0 ? lock : nil
|
40
|
+
end.compact
|
41
|
+
end
|
42
|
+
|
43
|
+
def explicitly_locked?(path)
|
44
|
+
self.explicit_locks(path).size > 0
|
45
|
+
end
|
46
|
+
|
47
|
+
def implicitly_locked?(path)
|
48
|
+
self.implicit_locks(path).size > 0
|
49
|
+
end
|
50
|
+
|
51
|
+
def generate(path, user, token)
|
52
|
+
l = Lock.new(:path => path, :user => user, :token => token)
|
53
|
+
l.store = self
|
54
|
+
add(l)
|
55
|
+
l
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
DAV4Rack::LockStore.create
|
data/lib/dav4rack/remote_file.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'net/http'
|
2
2
|
require 'uri'
|
3
|
+
require 'digest/sha1'
|
4
|
+
require 'rack/file'
|
3
5
|
|
4
6
|
module DAV4Rack
|
5
7
|
|
6
|
-
class RemoteFile
|
8
|
+
class RemoteFile < Rack::File
|
7
9
|
|
8
10
|
attr_accessor :path
|
9
11
|
|
@@ -14,49 +16,81 @@ module DAV4Rack
|
|
14
16
|
# Create a reference to a remote file.
|
15
17
|
# NOTE: HTTPError will be raised if path does not return 200 result
|
16
18
|
def initialize(path, args={})
|
17
|
-
@
|
19
|
+
@fpath = args[:url]
|
18
20
|
@size = args[:size] || nil
|
19
21
|
@mime_type = args[:mime_type] || 'text/plain'
|
20
22
|
@modified = args[:last_modified] || nil
|
21
|
-
@
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
23
|
+
@cache = args[:cache_directory] || nil
|
24
|
+
cached_file = @cache + '/' + Digest::SHA1.hexdigest(@fpath)
|
25
|
+
if(File.exists?(cached_file))
|
26
|
+
@root = ''
|
27
|
+
@path_info = cached_file
|
28
|
+
@path = @path_info
|
29
|
+
else
|
30
|
+
begin
|
31
|
+
@cf = File.open(cached_file, 'w+')
|
32
|
+
rescue
|
33
|
+
@cf = nil
|
34
|
+
end
|
35
|
+
@uri = URI.parse(path)
|
36
|
+
@con = Net::HTTP.new(@uri.host, @uri.port)
|
37
|
+
@call_path = @uri.path + (@uri.query ? "?#{@uri.query}" : '')
|
38
|
+
res = @con.request_get(@call_path)
|
39
|
+
@heads = res.to_hash
|
40
|
+
res.value
|
41
|
+
@store = nil
|
42
|
+
self.public_methods.each do |method|
|
43
|
+
m = method.to_s.dup
|
44
|
+
next unless m.slice!(0,7) == 'remote_'
|
45
|
+
self.class.class_eval "undef :'#{m}'"
|
46
|
+
self.class.class_eval "alias :'#{m}' :'#{method}'"
|
47
|
+
end
|
48
|
+
end
|
32
49
|
end
|
33
|
-
|
34
|
-
def
|
35
|
-
|
50
|
+
|
51
|
+
def remote_serving
|
52
|
+
[200, {
|
53
|
+
"Last-Modified" => last_modified,
|
54
|
+
"Content-Type" => content_type,
|
55
|
+
"Content-Length" => size
|
56
|
+
}, self]
|
36
57
|
end
|
37
58
|
|
38
|
-
def last_modified
|
39
|
-
@heads['last-modified'] || @modified
|
40
|
-
end
|
41
59
|
|
42
|
-
def
|
60
|
+
def remote_call(env)
|
43
61
|
dup._call(env)
|
44
62
|
end
|
45
|
-
|
46
|
-
def
|
63
|
+
|
64
|
+
def remote__call(env)
|
47
65
|
serving
|
48
66
|
end
|
49
67
|
|
50
|
-
def
|
68
|
+
def remote_each
|
51
69
|
if(@store)
|
52
70
|
yield @store
|
53
71
|
else
|
54
72
|
@con.request_get(@call_path) do |res|
|
55
73
|
res.read_body(@store) do |part|
|
74
|
+
@cf.write part if @cf
|
56
75
|
yield part
|
57
76
|
end
|
58
77
|
end
|
59
78
|
end
|
60
79
|
end
|
80
|
+
|
81
|
+
def size
|
82
|
+
@heads['content-length'] || @size
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def content_type
|
88
|
+
@mime_type || @heads['content-type']
|
89
|
+
end
|
90
|
+
|
91
|
+
def last_modified
|
92
|
+
@heads['last-modified'] || @modified
|
93
|
+
end
|
94
|
+
|
61
95
|
end
|
62
96
|
end
|
data/lib/dav4rack/resource.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'uuidtools'
|
2
|
+
|
1
3
|
module DAV4Rack
|
2
4
|
|
3
5
|
class LockFailure < RuntimeError
|
@@ -13,7 +15,36 @@ module DAV4Rack
|
|
13
15
|
end
|
14
16
|
|
15
17
|
class Resource
|
16
|
-
attr_reader :path, :options, :public_path, :request
|
18
|
+
attr_reader :path, :options, :public_path, :request, :response
|
19
|
+
attr_accessor :user
|
20
|
+
@@blocks = {}
|
21
|
+
|
22
|
+
class << self
|
23
|
+
|
24
|
+
# This lets us define a bunch of before and after blocks that are
|
25
|
+
# either called before all methods on the resource, or only specific
|
26
|
+
# methods on the resource
|
27
|
+
def method_missing(*args, &block)
|
28
|
+
class_sym = self.name.to_sym
|
29
|
+
@@blocks[class_sym] ||= {:before => {}, :after => {}}
|
30
|
+
m = args.shift
|
31
|
+
parts = m.to_s.split('_')
|
32
|
+
type = parts.shift.to_s.to_sym
|
33
|
+
method = parts.empty? ? nil : parts.join('_').to_sym
|
34
|
+
if(@@blocks[class_sym][type] && block_given?)
|
35
|
+
if(method)
|
36
|
+
@@blocks[class_sym][type][method] ||= []
|
37
|
+
@@blocks[class_sym][type][method] << block
|
38
|
+
else
|
39
|
+
@@blocks[class_sym][type][:'__all__'] ||= []
|
40
|
+
@@blocks[class_sym][type][:'__all__'] << block
|
41
|
+
end
|
42
|
+
else
|
43
|
+
raise NoMethodError.new("Undefined method #{m} for class #{self}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
17
48
|
|
18
49
|
include DAV4Rack::HTTPStatus
|
19
50
|
|
@@ -28,13 +59,56 @@ module DAV4Rack
|
|
28
59
|
# request -> /my/webdav/directory/actual/path
|
29
60
|
# public_path -> /my/webdav/directory/actual/path
|
30
61
|
# path -> /actual/path
|
31
|
-
|
62
|
+
# NOTE: Customized Resources should not use initialize for setup. Instead
|
63
|
+
# use the #setup method
|
64
|
+
def initialize(public_path, path, request, response, options)
|
65
|
+
@skip_alias = [:authenticate, :authentication_error_msg, :authentication_realm, :path, :options, :public_path, :request, :response, :user, :user=]
|
32
66
|
@public_path = public_path.dup
|
33
67
|
@path = path.dup
|
34
68
|
@request = request
|
69
|
+
@response = response
|
70
|
+
unless(options.has_key?(:lock_class))
|
71
|
+
require 'dav4rack/lock_store'
|
72
|
+
@lock_class = LockStore
|
73
|
+
else
|
74
|
+
@lock_class = options[:lock_class]
|
75
|
+
raise NameError.new("Unknown lock type constant provided: #{@lock_class}") unless @lock_class.nil? || defined?(@lock_class)
|
76
|
+
end
|
35
77
|
@options = options.dup
|
78
|
+
@max_timeout = options[:max_timeout] || 86400
|
79
|
+
@default_timeout = options[:default_timeout] || 60
|
80
|
+
@user = @options[:user] || request.ip
|
81
|
+
setup if respond_to?(:setup)
|
82
|
+
public_methods(false).each do |method|
|
83
|
+
next if @skip_alias.include?(method.to_sym) || method[0,4] == 'DAV_' || method[0,5] == '_DAV_'
|
84
|
+
self.class.class_eval "alias :'_DAV_#{method}' :'#{method}'"
|
85
|
+
self.class.class_eval "undef :'#{method}'"
|
86
|
+
end
|
87
|
+
@runner = lambda do |class_sym, kind, method_name|
|
88
|
+
[:'__all__', method_name.to_sym].each do |sym|
|
89
|
+
if(@@blocks[class_sym] && @@blocks[class_sym][kind] && @@blocks[class_sym][kind][sym])
|
90
|
+
@@blocks[class_sym][kind][sym].each do |b|
|
91
|
+
args = [self, sym == :'__all__' ? method_name : nil].compact
|
92
|
+
b.call(*args)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
36
97
|
end
|
37
|
-
|
98
|
+
|
99
|
+
# This allows us to call before and after blocks
|
100
|
+
def method_missing(*args)
|
101
|
+
result = nil
|
102
|
+
orig = args.shift
|
103
|
+
class_sym = self.class.name.to_sym
|
104
|
+
m = orig.to_s[0,5] == '_DAV_' ? orig : "_DAV_#{orig}" # If hell is doing the same thing over and over and expecting a different result this is a hell preventer
|
105
|
+
raise NoMethodError.new("Undefined method: #{orig} for class #{self}.") unless respond_to?(m)
|
106
|
+
@runner.call(class_sym, :before, orig)
|
107
|
+
result = send m, *args
|
108
|
+
@runner.call(class_sym, :after, orig)
|
109
|
+
result
|
110
|
+
end
|
111
|
+
|
38
112
|
# If this is a collection, return the child resources.
|
39
113
|
def children
|
40
114
|
raise NotImplementedError
|
@@ -45,11 +119,16 @@ module DAV4Rack
|
|
45
119
|
raise NotImplementedError
|
46
120
|
end
|
47
121
|
|
48
|
-
# Does this
|
122
|
+
# Does this resource exist?
|
49
123
|
def exist?
|
50
124
|
raise NotImplementedError
|
51
125
|
end
|
52
126
|
|
127
|
+
# Does the parent resource exist?
|
128
|
+
def parent_exists?
|
129
|
+
parent.exist?
|
130
|
+
end
|
131
|
+
|
53
132
|
# Return the creation time.
|
54
133
|
def creation_date
|
55
134
|
raise NotImplementedError
|
@@ -142,14 +221,77 @@ module DAV4Rack
|
|
142
221
|
# NOTE: See section 9.10 of RFC 4918 for guidance about
|
143
222
|
# how locks should be generated and the expected responses
|
144
223
|
# (http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10)
|
224
|
+
|
145
225
|
def lock(args)
|
146
|
-
raise NotImplemented
|
226
|
+
raise NotImplemented if @lock_class.nil?
|
227
|
+
raise Conflict unless parent_exists?
|
228
|
+
lock_check(args[:scope])
|
229
|
+
lock = @lock_class.explicit_locks(@path).find{|l| l.scope == args[:scope] && l.kind == args[:type] && l.user == @user}
|
230
|
+
unless(lock)
|
231
|
+
token = UUIDTools::UUID.random_create.to_s
|
232
|
+
lock = @lock_class.generate(@path, @user, token)
|
233
|
+
lock.scope = args[:scope]
|
234
|
+
lock.kind = args[:type]
|
235
|
+
lock.owner = args[:owner]
|
236
|
+
lock.depth = args[:depth].to_i
|
237
|
+
if(args[:timeout])
|
238
|
+
lock.timeout = args[:timeout] <= @max_timeout && args[:timeout] > 0 ? args[:timeout] : @max_timeout
|
239
|
+
else
|
240
|
+
lock.timeout = @default_timeout
|
241
|
+
end
|
242
|
+
lock.save if lock.respond_to? :save
|
243
|
+
end
|
244
|
+
begin
|
245
|
+
lock_check(args[:type])
|
246
|
+
rescue DAV4Rack::LockFailure => e
|
247
|
+
lock.destroy
|
248
|
+
raise e
|
249
|
+
end
|
250
|
+
[lock.remaining_timeout, lock.token]
|
251
|
+
end
|
252
|
+
|
253
|
+
# lock_scope:: scope of lock
|
254
|
+
# Check if resource is locked. Raise DAV4Rack::LockFailure if locks are in place.
|
255
|
+
def lock_check(lock_scope=nil)
|
256
|
+
return unless @lock_class
|
257
|
+
if(@lock_class.explicitly_locked?(@path))
|
258
|
+
raise Locked if @lock_class.explicit_locks(@path).find_all{|l|l.scope == 'exclusive' && l.user != @user}.size > 0
|
259
|
+
elsif(@lock_class.implicitly_locked?(@path))
|
260
|
+
if(lock_scope.to_s == 'exclusive')
|
261
|
+
locks = @lock_class.implicit_locks(@path)
|
262
|
+
failure = DAV4Rack::LockFailure.new("Failed to lock: #{@path}")
|
263
|
+
locks.each do |lock|
|
264
|
+
failure.add_failure(@path, Locked)
|
265
|
+
end
|
266
|
+
raise failure
|
267
|
+
else
|
268
|
+
locks = @lock_class.implict_locks(@path).find_all{|l| l.scope == 'exclusive' && l.user != @user}
|
269
|
+
if(locks.size > 0)
|
270
|
+
failure = LockFailure.new("Failed to lock: #{@path}")
|
271
|
+
locks.each do |lock|
|
272
|
+
failure.add_failure(@path, Locked)
|
273
|
+
end
|
274
|
+
raise failure
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
147
278
|
end
|
148
279
|
|
280
|
+
# token:: Lock token
|
281
|
+
# Remove the given lock
|
149
282
|
def unlock(token)
|
150
|
-
raise NotImplemented
|
283
|
+
raise NotImplemented if @lock_class.nil?
|
284
|
+
token = token.slice(1, token.length - 2)
|
285
|
+
raise BadRequest if token.nil? || token.empty?
|
286
|
+
lock = @lock_class.find_by_token(token)
|
287
|
+
raise Forbidden unless lock && lock.user == @user
|
288
|
+
raise Conflict unless lock.path =~ /^#{Regexp.escape(@path)}.*$/
|
289
|
+
lock.destroy
|
290
|
+
delete if name[0,1] == '.' && @options[:delete_dotfiles]
|
291
|
+
NoContent
|
151
292
|
end
|
152
|
-
|
293
|
+
|
294
|
+
|
153
295
|
# Create this resource as collection.
|
154
296
|
def make_collection
|
155
297
|
raise NotImplementedError
|
@@ -220,14 +362,14 @@ module DAV4Rack
|
|
220
362
|
new_path = path.dup
|
221
363
|
new_path = new_path + '/' unless new_path[-1,1] == '/'
|
222
364
|
new_path = '/' + new_path unless new_path[0,1] == '/'
|
223
|
-
self.class.new("#{new_public}#{name}", "#{new_path}#{name}", request, options)
|
365
|
+
self.class.new("#{new_public}#{name}", "#{new_path}#{name}", request, response, options.merge(:user => @user))
|
224
366
|
end
|
225
367
|
|
226
368
|
# Return parent of this resource
|
227
369
|
def parent
|
228
370
|
elements = @path.scan(/[^\/]+/)
|
229
371
|
return nil if elements.empty?
|
230
|
-
self.class.new(('/' + @public_path.scan(/[^\/]+/)[0..-2].join('/')), ('/' + elements[0..-2].to_a.join('/')), @request, @options)
|
372
|
+
self.class.new(('/' + @public_path.scan(/[^\/]+/)[0..-2].join('/')), ('/' + elements[0..-2].to_a.join('/')), @request, @response, @options.merge(:user => @user))
|
231
373
|
end
|
232
374
|
|
233
375
|
# Return list of descendants
|
@@ -239,10 +381,28 @@ module DAV4Rack
|
|
239
381
|
end
|
240
382
|
list
|
241
383
|
end
|
384
|
+
|
385
|
+
protected
|
386
|
+
|
387
|
+
# Index page template for GETs on collection
|
388
|
+
def index_page
|
389
|
+
'<html><head> <title>%s</title>
|
390
|
+
<meta http-equiv="content-type" content="text/html; charset=utf-8" /></head>
|
391
|
+
<body> <h1>%s</h1> <hr /> <table> <tr> <th class="name">Name</th>
|
392
|
+
<th class="size">Size</th> <th class="type">Type</th>
|
393
|
+
<th class="mtime">Last Modified</th> </tr> %s </table> <hr /> </body></html>'
|
394
|
+
end
|
242
395
|
|
243
396
|
# Does client allow GET redirection
|
244
397
|
def allows_redirect?
|
245
|
-
%w(
|
398
|
+
%w(cyberduck konqueror).any?{|x| (request.respond_to?(:user_agent) ? request.user_agent.to_s.downcase : request.env['HTTP_USER_AGENT'].to_s.downcase) =~ /#{Regexp.escape(x)}/}
|
399
|
+
end
|
400
|
+
|
401
|
+
# Returns authentication credentials if available in form of [username,password]
|
402
|
+
# TODO: Add support for digest
|
403
|
+
def auth_credentials
|
404
|
+
auth = Rack::Auth::Basic::Request.new(request.env)
|
405
|
+
auth.basic? && auth.credentials ? auth.credentials : [nil,nil]
|
246
406
|
end
|
247
407
|
|
248
408
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dav4rack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
- 0
|
9
8
|
- 1
|
10
|
-
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Chris Roberts
|
@@ -15,12 +15,43 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-08-
|
18
|
+
date: 2010-08-10 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
|
-
dependencies:
|
21
|
-
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: nokogiri
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 4
|
33
|
+
- 2
|
34
|
+
version: 1.4.2
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: uuidtools
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 9
|
46
|
+
segments:
|
47
|
+
- 2
|
48
|
+
- 1
|
49
|
+
- 1
|
50
|
+
version: 2.1.1
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
22
53
|
description: WebDAV handler for Rack
|
23
|
-
email:
|
54
|
+
email: chrisroberts.code@gmail.com
|
24
55
|
executables:
|
25
56
|
- dav4rack
|
26
57
|
extensions: []
|
@@ -40,11 +71,13 @@ files:
|
|
40
71
|
- lib/dav4rack/interceptor.rb
|
41
72
|
- lib/dav4rack/interceptor_resource.rb
|
42
73
|
- lib/dav4rack/remote_file.rb
|
74
|
+
- lib/dav4rack/lock.rb
|
75
|
+
- lib/dav4rack/lock_store.rb
|
43
76
|
- bin/dav4rack
|
44
77
|
- spec/handler_spec.rb
|
45
78
|
- README.rdoc
|
46
79
|
has_rdoc: true
|
47
|
-
homepage: http://github.com/dav4rack
|
80
|
+
homepage: http://github.com/chrisroberts/dav4rack
|
48
81
|
licenses: []
|
49
82
|
|
50
83
|
post_install_message:
|