dav4rack 0.2.11 → 0.3.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.
@@ -17,7 +17,7 @@ module DAV4Rack
17
17
  def _call(env)
18
18
  begin
19
19
  if F.file?(@path) && F.readable?(@path)
20
- serving
20
+ serving(env)
21
21
  else
22
22
  raise Errno::EPERM
23
23
  end
@@ -0,0 +1,159 @@
1
+ require 'pstore'
2
+
3
+ module DAV4Rack
4
+ class FileResourceLock
5
+ attr_accessor :path
6
+ attr_accessor :token
7
+ attr_accessor :timeout
8
+ attr_accessor :depth
9
+ attr_accessor :user
10
+ attr_accessor :scope
11
+ attr_accessor :kind
12
+ attr_accessor :owner
13
+ attr_reader :created_at
14
+ attr_reader :root
15
+
16
+ class << self
17
+ def explicitly_locked?(path, croot=nil)
18
+ store = init_pstore(croot)
19
+ !!store.transaction(true){
20
+ store[:paths][path]
21
+ }
22
+ end
23
+
24
+ def implicitly_locked?(path, croot=nil)
25
+ store = init_pstore(croot)
26
+ !!store.transaction(true){
27
+ store[:paths].keys.detect do |check|
28
+ check.start_with?(path)
29
+ end
30
+ }
31
+ end
32
+
33
+ def explicit_locks(path, croot, args={})
34
+ end
35
+
36
+ def find_by_path(path, croot=nil)
37
+ lock = self.class.new(:path => path, :root => croot)
38
+ lock.token.nil? ? nil : lock
39
+ end
40
+
41
+ def find_by_token(token, croot=nil)
42
+ store = init_pstore(croot)
43
+ struct = store.transaction(true){
44
+ store[:tokens][token]
45
+ }
46
+ if(tok)
47
+ self.class.new(:path => struct[:path], :root => croot)
48
+ else
49
+ nil
50
+ end
51
+ end
52
+
53
+ def generate(path, user, token, croot)
54
+ lock = self.new(:root => croot)
55
+ lock.user = user
56
+ lock.path = path
57
+ lock.token = token
58
+ lock.save
59
+ lock
60
+ end
61
+
62
+ def root=(path)
63
+ @root = path
64
+ end
65
+
66
+ def root
67
+ @root || '/tmp/dav4rack'
68
+ end
69
+
70
+ def init_pstore(croot)
71
+ path = File.join(croot, '.attribs', 'locks.pstore')
72
+ FileUtils.mkdir_p(File.dirname(path)) unless File.directory?(File.dirname(path))
73
+ store = IS_18 ? PStore.new(path) : PStore.new(path, true)
74
+ store.transaction do
75
+ unless(store[:paths])
76
+ store[:paths] = {}
77
+ store[:tokens] = {}
78
+ store.commit
79
+ end
80
+ end
81
+ store
82
+ end
83
+ end
84
+
85
+ def initialize(args={})
86
+ @path = args[:path]
87
+ @root = args[:root]
88
+ @owner = args[:owner]
89
+ @store = init_pstore(@root)
90
+ @max_timeout = args[:max_timeout] || 86400
91
+ @default_timeout = args[:max_timeout] || 60
92
+ load_if_exists!
93
+ @new_record = true if token.nil?
94
+ end
95
+
96
+ def owner?(user)
97
+ user == owner
98
+ end
99
+
100
+ def reload
101
+ load_if_exists
102
+ self
103
+ end
104
+
105
+ def remaining_timeout
106
+ t = timeout.to_i - (Time.now.to_i - created_at.to_i)
107
+ t < 0 ? 0 : t
108
+ end
109
+
110
+ def save
111
+ struct = {
112
+ :path => path,
113
+ :token => token,
114
+ :timeout => timeout,
115
+ :depth => depth,
116
+ :created_at => Time.now,
117
+ :owner => owner
118
+ }
119
+ @store.transaction do
120
+ @store[:paths][path] = struct
121
+ @store[:tokens][token] = struct
122
+ @store.commit
123
+ end
124
+ @new_record = false
125
+ self
126
+ end
127
+
128
+ def destroy
129
+ @store.transaction do
130
+ @store[:paths].delete(path)
131
+ @store[:tokens].delete(token)
132
+ @store.commit
133
+ end
134
+ nil
135
+ end
136
+
137
+ private
138
+
139
+ def load_if_exists!
140
+ struct = @store.transaction do
141
+ @store[:paths][path]
142
+ end
143
+ if(struct)
144
+ @path = struct[:path]
145
+ @token = struct[:token]
146
+ @timeout = struct[:timeout]
147
+ @depth = struct[:depth]
148
+ @created_at = struct[:created_at]
149
+ @owner = struct[:owner]
150
+ end
151
+ self
152
+ end
153
+
154
+ def init_pstore(croot=nil)
155
+ self.class.init_pstore(croot || @root)
156
+ end
157
+
158
+ end
159
+ end
@@ -7,7 +7,7 @@ module DAV4Rack
7
7
  def initialize(options={})
