rack-webdav 0.4.3 → 0.4.4

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,247 @@
1
+ require 'digest'
2
+ require 'ffi-xattr'
3
+
4
+ module RackWebDAV
5
+ class FileResource < Resource
6
+ include ::WEBrick::HTTPUtils
7
+
8
+ # If this is a collection, return the child resources.
9
+ def children
10
+ Dir[file_path + '/*'].map do |path|
11
+ child File.basename(path)
12
+ end
13
+ end
14
+
15
+ # Is this resource a collection?
16
+ def collection?
17
+ File.directory?(file_path)
18
+ end
19
+
20
+ # Does this recource exist?
21
+ def exist?
22
+ File.exist?(file_path)
23
+ end
24
+
25
+ # Return the creation time.
26
+ def creation_date
27
+ stat.ctime
28
+ end
29
+
30
+ # Return the time of last modification.
31
+ def last_modified
32
+ stat.mtime
33
+ end
34
+
35
+ # Set the time of last modification.
36
+ def last_modified=(time)
37
+ File.utime(Time.now, time, file_path)
38
+ end
39
+
40
+ # Return an Etag, an unique hash value for this resource.
41
+ def etag
42
+ sprintf('%x-%x-%x', stat.ino, stat.size, stat.mtime.to_i)
43
+ end
44
+
45
+ # Return the resource type.
46
+ #
47
+ # If this is a collection, return a collection element
48
+ def resource_type
49
+ if collection?
50
+ Nokogiri::XML::fragment('<D:collection xmlns:D="DAV:"/>').children.first
51
+ end
52
+ end
53
+
54
+ # Return the mime type of this resource.
55
+ def content_type
56
+ if stat.directory?
57
+ "text/html"
58
+ else
59
+ mime_type(file_path, DefaultMimeTypes)
60
+ end
61
+ end
62
+
63
+ # Return the size in bytes for this resource.
64
+ def content_length
65
+ stat.size
66
+ end
67
+
68
+ def set_custom_property(name, value)
69
+ if value.nil? || value.empty?
70
+ begin
71
+ xattr.remove("rack-webdav:#{name}")
72
+ rescue Errno::ENOATTR
73
+ # If the attribute being deleted doesn't exist, just do nothing
74
+ end
75
+ else
76
+ xattr["rack-webdav:#{name}"] = value
77
+ end
78
+ end
79
+
80
+ def get_custom_property(name)
81
+ value = xattr["rack-webdav:#{name}"]
82
+ raise HTTPStatus::NotFound if value.nil?
83
+ value
84
+ end
85
+
86
+ def list_custom_properties
87
+ xattr.list.select { |a| a.start_with?('rack-webdav') }.map { |a| a.sub(/^rack-webdav:/, '') }
88
+ end
89
+
90
+ # HTTP GET request.
91
+ #
92
+ # Write the content of the resource to the response.body.
93
+ def get
94
+ if stat.directory?
95
+ content = ""
96
+ Rack::Directory.new(root).call(@request.env)[2].each { |line| content << line }
97
+ @response.body = [content]
98
+ @response['Content-Length'] = (content.respond_to?(:bytesize) ? content.bytesize : content.size).to_s
99
+ else
100
+ file = File.open(file_path)
101
+ @response.body = file
102
+ end
103
+ end
104
+
105
+ # HTTP PUT request.
106
+ #
107
+ # Save the content of the request.body.
108
+ def put
109
+ if @request.env['HTTP_CONTENT_MD5']
110
+ content_md5_pass?(@request.env) or raise HTTPStatus::BadRequest.new('Content-MD5 mismatch')
111
+ end
112
+
113
+ if content_continue_pass?(@request.env)
114
+ raise HTTPStatus::Continue.new()
115
+ end
116
+
117
+ write(@request.body)
118
+ end
119
+
120
+ # HTTP POST request.
121
+ #
122
+ # Usually forbidden.
123
+ def post
124
+ raise HTTPStatus::Forbidden
125
+ end
126
+
127
+ # HTTP DELETE request.
128
+ #
129
+ # Delete this resource.
130
+ def delete
131
+ if stat.directory?
132
+ Dir.rmdir(file_path)
133
+ else
134
+ File.unlink(file_path)
135
+ end
136
+ end
137
+
138
+ # HTTP COPY request.
139
+ #
140
+ # Copy this resource to given destination resource.
141
+ def copy(dest)
142
+ if stat.directory?
143
+ dest.make_collection
144
+ else
145
+ open(file_path, "rb") do |file|
146
+ dest.write(file)
147
+ end
148
+
149
+ list_custom_properties.each do |prop|
150
+ dest.set_custom_property(prop, get_custom_property(prop))
151
+ end
152
+ end
153
+ end
154
+
155
+ # HTTP MOVE request.
156
+ #
157
+ # Move this resource to given destination resource.
158
+ def move(dest)
159
+ copy(dest)
160
+ delete
161
+ end
162
+
163
+ # HTTP MKCOL request.
164
+ #
165
+ # Create this resource as collection.
166
+ def make_collection
167
+ Dir.mkdir(file_path)
168
+ end
169
+
170
+ # Write to this resource from given IO.
171
+ def write(io)
172
+ tempfile = "#{file_path}.#{Process.pid}.#{object_id}"
173
+
174
+ open(tempfile, "wb") do |file|
175
+ while part = io.read(8192)
176
+ file << part
177
+ end
178
+ end
179
+
180
+ File.rename(tempfile, file_path)
181
+ ensure
182
+ File.unlink(tempfile) rescue nil
183
+ end
184
+
185
+ @@locks = {}
186
+
187
+ def lock(token, timeout, scope = nil, type = nil, owner = nil)
188
+ if scope && type && owner
189
+ # Create lock
190
+ @@locks[token] = {
191
+ :timeout => timeout,
192
+ :scope => scope,
193
+ :type => type,
194
+ :owner => owner
195
+ }
196
+ return true
197
+ else
198
+ # Refresh lock
199
+ lock = @@locks[token]
200
+ return false unless lock
201
+ return [ lock[:timeout], lock[:scope], lock[:type], lock[:owner] ]
202
+ end
203
+ end
204
+
205
+ def unlock(token)
206
+ !!@@locks.delete(token)
207
+ end
208
+
209
+
210
+ private
211
+
212
+ def root
213
+ @options[:root]
214
+ end
215
+
216
+ def file_path
217
+ root + '/' + path
218
+ end
219
+
220
+ def stat
221
+ @stat ||= File.stat(file_path)
222
+ end
223
+
224
+ def xattr
225
+ @xattr ||= Xattr.new(file_path)
226
+ end
227
+
228
+ def content_md5_pass?(env)
229
+ expected = env['HTTP_CONTENT_MD5'] or return true
230
+
231
+ body = env['rack.input'].dup
232
+ digest = Digest::MD5.new.digest(body.read)
233
+ actual = [ digest ].pack('m').strip
234
+
235
+ body.rewind
236
+
237
+ expected == actual
238
+ end
239
+
240
+ def content_continue_pass?(env)
241
+ expected = env['HTTP_EXPECT'] or return false
242
+
243
+ expected.downcase == '100-continue'
244
+ end
245
+ end
246
+
247
+ end
@@ -1,62 +1,39 @@
1
- # encoding: UTF-8
2
- require 'rack-webdav/logger'
3
-
4
1
  module RackWebDAV
