rack-webdav 0.4.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.
@@ -0,0 +1,379 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'pstore'
4
+ require 'webrick/httputils'
5
+ require 'rack-webdav/file_resource_lock'
6
+
7
+ module RackWebDAV
8
+
9
+ class FileResource < Resource
10
+
11
+ include WEBrick::HTTPUtils
12
+ include RackWebDAV::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
+ status, headers, body = file.call(request.env)
79
+
80
+ if body.respond_to?(:to_path)
81
+ response.body = ::File.open(body.to_path, 'rb').read
82
+ response['Content-Length'] = response.body.bytesize.to_s
83
+ else
84
+ response.body = ""
85
+ total_bytes = 0
86
+ body.each { |part|
87
+ response.body += part
88
+ total_bytes += part.bytesize
89
+ }
90
+ response['Content-Length'] = total_bytes
91
+ end
92
+ end
93
+ OK
94
+ end
95
+
96
+ # HTTP PUT request.
97
+ #
98
+ # Save the content of the request.body.
99
+ def put(request, response)
100
+ write(request.body)
101
+ Created
102
+ end
103
+
104
+ # HTTP POST request.
105
+ #
106
+ # Usually forbidden.
107
+ def post(request, response)
108
+ raise HTTPStatus::Forbidden
109
+ end
110
+
111
+ # HTTP DELETE request.
112
+ #
113
+ # Delete this resource.
114
+ def delete
115
+ if stat.directory?
116
+ FileUtils.rm_rf(file_path)
117
+ else
118
+ ::File.unlink(file_path)
119
+ end
120
+ ::File.unlink(prop_path) if ::File.exists?(prop_path)
121
+ @_prop_hash = nil
122
+ NoContent
123
+ end
124
+
125
+ # HTTP COPY request.
126
+ #
127
+ # Copy this resource to given destination resource.
128
+ # Copy this resource to given destination resource.
129
+ def copy(dest, overwrite)
130
+ if(collection?)
131
+ if(dest.exist?)
132
+ if(dest.collection? && overwrite)
133
+ FileUtils.cp_r(file_path, dest.send(:file_path))
134
+ Created
135
+ else
136
+ if(overwrite)
137
+ FileUtils.rm(dest.send(:file_path))
138
+ FileUtils.cp_r(file_path, dest.send(:file_path))
139
+ NoContent
140
+ else
141
+ PreconditionFailed
142
+ end
143
+ end
144
+ else
145
+ FileUtils.cp_r(file_path, dest.send(:file_path))
146
+ Created
147
+ end
148
+ else
149
+ if(dest.exist? && !overwrite)
150
+ PreconditionFailed
151
+ else
152
+ if(::File.directory?(::File.dirname(dest.send(:file_path))))
153
+ new = !dest.exist?
154
+ if(dest.collection? && dest.exist?)
155
+ FileUtils.rm_rf(dest.send(:file_path))
156
+ end
157
+ FileUtils.cp(file_path, dest.send(:file_path).sub(/\/$/, ''))
158
+ FileUtils.cp(prop_path, dest.prop_path) if ::File.exist? prop_path
159
+ new ? Created : NoContent
160
+ else
161
+ Conflict
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ # HTTP MOVE request.
168
+ #
169
+ # Move this resource to given destination resource.
170
+ def move(*args)
171
+ result = copy(*args)
172
+ delete if [Created, NoContent].include?(result)
173
+ result
174
+ end
175
+
176
+ # HTTP MKCOL request.
177
+ #
178
+ # Create this resource as collection.
179
+ def make_collection
180
+ if(request.body.read.to_s == '')
181
+ if(::File.directory?(file_path))
182
+ MethodNotAllowed
183
+ else
184
+ if(::File.directory?(::File.dirname(file_path)) && !::File.exists?(file_path))
185
+ Dir.mkdir(file_path)
186
+ Created
187
+ else
188
+ Conflict
189
+ end
190
+ end
191
+ else
192
+ UnsupportedMediaType
193
+ end
194
+ end
195
+
196
+ # Write to this resource from given IO.
197
+ def write(io)
198
+ tempfile = "#{file_path}.#{Process.pid}.#{object_id}"
199
+ open(tempfile, "wb") do |file|
200
+ while part = io.read(8192)
201
+ file << part
202
+ end
203
+ end
204
+ ::File.rename(tempfile, file_path)
205
+ ensure
206
+ ::File.unlink(tempfile) rescue nil
207
+ end
208
+
209
+ # name:: String - Property name
210
+ # Returns the value of the given property
211
+ def get_property(name)
212
+ begin
213
+ super
214
+ rescue NotImplemented
215
+ custom_props(name)
216
+ end
217
+ end
218
+
219
+ # name:: String - Property name
220
+ # value:: New value
221
+ # Set the property to the given value
222
+ def set_property(name, value)
223
+ begin
224
+ super
225
+ rescue NotImplemented
226
+ set_custom_props(name,value)
227
+ end
228
+ end
229
+
230
+ def remove_property(element)
231
+ prop_hash.transaction do
232
+ prop_hash.delete(to_element_key(element))
233
+ prop_hash.commit
234
+ end
235
+ val = prop_hash.transaction{ prop_hash[to_element_key(element)] }
236
+ end
237
+
238
+ def lock(args)
239
+ unless(parent_exists?)
240
+ Conflict
241
+ else
242
+ lock_check(args[:type])
243
+ lock = FileResourceLock.explicit_locks(@path, root, :scope => args[:scope], :kind => args[:type], :user => @user)
244
+ unless(lock)
245
+ token = UUIDTools::UUID.random_create.to_s
246
+ lock = FileResourceLock.generate(@path, @user, token, root)
247
+ lock.scope = args[:scope]
248
+ lock.kind = args[:type]
249
+ lock.owner = args[:owner]
250
+ lock.depth = args[:depth]
251
+ if(args[:timeout])
252
+ lock.timeout = args[:timeout] <= @max_timeout && args[:timeout] > 0 ? args[:timeout] : @max_timeout
253
+ else
254
+ lock.timeout = @default_timeout
255
+ end
256
+ lock.save
257
+ end
258
+ begin
259
+ lock_check(args[:type])
260
+ rescue RackWebDAV::LockFailure => lock_failure
261
+ lock.destroy
262
+ raise lock_failure
263
+ rescue HTTPStatus::Status => status
264
+ status
265
+ end
266
+ [lock.remaining_timeout, lock.token]
267
+ end
268
+ end
269
+
270
+ def unlock(token)
271
+ token = token.slice(1, token.length - 2)
272
+ if(token.nil? || token.empty?)
273
+ BadRequest
274
+ else
275
+ lock = FileResourceLock.find_by_token(token, root)
276
+ if lock.nil?
277
+ Forbidden
278
+ elsif(lock.path !~ /^#{Regexp.escape(@path)}.*$/)
279
+ Conflict
280
+ else
281
+ lock.destroy
282
+ NoContent
283
+ end
284
+ end
285
+ end
286
+
287
+ protected
288
+
289
+ def lock_check(lock_type=nil)
290
+ if(FileResourceLock.explicitly_locked?(@path, root))
291
+ raise Locked if lock_type && lock_type == 'exclusive'
292
+ #raise Locked if FileResourceLock.explicit_locks(@path, root).find(:all, :conditions => ["scope = 'exclusive' AND user_id != ?", @user.id]).size > 0
293
+ elsif(FileResourceLock.implicitly_locked?(@path, root))
294
+ if(lock_type.to_s == 'exclusive')
295
+ locks = FileResourceLock.implicit_locks(@path)
296
+ failure = RackWebDAV::LockFailure.new("Failed to lock: #{@path}")
297
+ locks.each do |lock|
298
+ failure.add_failure(@path, Locked)
299
+ end
300
+ raise failure
301
+ else
302
+ locks = FileResourceLock.implict_locks(@path).find(:all, :conditions => ["scope = 'exclusive' AND user_id != ?", @user.id])
303
+ if(locks.size > 0)
304
+ failure = LockFailure.new("Failed to lock: #{@path}")
305
+ locks.each do |lock|
306
+ failure.add_failure(@path, Locked)
307
+ end
308
+ raise failure
309
+ end
310
+ end
311
+ end
312
+ end
313
+
314
+ def set_custom_props(element, val)
315
+ prop_hash.transaction do
316
+ prop_hash[to_element_key(element)] = val
317
+ prop_hash.commit
318
+ end
319
+ end
320
+
321
+ def custom_props(element)
322
+ val = prop_hash.transaction(true) do
323
+ prop_hash[to_element_key(element)]
324
+ end
325
+ raise NotFound unless val
326
+ val
327
+ end
328
+
329
+ def store_directory
330
+ path = ::File.join(root, '.attrib_store')
331
+ unless(::File.directory?(::File.dirname(path)))
332
+ FileUtils.mkdir_p(::File.dirname(path))
333
+ end
334
+ path
335
+ end
336
+
337
+ def prop_path
338
+ path = ::File.join(store_directory, "#{::File.join(::File.dirname(file_path), ::File.basename(file_path)).gsub('/', '_')}.pstore")
339
+ unless(::File.directory?(::File.dirname(path)))
340
+ FileUtils.mkdir_p(::File.dirname(path))
341
+ end
342
+ path
343
+ end
344
+
345
+ def lock_path
346
+ path = ::File.join(store_directory, 'locks.pstore')
347
+ unless(::File.directory?(::File.dirname(path)))
348
+ FileUtils.mkdir_p(::File.dirname(path))
349
+ end
350
+ path
351
+ end
352
+
353
+ def prop_hash
354
+ @_prop_hash ||= IS_18 ? PStore.new(prop_path) : PStore.new(prop_path, true)
355
+ end
356
+
357
+ def authenticate(user, pass)
358
+ if(options[:username])
359
+ options[:username] == user && options[:password] == pass
360
+ else
361
+ true
362
+ end
363
+ end
364
+
365
+ def root
366
+ @options[:root]
367
+ end
368
+
369
+ def file_path
370
+ ::File.join(root, path)
371
+ end
372
+
373
+ def stat
374
+ @stat ||= ::File.stat(file_path)
375
+ end
376
+
377
+ end
378
+
379
+ end
@@ -0,0 +1,341 @@
1
+ # coding: utf-8
2
+ require 'mime/types'
3
+
4
+ module RackWebDAV
5
+
6
+ class MongoResource < RackWebDAV::Resource
7
+
8
+ # @@logger = Rails.logger
9
+
10
+ def initialize(public_path, path, request, response, options)
11
+ # 'ASCII-8BIT'で渡される場合があるので'UTF-8'を指定しておく
12
+ _force_encoding!(public_path)
13
+ _force_encoding!(path)
14
+ super(public_path, path, request, response, options)
15
+ @filesystem = Mongo::GridFileSystem.new(Mongoid.database)
16
+ @collection = Mongoid.database.collection('fs.files')
17
+ if options[:bson]
18
+ @bson = options[:bson]
19
+ elsif path.length <= 1
20
+ # ルートの場合 (''の場合と'/'の場合がある)
21
+ @bson = {'filename' => root + '/'}
22
+ else
23
+ # ファイルかディレクトリが、パラメータだけでは判断できない。ので \/? が必要。
24
+ # だから、ディレクトリと同名のファイルは、作成できない。
25
+ @bson = @collection.find_one({:filename => /^#{Regexp.escape(file_path)}\/?$/}) rescue nil
26
+ end
27
+ end
28
+
29
+ def child(bson)
30
+ path = remove(bson['filename'], root)
31
+ public_path = @options[:root_uri_path] + path
32
+ @options[:bson] = bson
33
+ self.class.new(public_path, path, @request, @response, @options)
34
+ end
35
+
36
+ # If this is a collection, return the child resources.
37
+ def children
38
+ # Dir[file_path + '/*'].map do |path|
39
+ # child File.basename(path)
40
+ # end
41
+ @collection.find({:filename => /^#{Regexp.escape(@bson['filename'])}[^\/]+\/?$/}).map do |bson|
42
+ child bson
43
+ end
44
+ end
45
+
46
+ # Is this resource a collection?
47
+ def collection?
48
+ # File.directory?(file_path)
49
+ @bson && _collection?(@bson['filename'])
50
+ end
51
+
52
+ # Does this recource exist?
53
+ def exist?
54
+ # File.exist?(file_path)
55
+ @bson
56
+ end
57
+
58
+ # Return the creation time.
59
+ def creation_date
60
+ # stat.ctime
61
+ @bson['uploadDate'] || Date.new
62
+ end
63
+
64
+ # Return the time of last modification.
65
+ def last_modified
66
+ # stat.mtime
67
+ @bson['uploadDate'] || Date.new
68
+ end
69
+
70
+ # Set the time of last modification.
71
+ def last_modified=(time)
72
+ # File.utime(Time.now, time, file_path)
73
+ end
74
+
75
+ # Return an Etag, an unique hash value for this resource.
76
+ def etag
77
+ # sprintf('%x-%x-%x', stat.ino, stat.size, stat.mtime.to_i)
78
+ @bson['_id'].to_s
79
+ end
80
+
81
+ # Return the mime type of this resource.
82
+ def content_type
83
+ # if stat.directory?
84
+ # "text/html"
85
+ # else
86
+ # mime_type(file_path, DefaultMimeTypes)
87
+ # end
88
+ @bson['contentType'] || "text/html"
89
+ end
90
+
91
+ # Return the size in bytes for this resource.
92
+ def content_length
93
+ # stat.size
94
+ @bson['length'] || 0
95
+ end
96
+
97
+ # HTTP GET request.
98
+ #
99
+ # Write the content of the resource to the response.body.
100
+ def get(request, response)
101
+ raise NotFound unless exist?
102
+ # if stat.directory?
103
+ # response.body = ""
104
+ # Rack::Directory.new(root).call(request.env)[2].each do |line|
105
+ # response.body << line
106
+ # end
107
+ # response['Content-Length'] = response.body.size.to_s
108
+ # else
109
+ # file = Rack::File.new(root)
110
+ # response.body = file
111
+ # end
112
+ if collection?
113
+ response.body = "<html>"
114
+ response.body << "<h2>" + file_path.html_safe + "</h2>"
115
+ children.each do |child|
116
+ name = child.file_path.html_safe
117
+ path = child.public_path
118
+ response.body << "<a href='" + path + "'>" + name + "</a>"
119
+ response.body << "</br>"
120
+ end
121
+ response.body << "</html>"
122
+ response['Content-Length'] = response.body.size.to_s
123
+ response['Content-Type'] = 'text/html'
124
+ else
125
+ @filesystem.open(file_path, 'r') do |f|
126
+ response.body = f
127
+ response['Content-Type'] = @bson['contentType']
128
+ end
129
+ end
130
+
131
+ end
132
+
133
+ # HTTP PUT request.
134
+ #
135
+ # Save the content of the request.body.
136
+ def put(request, response)
137
+ write(request.body)
138
+ Created
139
+ end
140
+
141
+ # HTTP POST request.
142
+ #
143
+ # Usually forbidden.
144
+ def post(request, response)
145
+ raise HTTPStatus::Forbidden
146
+ end
147
+
148
+ # HTTP DELETE request.
149
+ #
150
+ # Delete this resource.
151
+ def delete
152
+ # if stat.directory?
153
+ # FileUtils.rm_rf(file_path)
154
+ # else
155
+ # File.unlink(file_path)
156
+ # end
157
+ if collection?
158
+ @collection.find({:filename => /^#{Regexp.escape(@bson['filename'])}/}).each do |bson|
159
+ @collection.remove(bson)
160
+ end
161
+ else
162
+ @collection.remove(@bson)
163
+ end
164
+ NoContent
165
+ end
166
+
167
+ # HTTP COPY request.
168
+ #
169
+ # Copy this resource to given destination resource.
170
+ def copy(dest, overwrite = false)
171
+ # if(dest.path == path)
172
+ # Conflict
173
+ # elsif(stat.directory?)
174
+ # dest.make_collection
175
+ # FileUtils.cp_r("#{file_path}/.", "#{dest.send(:file_path)}/")
176
+ # OK
177
+ # else
178
+ # exists = File.exists?(file_path)
179
+ # if(exists && !overwrite)
180
+ # PreconditionFailed
181
+ # else
182
+ # open(file_path, "rb") do |file|
183
+ # dest.write(file)
184
+ # end
185
+ # exists ? NoContent : Created
186
+ # end
187
+ # end
188
+
189
+ # ディレクトリなら末尾に「/」をつける。
190
+ # (dstにもともと「/」が付いているかどうか、クライアントに依存している)
191
+ # CarotDAV : 「/」が付いていない
192
+ # TeamFile : 「/」が付いている
193
+ dest.collection! if collection?
194
+
195
+ src = @bson['filename']
196
+ dst = dest.file_path
197
+ exists = nil
198
+
199
+ @collection.find({:filename => /^#{Regexp.escape(src)}/}).each do |bson|
200
+ src_name = bson['filename']
201
+ dst_name = dst + src_name.slice(src.length, src_name.length)
202
+
203
+ exists = @collection.find_one({:filename => dst_name}) rescue nil
204
+
205
+ return PreconditionFailed if (exists && !overwrite && !collection?)
206
+
207
+ @filesystem.open(src_name, "r") do |src|
208
+ @filesystem.open(dst_name, "w") do |dst|
209
+ dst.write(src) if src.file_length > 0
210
+ end
211
+ end
212
+
213
+ @collection.remove(exists) if exists
214
+ end
215
+
216
+ collection? ? Created : (exists ? NoContent : Created)
217
+ end
218
+
219
+ # HTTP MOVE request.
220
+ #
221
+ # Move this resource to given destination resource.
222
+ def move(dest, overwrite = false)
223
+
224
+ # ディレクトリなら末尾に「/」をつける。
225
+ # (dstにもともと「/」が付いているかどうか、クライアントに依存している)
226
+ # CarotDAV : 「/」が付いていない
227
+ # TeamFile : 「/」が付いている
228
+ dest.collection! if collection?
229
+
230
+ src = @bson['filename']
231
+ dst = dest.file_path
232
+ exists = nil
233
+
234
+ @collection.find({:filename => /^#{Regexp.escape(src)}/}).each do |bson|
235
+ src_name = bson['filename']
236
+ dst_name = dst + src_name.slice(src.length, src_name.length)
237
+
238
+ exists = @collection.find_one({:filename => dst_name}) rescue nil
239
+
240
+ # http://mongoid.org/docs/persistence/atomic.html
241
+ # http://rubydoc.info/github/mongoid/mongoid/master/Mongoid/Collection#update-instance_method
242
+ @collection.update({'_id' => bson['_id']}, {'$set' => {'filename' => dst_name}}, :safe => true)
243
+
244
+ @collection.remove(exists) if exists
245
+ end
246
+
247
+ collection? ? Created : (exists ? NoContent : Created)
248
+ end
249
+
250
+ # HTTP MKCOL request.
251
+ #
252
+ # Create this resource as collection.
253
+ def make_collection
254
+ # Dir.mkdir(file_path)
255
+ # Created
256
+
257
+ # ディレクトリなら末尾に「/」をつける。
258
+ # (dstにもともと「/」が付いているかどうか、クライアントに依存している)
259
+ # CarotDAV : 「/」が付いていない
260
+ # TeamFile : 「/」が付いている
261
+ collection!
262
+
263
+ bson = @collection.find_one({:filename => file_path}) rescue nil
264
+
265
+ # 0バイトのファイルを作成しディレクトリの代わりとする
266
+ @filesystem.open(file_path, "w") { |f| } if !bson
267
+
268
+ # @@logger.error('make_collection : ' + file_path)
269
+
270
+ Created
271
+ end
272
+
273
+ # Write to this resource from given IO.
274
+ def write(io)
275
+ # tempfile = "#{file_path}.#{Process.pid}.#{object_id}"
276
+ # open(tempfile, "wb") do |file|
277
+ # while part = io.read(8192)
278
+ # file << part
279
+ # end
280
+ # end
281
+ # File.rename(tempfile, file_path)
282
+ # ensure
283
+ # File.unlink(tempfile) rescue nil
284
+
285
+ # 同名のファイルができないように
286
+ bson = @collection.find_one({:filename => file_path}) rescue nil
287
+
288
+ @filesystem.open(file_path, "w", :content_type => _content_type(file_path)) { |f| f.write(io) }
289
+
290
+ # 同名のファイルができないように
291
+ @collection.remove(bson) if bson
292
+
293
+ end
294
+
295
+ protected
296
+
297
+ def file_path
298
+ root + path
299
+ end
300
+
301
+ # ファイル名の末尾に「/」を付加してディレクトリ(コレクション)とする
302
+ def collection!
303
+ path << '/' if !_collection?(path)
304
+ end
305
+
306
+ private
307
+
308
+ def _content_type(filename)
309
+ MIME::Types.type_for(filename).first.to_s || 'text/html'
310
+ end
311
+
312
+ def authenticate(user, pass)
313
+ if(options[:username])
314
+ options[:username] == user && options[:password] == pass
315
+ else
316
+ true
317
+ end
318
+ end
319
+
320
+ # path1の先頭からpath2を取り除く
321
+ def remove(path1, path2)
322
+ path1.slice(path2.length, path1.length)
323
+ end
324
+
325
+ def root
326
+ @options[:root]
327
+ end
328
+
329
+ # ファイル名の末尾が「/」のファイルをディレクトリ(コレクション)とする
330
+ def _collection?(path)
331
+ path && path[-1].chr == '/'
332
+ end
333
+
334
+ # 'ASCII-8BIT'で渡される場合があるので'UTF-8'を指定しておく
335
+ def _force_encoding!(str)
336
+ str.force_encoding('UTF-8')
337
+ end
338
+
339
+ end
340
+
341
+ end