8
8
  @options = options.dup
9
9
  unless(@options[:resource_class])
10
- require 'dav4rack/file_resource'
10
+ require 'dav4rack/resources/file_resource'
11
11
  @options[:resource_class] = FileResource
12
12
  @options[:root] ||= Dir.pwd
13
13
  end
@@ -117,7 +117,12 @@ module DAV4Rack
117
117
  @runner.call(class_sym, :after, orig)
118
118
  result
119
119
  end
120
-
120
+
121
+ # Returns if resource supports locking
122
+ def supports_locking?
123
+ false #true
124
+ end
125
+
121
126
  # If this is a collection, return the child resources.
122
127
  def children
123
128
  NotImplemented
@@ -138,6 +143,11 @@ module DAV4Rack
138
143
  parent.exist?
139
144
  end
140
145
 
146
+ # Is the parent resource a collection?
147
+ def parent_collection?
148
+ parent.collection?
149
+ end
150
+
141
151
  # Return the creation time.
142
152
  def creation_date
143
153
  raise NotImplemented
@@ -341,14 +351,17 @@ module DAV4Rack
341
351
  end
342
352
 
343
353
  # Available properties
344
- def property_names
345
- %w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength)
354
+ def properties
355
+ %w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength).collect do |prop|
356
+ {:name => prop, :ns_href => 'DAV:'}
357
+ end
346
358
  end
347
359
 
348
360
  # name:: String - Property name
349
361
  # Returns the value of the given property
350
- def get_property(name)
351
- case name
362
+ def get_property(element)
363
+ raise NotImplemented if (element[:ns_href] != 'DAV:')
364
+ case element[:name]
352
365
  when 'resourcetype' then resource_type
353
366
  when 'displayname' then display_name
354
367
  when 'creationdate' then use_ms_compat_creationdate? ? creation_date.httpdate : creation_date.xmlschema
@@ -356,24 +369,27 @@ module DAV4Rack
356
369
  when 'getcontenttype' then content_type
357
370
  when 'getetag' then etag
358
371
  when 'getlastmodified' then last_modified.httpdate
372
+ else raise NotImplemented
359
373
  end
360
374
  end
361
375
 
362
376
  # name:: String - Property name
363
377
  # value:: New value
364
378
  # Set the property to the given value
365
- def set_property(name, value)
366
- case name
379
+ def set_property(element, value)
380
+ raise NotImplemented if (element[:ns_href] != 'DAV:')
381
+ case element[:name]
367
382
  when 'resourcetype' then self.resource_type = value
368
383
  when 'getcontenttype' then self.content_type = value
369
384
  when 'getetag' then self.etag = value
370
385
  when 'getlastmodified' then self.last_modified = Time.httpdate(value)
386
+ else raise NotImplemented
371
387
  end
372
388
  end
373
389
 
374
390
  # name:: Property name
375
391
  # Remove the property from the resource
376
- def remove_property(name)
392
+ def remove_property(element)
377
393
  Forbidden
378
394
  end
