rack-webdav 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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