5
2
 
6
3
  class Handler
7
- include RackWebDAV::HTTPStatus
8
- def initialize(options={})
9
- @options = options.dup
10
- unless(@options[:resource_class])
11
- require 'rack-webdav/resources/file_resource'
12
- @options[:resource_class] = FileResource
13
- @options[:root] ||= Dir.pwd
14
- end
15
- Logger.set(*@options[:log_to])
4
+
5
+ # @return [Hash] The hash of options.
6
+ attr_reader :options
7
+
8
+
9
+ # Initializes a new instance with given options.
10
+ #
11
+ # @param [Hash] options Hash of options to customize the handler behavior.
12
+ # @option options [Class] :resource_class (FileResource)
13
+ # The resource class.
14
+ # @option options [String] :root (".")
15
+ # The root resource folder.
16
+ #
17
+ def initialize(options = {})
18
+ @options = {
19
+ :resource_class => FileResource,
20
+ :root => Dir.pwd
21
+ }.merge(options)
16
22
  end
17
23
 
18
24
  def call(env)
19
- begin
20
- start = Time.now
21
- request = Rack::Request.new(env)
22
- response = Rack::Response.new
23
-
24
- Logger.info "Processing WebDAV request: #{request.path} (for #{request.ip} at #{Time.now}) [#{request.request_method}]"
25
+ request = Rack::Request.new(env)
26
+ response = Rack::Response.new
25
27
 
26
- controller = nil
27
- begin
28
- controller_class = @options[:controller_class] || Controller
29
- controller = controller_class.new(request, response, @options.dup)
30
- controller.authenticate
31
- res = controller.send(request.request_method.downcase)
32
- response.status = res.code if res.respond_to?(:code)
33
- rescue HTTPStatus::Unauthorized => status
34
- response.body = controller.resource.respond_to?(:authentication_error_msg) ? controller.resource.authentication_error_msg : 'Not Authorized'
35
- response['WWW-Authenticate'] = "Basic realm=\"#{controller.resource.respond_to?(:authentication_realm) ? controller.resource.authentication_realm : 'Locked content'}\""
36
- response.status = status.code
37
- rescue HTTPStatus::Status => status
38
- response.status = status.code
39
- end
40
-
41
- # Strings in Ruby 1.9 are no longer enumerable. Rack still expects the response.body to be
42
- # enumerable, however.
43
-
44
- response['Content-Length'] = response.body.to_s.length unless response['Content-Length'] || !response.body.is_a?(String)
45
- response.body = [response.body] unless response.body.respond_to? :each
46
- response.status = response.status ? response.status.to_i : 200
47
- response.headers.keys.each{|k| response.headers[k] = response[k].to_s}
48
-
49
- # Apache wants the body dealt with, so just read it and junk it
50
- buf = true
51
- buf = request.body.read(8192) while buf
28
+ begin
29
+ controller = Controller.new(request, response, @options)
30
+ controller.send(request.request_method.downcase)
52
31
 
53
- Logger.debug "Response in string form. Outputting contents: \n#{response.body}" if response.body.is_a?(String)
54
- Logger.info "Completed in: #{((Time.now.to_f - start.to_f) * 1000).to_i} ms | #{response.status} [#{request.url}]"
55
- response
56
- rescue Exception => e
57
- Logger.error "WebDAV Error: #{e}\n#{e.backtrace.join("\n")}"
58
- raise e
32
+ rescue HTTPStatus::Status => status
33
+ response.status = status.code
59
34
  end
35
+
36
+ response.finish
60
37
  end
61
38
 
62
39
  end
@@ -1,19 +1,19 @@
1
1
  module RackWebDAV
2
2
 
3
3
  module HTTPStatus
4
-
4
+
5
5
  class Status < Exception
6
-
6
+
7
7
  class << self
8
8
  attr_accessor :code, :reason_phrase
9
9
  alias_method :to_i, :code
10
-
10
+
11
11
  def status_line
12
12
  "#{code} #{reason_phrase}"
13
13
  end
14
-
14
+
15
15
  end
16
-
16
+
17
17
  def code
18
18
  self.class.code
19
19
  end
@@ -21,17 +21,17 @@ module RackWebDAV
21
21
  def reason_phrase
22
22
  self.class.reason_phrase
23
23
  end
24
-
24
+
25
25
  def status_line
26
26
  self.class.status_line
27
27
  end
28
-
28
+
29
29
  def to_i
30
30
  self.class.to_i
31
31
  end
32
-
32
+
33
33
  end
34
-
34
+
35
35
  StatusMessage = {
36
36
  100 => 'Continue',
37
37
  101 => 'Switching Protocols',
@@ -71,7 +71,7 @@ module RackWebDAV
71
71
  417 => 'Expectation Failed',
72
72
  422 => 'Unprocessable Entity',
73
73
  423 => 'Locked',
74
- 424 => 'Failed Dependency',
74
+ 424 => 'Failed Dependency',
75
75
  500 => 'Internal Server Error',
76
76
  501 => 'Not Implemented',
77
77
  502 => 'Bad Gateway',