379
395
 
@@ -463,7 +479,7 @@ module DAV4Rack
463
479
  auth = Rack::Auth::Basic::Request.new(request.env)
464
480
  auth.provided? && auth.basic? ? auth.credentials : [nil,nil]
465
481
  end
466
-
482
+
467
483
  end
468
484
 
469
485
  end
@@ -0,0 +1,366 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'pstore'
4
+ require 'webrick/httputils'
5
+ require 'dav4rack/file_resource_lock'
6
+
7
+ module DAV4Rack
8
+
9
+ class FileResource < Resource
10
+
11
+ include WEBrick::HTTPUtils
12
+ include DAV4Rack::Utils
13
+
14
+ # If this is a collection, return the child resources.
15
+ def children
16
+ Dir[file_path + '/*'].map do |path|
17
+ child ::File.basename(path)
18
+ end
19
+ end
20
+
21
+ # Is this resource a collection?
22
+ def collection?
23
+ ::File.directory?(file_path)
24
+ end
25
+
26
+ # Does this recource exist?
27
+ def exist?
28
+ ::File.exist?(file_path)
29
+ end
30
+
31
+ # Return the creation time.
32
+ def creation_date
33
+ stat.ctime
34
+ end
35
+
36
+ # Return the time of last modification.
37
+ def last_modified
38
+ stat.mtime
39
+ end
40
+
41
+ # Set the time of last modification.
42
+ def last_modified=(time)
43
+ ::File.utime(Time.now, time, file_path)
44
+ end
45
+
46
+ # Return an Etag, an unique hash value for this resource.
47
+ def etag
48
+ sprintf('%x-%x-%x', stat.ino, stat.size, stat.mtime.to_i)
49
+ end
50
+
51
+ # Return the mime type of this resource.
52
+ def content_type
53
+ if stat.directory?
54
+ "text/html"
55
+ else
56
+ mime_type(file_path, DefaultMimeTypes)
57
+ end
58
+ end
59
+
60
+ # Return the size in bytes for this resource.
61
+ def content_length
62
+ stat.size
63
+ end
64
+
65
+ # HTTP GET request.
66
+ #
67
+ # Write the content of the resource to the response.body.
68
+ def get(request, response)
69
+ raise NotFound unless exist?
70
+ if stat.directory?
71
+ response.body = ""
72
+ Rack::Directory.new(root).call(request.env)[2].each do |line|
73
+ response.body << line
74
+ end
75
+ response['Content-Length'] = response.body.bytesize.to_s
76
+ else
77
+ file = Rack::File.new(root)
78
+ response.body = file
79
+ end
80
+ OK
81
+ end
82
+
83
+ # HTTP PUT request.
84
+ #
85
+ # Save the content of the request.body.
86
+ def put(request, response)
87
+ write(request.body)
88
+ Created
89
+ end
90
+
91
+ # HTTP POST request.
92
+ #
93
+ # Usually forbidden.
94
+ def post(request, response)
95
+ raise HTTPStatus::Forbidden
96
+ end
97
+
98
+ # HTTP DELETE request.
99
+ #
100
+ # Delete this resource.
101
+ def delete
102
+ if stat.directory?
103
+ FileUtils.rm_rf(file_path)
104
+ else
105
+ ::File.unlink(file_path)
106
+ end
107
+ ::File.unlink(prop_path) if ::File.exists?(prop_path)
108
+ @_prop_hash = nil
109
+ NoContent
110
+ end
111
+
112
+ # HTTP COPY request.
113
+ #
114
+ # Copy this resource to given destination resource.
115
+ # Copy this resource to given destination resource.
116
+ def copy(dest, overwrite)
117
+ if(collection?)
118
+ if(dest.exist?)
119
+ if(dest.collection? && overwrite)
120
+ FileUtils.cp_r(file_path, dest.send(:file_path))
121
+ Created
122
+ else
123
+ if(overwrite)
124
+ FileUtils.rm(dest.send(:file_path))
125
+ FileUtils.cp_r(file_path, dest.send(:file_path))
126
+ NoContent
127
+ else
128
+ PreconditionFailed
129
+ end
130
+ end
131
+ else
132
+ FileUtils.cp_r(file_path, dest.send(:file_path))
133
+ Created
134
+ end
135
+ else
136
+ if(dest.exist? && !overwrite)
137
+ PreconditionFailed
138
+ else
139
+ if(::File.directory?(::File.dirname(dest.send(:file_path))))
140
+ new = !dest.exist?
141
+ if(dest.collection? && dest.exist?)
142
+ FileUtils.rm_rf(dest.send(:file_path))
143
+ end
144
+ FileUtils.cp(file_path, dest.send(:file_path).sub(/\/$/, ''))
145
+ FileUtils.cp(prop_path, dest.prop_path) if ::File.exist? prop_path
146
+ new ? Created : NoContent
147
+ else
148
+ Conflict
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ # HTTP MOVE request.
155
+ #
156
+ # Move this resource to given destination resource.
157
+ def move(*args)
158
+ result = copy(*args)
159
+ delete if [Created, NoContent].include?(result)
160
+ result
161
+ end
162
+
163
+ # HTTP MKCOL request.
164
+ #
165
+ # Create this resource as collection.
166
+ def make_collection
167
+ if(request.body.read.to_s == '')
168
+ if(::File.directory?(file_path))
169
+ MethodNotAllowed
170
+ else
171
+ if(::File.directory?(::File.dirname(file_path)) && !::File.exists?(file_path))
172
+ Dir.mkdir(file_path)
173
+ Created
174
+ else
175
+ Conflict
176
+ end
177
+ end
178
+ else
179
+ UnsupportedMediaType
180
+ end
181
+ end
182
+
183
+ # Write to this resource from given IO.
184
+ def write(io)
185
+ tempfile = "#{file_path}.#{Process.pid}.#{object_id}"
186
+ open(tempfile, "wb") do |file|
187
+ while part = io.read(8192)
188
+ file << part
189
+ end
190
+ end
191
+ ::File.rename(tempfile, file_path)
192
+ ensure
193
+ ::File.unlink(tempfile) rescue nil
194
+ end
195
+
196
+ # name:: String - Property name
197
+ # Returns the value of the given property
198
+ def get_property(name)
199
+ begin
200
+ super
201
+ rescue NotImplemented
202
+ custom_props(name)
203
+ end
204
+ end
205
+
206
+ # name:: String - Property name
207
+ # value:: New value
208
+ # Set the property to the given value
209
+ def set_property(name, value)
210
+ begin
211
+ super
212
+ rescue NotImplemented
213
+ set_custom_props(name,value)
214
+ end
215
+ end
216
+
217
+ def remove_property(element)
218
+ prop_hash.transaction do
219
+ prop_hash.delete(to_element_key(element))
220
+ prop_hash.commit
221
+ end
222
+ val = prop_hash.transaction{ prop_hash[to_element_key(element)] }
223
+ end
224
+
225
+ def lock(args)
226
+ unless(parent_exists?)
227
+ Conflict
228
+ else
229
+ lock_check(args[:type])
230
+ lock = FileResourceLock.explicit_locks(@path, root, :scope => args[:scope], :kind => args[:type], :user => @user)
231
+ unless(lock)
232
+ token = UUIDTools::UUID.random_create.to_s
233
+ lock = FileResourceLock.generate(@path, @user, token, root)
234
+ lock.scope = args[:scope]
235
+ lock.kind = args[:type]
236
+ lock.owner = args[:owner]
237
+ lock.depth = args[:depth]
238
+ if(args[:timeout])
239
+ lock.timeout = args[:timeout] <= @max_timeout && args[:timeout] > 0 ? args[:timeout] : @max_timeout
240
+ else
241
+ lock.timeout = @default_timeout
242
+ end
243
+ lock.save
244
+ end
245
+ begin
246
+ lock_check(args[:type])
247
+ rescue DAV4Rack::LockFailure => lock_failure
248
+ lock.destroy
249
+ raise lock_failure
250
+ rescue HTTPStatus::Status => status
251
+ status
252
+ end
253
+ [lock.remaining_timeout, lock.token]
254
+ end
255
+ end
256
+
257
+ def unlock(token)
258
+ token = token.slice(1, token.length - 2)
259
+ if(token.nil? || token.empty?)
260
+ BadRequest
261
+ else
262
+ lock = FileResourceLock.find_by_token(token)
263
+ if(lock.nil? || lock.user_id != @user.id)
264
+ Forbidden
265
+ elsif(lock.path !~ /^#{Regexp.escape(@path)}.*$/)
266
+ Conflict
267
+ else
268
+ lock.destroy
269
+ NoContent
270
+ end
271
+ end
272
+ end
273
+
274
+ protected
275
+
276
+ def lock_check(lock_type=nil)
277
+ if(FileResourceLock.explicitly_locked?(@path, root))
278
+ raise Locked if lock_type && lock_type == 'exclusive'
279
+ #raise Locked if FileResourceLock.explicit_locks(@path, root).find(:all, :conditions => ["scope = 'exclusive' AND user_id != ?", @user.id]).size > 0
280
+ elsif(FileResourceLock.implicitly_locked?(@path, root))
281
+ if(lock_type.to_s == 'exclusive')
282
+ locks = FileResourceLock.implicit_locks(@path)
283
+ failure = DAV4Rack::LockFailure.new("Failed to lock: #{@path}")
284
+ locks.each do |lock|
285
+ failure.add_failure(@path, Locked)
286
+ end
287
+ raise failure
288
+ else
289
+ locks = FileResourceLock.implict_locks(@path).find(:all, :conditions => ["scope = 'exclusive' AND user_id != ?", @user.id])
290
+ if(locks.size > 0)
291
+ failure = LockFailure.new("Failed to lock: #{@path}")
292
+ locks.each do |lock|
293
+ failure.add_failure(@path, Locked)
294
+ end
295
+ raise failure
296
+ end
297
+ end
298
+ end
299
+ end
300
+
301
+ def set_custom_props(element, val)
302
+ prop_hash.transaction do
303
+ prop_hash[to_element_key(element)] = val
304
+ prop_hash.commit
305
+ end
306
+ end
307
+
308
+ def custom_props(element)
309
+ val = prop_hash.transaction(true) do
310
+ prop_hash[to_element_key(element)]
311
+ end
312
+ raise NotFound unless val
313
+ val
314
+ end
315
+
316
+ def store_directory
317
+ path = ::File.join(root, '.attrib_store')
318
+ unless(::File.directory?(::File.dirname(path)))
319
+ FileUtils.mkdir_p(::File.dirname(path))
320
+ end
321
+ path
322
+ end
323
+
324
+ def prop_path
325
+ path = ::File.join(store_directory, "#{::File.join(::File.dirname(file_path), ::File.basename(file_path)).gsub('/', '_')}.pstore")
326
+ unless(::File.directory?(::File.dirname(path)))
327
+ FileUtils.mkdir_p(::File.dirname(path))
328
+ end
329
+ path
330
+ end
331
+
332
+ def lock_path
333
+ path = ::File.join(store_directory, 'locks.pstore')
334
+ unless(::File.directory?(::File.dirname(path)))
335
+ FileUtils.mkdir_p(::File.dirname(path))
336
+ end
337
+ path
338
+ end
339
+
340
+ def prop_hash
341
+ @_prop_hash ||= IS_18 ? PStore.new(prop_path) : PStore.new(prop_path, true)
342
+ end
343
+
344
+ def authenticate(user, pass)
345
+ if(options[:username])
346
+ options[:username] == user && options[:password] == pass
347
+ else
348
+ true
349
+ end
350
+ end
351
+
352
+ def root
353
+ @options[:root]
354
+ end
355
+
356
+ def file_path
357
+ ::File.join(root, path)
358
+ end
359
+
360
+ def stat
361
+ @stat ||= ::File.stat(file_path)
362
+ end
363
+
364
+ end
365
+
366
+ end