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.
- checksums.yaml +4 -4
- data/bin/byebug +17 -0
- data/bin/coderay +17 -0
- data/bin/htmldiff +17 -0
- data/bin/ldiff +17 -0
- data/bin/nokogiri +17 -0
- data/bin/pry +17 -0
- data/bin/puma +17 -0
- data/bin/pumactl +17 -0
- data/bin/rack-webdav +23 -109
- data/bin/rackup +17 -0
- data/bin/rake +17 -0
- data/bin/rspec +17 -0
- data/bin/rubocop +17 -0
- data/bin/ruby-parse +17 -0
- data/bin/ruby-rewrite +17 -0
- data/bin/unicorn +17 -0
- data/bin/unicorn_rails +17 -0
- data/lib/rack-webdav/controller.rb +447 -480
- data/lib/rack-webdav/file_resource.rb +247 -0
- data/lib/rack-webdav/handler.rb +27 -50
- data/lib/rack-webdav/http_status.rb +10 -10
- data/lib/rack-webdav/resource.rb +73 -368
- data/lib/rack-webdav/string.rb +28 -0
- data/lib/rack-webdav/version.rb +10 -11
- data/lib/rack-webdav.rb +5 -1
- data/rack-webdav.gemspec +3 -0
- metadata +62 -9
- data/lib/rack-webdav/file.rb +0 -37
- data/lib/rack-webdav/interceptor.rb +0 -22
- data/lib/rack-webdav/interceptor_resource.rb +0 -119
- data/lib/rack-webdav/lock.rb +0 -40
- data/lib/rack-webdav/logger.rb +0 -30
- data/lib/rack-webdav/remote_file.rb +0 -148
- data/lib/rack-webdav/resources/file_resource.rb +0 -384
|
@@ -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
|
data/lib/rack-webdav/handler.rb
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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',
|