dav4rack 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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. The simple file resource allows very basic authentication
24
- which is used for an example. To enable it:
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 connecting
47
- directly to the host, we will have to connect to: http://host/webdav/share/
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 simple tell Rack to download and send the file from the provided resource and go away, allowing the
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 initialize(*args)
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
@@ -62,4 +62,14 @@ app = Rack::Builder.new do
62
62
 
63
63
  end.to_app
64
64
 
65
- Rack::Handler::WEBrick.run(app, :Port => 3000)
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
@@ -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 = 'chris@chrisroberts.org'
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
@@ -9,5 +9,5 @@ require 'dav4rack/handler'
9
9
  require 'dav4rack/controller'
10
10
 
11
11
  module DAV4Rack
12
- VERSION = '0.0.1'
12
+ VERSION = '0.1.0'
13
13
  end
@@ -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(response.status == 200 && !resource.collection?)
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.put(request, response)
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.make_collection
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
@@ -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.empty?
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
@@ -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
- @path = path
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
- @uri = URI.parse(@path)
22
- @con = Net::HTTP.new(@uri.host, @uri.port)
23
- @call_path = @uri.path + (@uri.query ? "?#{@uri.query}" : '')
24
- res = @con.request_get(@call_path)
25
- @heads = res.to_hash
26
- res.value
27
- @store = nil
28
- end
29
-
30
- def size
31
- @heads['content-length'] || @size
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 content_type
35
- @mime_type || @heads['content-type']
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 call(env)
60
+ def remote_call(env)
43
61
  dup._call(env)
44
62
  end
45
-
46
- def _call(env)
63
+
64
+ def remote__call(env)
47
65
  serving
48
66
  end
49
67
 
50
- def each
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
@@ -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
- def initialize(public_path, path, request, options)
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 recource exist?
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(webdrive cyberduck konqueror).any?{|x| (request.respond_to?(:user_agent) ? request.user_agent.downcase : request.env['HTTP_USER_AGENT'].downcase) =~ /#{Regexp.escape(x)}/}
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: 29
4
+ hash: 27
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 0
9
8
  - 1
10
- version: 0.0.1
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-03 00:00:00 -07:00
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: chris@chrisroberts.org
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: