rack_dav 0.1.3 → 0.3.1
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.
- data/.gitignore +2 -0
- data/CHANGELOG.md +27 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +31 -0
- data/README.md +14 -15
- data/Rakefile +36 -0
- data/bin/rack_dav +11 -7
- data/lib/rack_dav.rb +1 -3
- data/lib/rack_dav/controller.rb +300 -218
- data/lib/rack_dav/file_resource.rb +48 -32
- data/lib/rack_dav/handler.rb +19 -12
- data/lib/rack_dav/http_status.rb +10 -10
- data/lib/rack_dav/resource.rb +21 -17
- data/lib/rack_dav/string.rb +28 -0
- data/lib/rack_dav/version.rb +16 -0
- data/rack_dav.gemspec +22 -26
- data/spec/controller_spec.rb +498 -0
- data/spec/file_resource_spec.rb +11 -0
- data/spec/fixtures/folder/01.txt +0 -0
- data/spec/fixtures/folder/02.txt +0 -0
- data/spec/fixtures/requests/lock.xml +12 -0
- data/spec/handler_spec.rb +23 -256
- data/spec/spec_helper.rb +27 -0
- data/spec/support/lockable_file_resource.rb +30 -0
- metadata +107 -50
- data/lib/rack_dav/builder_namespace.rb +0 -22
@@ -1,8 +1,10 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
1
3
|
module RackDAV
|
2
4
|
|
3
5
|
class FileResource < Resource
|
4
6
|
include WEBrick::HTTPUtils
|
5
|
-
|
7
|
+
|
6
8
|
# If this is a collection, return the child resources.
|
7
9
|
def children
|
8
10
|
Dir[file_path + '/*'].map do |path|
|
@@ -19,7 +21,7 @@ module RackDAV
|
|
19
21
|
def exist?
|
20
22
|
File.exist?(file_path)
|
21
23
|
end
|
22
|
-
|
24
|
+
|
23
25
|
# Return the creation time.
|
24
26
|
def creation_date
|
25
27
|
stat.ctime
|
@@ -29,7 +31,7 @@ module RackDAV
|
|
29
31
|
def last_modified
|
30
32
|
stat.mtime
|
31
33
|
end
|
32
|
-
|
34
|
+
|
33
35
|
# Set the time of last modification.
|
34
36
|
def last_modified=(time)
|
35
37
|
File.utime(Time.now, time, file_path)
|
@@ -42,11 +44,10 @@ module RackDAV
|
|
42
44
|
|
43
45
|
# Return the resource type.
|
44
46
|
#
|
45
|
-
# If this is a collection, return
|
46
|
-
# REXML::Element.new('D:collection')
|
47
|
+
# If this is a collection, return a collection element
|
47
48
|
def resource_type
|
48
49
|
if collection?
|
49
|
-
|
50
|
+
Nokogiri::XML::fragment('<D:collection xmlns:D="DAV:"/>').children.first
|
50
51
|
end
|
51
52
|
end
|
52
53
|
|
@@ -54,7 +55,7 @@ module RackDAV
|
|
54
55
|
def content_type
|
55
56
|
if stat.directory?
|
56
57
|
"text/html"
|
57
|
-
else
|
58
|
+
else
|
58
59
|
mime_type(file_path, DefaultMimeTypes)
|
59
60
|
end
|
60
61
|
end
|
@@ -69,14 +70,12 @@ module RackDAV
|
|
69
70
|
# Write the content of the resource to the response.body.
|
70
71
|
def get(request, response)
|
71
72
|
if stat.directory?
|
72
|
-
|
73
|
-
Rack::Directory.new(root).call(request.env)[2].each
|
74
|
-
|
75
|
-
|
76
|
-
response['Content-Length'] = response.body.size.to_s
|
73
|
+
content = ""
|
74
|
+
Rack::Directory.new(root).call(request.env)[2].each { |line| content << line }
|
75
|
+
response.body = [content]
|
76
|
+
response['Content-Length'] = (content.respond_to?(:bytesize) ? content.bytesize : content.size).to_s
|
77
77
|
else
|
78
|
-
file =
|
79
|
-
file.path = file_path
|
78
|
+
file = File.open(file_path)
|
80
79
|
response.body = file
|
81
80
|
end
|
82
81
|
end
|
@@ -85,16 +84,20 @@ module RackDAV
|
|
85
84
|
#
|
86
85
|
# Save the content of the request.body.
|
87
86
|
def put(request, response)
|
87
|
+
if request.env['HTTP_CONTENT_MD5']
|
88
|
+
content_md5_pass?(request.env) or raise HTTPStatus::BadRequest.new('Content-MD5 mismatch')
|
89
|
+
end
|
90
|
+
|
88
91
|
write(request.body)
|
89
92
|
end
|
90
|
-
|
93
|
+
|
91
94
|
# HTTP POST request.
|
92
95
|
#
|
93
96
|
# Usually forbidden.
|
94
97
|
def post(request, response)
|
95
98
|
raise HTTPStatus::Forbidden
|
96
99
|
end
|
97
|
-
|
100
|
+
|
98
101
|
# HTTP DELETE request.
|
99
102
|
#
|
100
103
|
# Delete this resource.
|
@@ -105,7 +108,7 @@ module RackDAV
|
|
105
108
|
File.unlink(file_path)
|
106
109
|
end
|
107
110
|
end
|
108
|
-
|
111
|
+
|
109
112
|
# HTTP COPY request.
|
110
113
|
#
|
111
114
|
# Copy this resource to given destination resource.
|
@@ -118,7 +121,7 @@ module RackDAV
|
|
118
121
|
end
|
119
122
|
end
|
120
123
|
end
|
121
|
-
|
124
|
+
|
122
125
|
# HTTP MOVE request.
|
123
126
|
#
|
124
127
|
# Move this resource to given destination resource.
|
@@ -126,42 +129,55 @@ module RackDAV
|
|
126
129
|
copy(dest)
|
127
130
|
delete
|
128
131
|
end
|
129
|
-
|
132
|
+
|
130
133
|
# HTTP MKCOL request.
|
131
134
|
#
|
132
135
|
# Create this resource as collection.
|
133
136
|
def make_collection
|
134
137
|
Dir.mkdir(file_path)
|
135
138
|
end
|
136
|
-
|
139
|
+
|
137
140
|
# Write to this resource from given IO.
|
138
141
|
def write(io)
|
139
142
|
tempfile = "#{file_path}.#{Process.pid}.#{object_id}"
|
140
|
-
|
143
|
+
|
141
144
|
open(tempfile, "wb") do |file|
|
142
145
|
while part = io.read(8192)
|
143
146
|
file << part
|
144
147
|
end
|
145
148
|
end
|
146
149
|
|
147
|
-
File.rename(tempfile, file_path)
|
150
|
+
File.rename(tempfile, file_path)
|
148
151
|
ensure
|
149
152
|
File.unlink(tempfile) rescue nil
|
150
153
|
end
|
151
|
-
|
154
|
+
|
155
|
+
|
152
156
|
private
|
153
157
|
|
154
|
-
|
155
|
-
|
156
|
-
|
158
|
+
def root
|
159
|
+
@options[:root]
|
160
|
+
end
|
157
161
|
|
158
|
-
|
159
|
-
|
160
|
-
|
162
|
+
def file_path
|
163
|
+
root + '/' + path
|
164
|
+
end
|
161
165
|
|
162
|
-
|
163
|
-
|
164
|
-
|
166
|
+
def stat
|
167
|
+
@stat ||= File.stat(file_path)
|
168
|
+
end
|
169
|
+
|
170
|
+
def content_md5_pass?(env)
|
171
|
+
expected = env['HTTP_CONTENT_MD5'] or return true
|
172
|
+
|
173
|
+
body = env['rack.input'].dup
|
174
|
+
digest = Digest::MD5.new.digest(body.read)
|
175
|
+
actual = [ digest ].pack('m').strip
|
176
|
+
|
177
|
+
body.rewind
|
178
|
+
|
179
|
+
expected == actual
|
180
|
+
end
|
165
181
|
|
166
182
|
end
|
167
183
|
|
data/lib/rack_dav/handler.rb
CHANGED
@@ -1,8 +1,20 @@
|
|
1
1
|
module RackDAV
|
2
|
-
|
2
|
+
|
3
3
|
class Handler
|
4
|
-
|
5
|
-
|
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 = {})
|
6
18
|
@options = {
|
7
19
|
:resource_class => FileResource,
|
8
20
|
:root => Dir.pwd
|
@@ -10,25 +22,20 @@ module RackDAV
|
|
10
22
|
end
|
11
23
|
|
12
24
|
def call(env)
|
13
|
-
request
|
25
|
+
request = Rack::Request.new(env)
|
14
26
|
response = Rack::Response.new
|
15
27
|
|
16
28
|
begin
|
17
|
-
controller = Controller.new(request, response, @options
|
29
|
+
controller = Controller.new(request, response, @options)
|
18
30
|
controller.send(request.request_method.downcase)
|
19
|
-
|
31
|
+
|
20
32
|
rescue HTTPStatus::Status => status
|
21
33
|
response.status = status.code
|
22
34
|
end
|
23
|
-
|
24
|
-
# Strings in Ruby 1.9 are no longer enumerable. Rack still expects the response.body to be
|
25
|
-
# enumerable, however.
|
26
|
-
response.body = [response.body] if not response.body.respond_to? :each
|
27
35
|
|
28
|
-
response.status = response.status ? response.status.to_i : 200
|
29
36
|
response.finish
|
30
37
|
end
|
31
|
-
|
38
|
+
|
32
39
|
end
|
33
40
|
|
34
41
|
end
|
data/lib/rack_dav/http_status.rb
CHANGED
@@ -1,19 +1,19 @@
|
|
1
1
|
module RackDAV
|
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 RackDAV
|
|
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 RackDAV
|
|
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',
|
data/lib/rack_dav/resource.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
module RackDAV
|
2
2
|
|
3
3
|
class Resource
|
4
|
+
|
4
5
|
attr_reader :path, :options
|
5
|
-
|
6
|
+
|
6
7
|
def initialize(path, options)
|
7
8
|
@path = path
|
8
9
|
@options = options
|
9
10
|
end
|
10
|
-
|
11
|
+
|
11
12
|
# If this is a collection, return the child resources.
|
12
13
|
def children
|
13
14
|
raise NotImplementedError
|
@@ -22,7 +23,7 @@ module RackDAV
|
|
22
23
|
def exist?
|
23
24
|
raise NotImplementedError
|
24
25
|
end
|
25
|
-
|
26
|
+
|
26
27
|
# Return the creation time.
|
27
28
|
def creation_date
|
28
29
|
raise NotImplementedError
|
@@ -32,7 +33,7 @@ module RackDAV
|
|
32
33
|
def last_modified
|
33
34
|
raise NotImplementedError
|
34
35
|
end
|
35
|
-
|
36
|
+
|
36
37
|
# Set the time of last modification.
|
37
38
|
def last_modified=(time)
|
38
39
|
raise NotImplementedError
|
@@ -45,11 +46,10 @@ module RackDAV
|
|
45
46
|
|
46
47
|
# Return the resource type.
|
47
48
|
#
|
48
|
-
# If this is a collection, return
|
49
|
-
# REXML::Element.new('D:collection')
|
49
|
+
# If this is a collection, return a collection element
|
50
50
|
def resource_type
|
51
51
|
if collection?
|
52
|
-
|
52
|
+
Nokogiri::XML::fragment('<D:collection xmlns:D="DAV:"/>').children.first
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
@@ -76,28 +76,28 @@ module RackDAV
|
|
76
76
|
def put(request, response)
|
77
77
|
raise NotImplementedError
|
78
78
|
end
|
79
|
-
|
79
|
+
|
80
80
|
# HTTP POST request.
|
81
81
|
#
|
82
82
|
# Usually forbidden.
|
83
83
|
def post(request, response)
|
84
84
|
raise NotImplementedError
|
85
85
|
end
|
86
|
-
|
86
|
+
|
87
87
|
# HTTP DELETE request.
|
88
88
|
#
|
89
89
|
# Delete this resource.
|
90
90
|
def delete
|
91
91
|
raise NotImplementedError
|
92
92
|
end
|
93
|
-
|
93
|
+
|
94
94
|
# HTTP COPY request.
|
95
95
|
#
|
96
96
|
# Copy this resource to given destination resource.
|
97
97
|
def copy(dest)
|
98
98
|
raise NotImplementedError
|
99
99
|
end
|
100
|
-
|
100
|
+
|
101
101
|
# HTTP MOVE request.
|
102
102
|
#
|
103
103
|
# Move this resource to given destination resource.
|
@@ -105,7 +105,7 @@ module RackDAV
|
|
105
105
|
copy(dest)
|
106
106
|
delete
|
107
107
|
end
|
108
|
-
|
108
|
+
|
109
109
|
# HTTP MKCOL request.
|
110
110
|
#
|
111
111
|
# Create this resource as collection.
|
@@ -124,20 +124,24 @@ module RackDAV
|
|
124
124
|
def display_name
|
125
125
|
name
|
126
126
|
end
|
127
|
-
|
127
|
+
|
128
128
|
def child(name, option={})
|
129
129
|
self.class.new(path + '/' + name, options)
|
130
130
|
end
|
131
|
-
|
131
|
+
|
132
|
+
def lockable?
|
133
|
+
self.respond_to?(:lock) && self.respond_to?(:unlock)
|
134
|
+
end
|
135
|
+
|
132
136
|
def property_names
|
133
137
|
%w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength)
|
134
138
|
end
|
135
|
-
|
139
|
+
|
136
140
|
def get_property(name)
|
137
141
|
case name
|
138
142
|
when 'resourcetype' then resource_type
|
139
143
|
when 'displayname' then display_name
|
140
|
-
when 'creationdate' then creation_date.xmlschema
|
144
|
+
when 'creationdate' then creation_date.xmlschema
|
141
145
|
when 'getcontentlength' then content_length.to_s
|
142
146
|
when 'getcontenttype' then content_type
|
143
147
|
when 'getetag' then etag
|
@@ -165,7 +169,7 @@ module RackDAV
|
|
165
169
|
return nil if elements.empty?
|
166
170
|
self.class.new('/' + elements[0..-2].to_a.join('/'), @options)
|
167
171
|
end
|
168
|
-
|
172
|
+
|
169
173
|
def descendants
|
170
174
|
list = []
|
171
175
|
children.each do |child|
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class String
|
2
|
+
|
3
|
+
if RUBY_VERSION >= "1.9"
|
4
|
+
def force_valid_encoding
|
5
|
+
find_encoding(Encoding.list.to_enum)
|
6
|
+
end
|
7
|
+
else
|
8
|
+
def force_valid_encoding
|
9
|
+
self
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def find_encoding(encodings)
|
16
|
+
if valid_encoding?
|
17
|
+
self
|
18
|
+
else
|
19
|
+
force_next_encoding(encodings)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def force_next_encoding(encodings)
|
24
|
+
force_encoding(encodings.next)
|
25
|
+
find_encoding(encodings)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module RackDAV
|
2
|
+
|
3
|
+
# Holds information about library version.
|
4
|
+
module Version
|
5
|
+
MAJOR = 0
|
6
|
+
MINOR = 3
|
7
|
+
PATCH = 1
|
8
|
+
BUILD = nil
|
9
|
+
|
10
|
+
STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join(".")
|
11
|
+
end
|
12
|
+
|
13
|
+
# The current library version.
|
14
|
+
VERSION = Version::STRING
|
15
|
+
|
16
|
+
end
|