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