rack_dav_sp 0.2.dev
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 +4 -0
- data/CHANGELOG.md +27 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +31 -0
- data/LICENSE +18 -0
- data/README.md +107 -0
- data/Rakefile +36 -0
- data/bin/rack_dav +23 -0
- data/lib/rack_dav.rb +14 -0
- data/lib/rack_dav/builder_namespace.rb +21 -0
- data/lib/rack_dav/controller.rb +495 -0
- data/lib/rack_dav/file_resource.rb +168 -0
- data/lib/rack_dav/handler.rb +41 -0
- data/lib/rack_dav/http_status.rb +108 -0
- data/lib/rack_dav/resource.rb +185 -0
- data/lib/rack_dav/version.rb +16 -0
- data/rack_dav.gemspec +23 -0
- data/spec/controller_spec.rb +441 -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 +37 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/lockable_file_resource.rb +30 -0
- metadata +104 -0
@@ -0,0 +1,168 @@
|
|
1
|
+
module RackDAV
|
2
|
+
|
3
|
+
class FileResource < Resource
|
4
|
+
include WEBrick::HTTPUtils
|
5
|
+
|
6
|
+
# If this is a collection, return the child resources.
|
7
|
+
def children
|
8
|
+
Dir[file_path + '/*'].map do |path|
|
9
|
+
child File.basename(path)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Is this resource a collection?
|
14
|
+
def collection?
|
15
|
+
File.directory?(file_path)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Does this recource exist?
|
19
|
+
def exist?
|
20
|
+
File.exist?(file_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return the creation time.
|
24
|
+
def creation_date
|
25
|
+
stat.ctime
|
26
|
+
end
|
27
|
+
|
28
|
+
# Return the time of last modification.
|
29
|
+
def last_modified
|
30
|
+
stat.mtime
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set the time of last modification.
|
34
|
+
def last_modified=(time)
|
35
|
+
File.utime(Time.now, time, file_path)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return an Etag, an unique hash value for this resource.
|
39
|
+
def etag
|
40
|
+
sprintf('%x-%x-%x', stat.ino, stat.size, stat.mtime.to_i)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Return the resource type.
|
44
|
+
#
|
45
|
+
# If this is a collection, return
|
46
|
+
# REXML::Element.new('D:collection')
|
47
|
+
def resource_type
|
48
|
+
if collection?
|
49
|
+
REXML::Element.new('D:collection')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return the mime type of this resource.
|
54
|
+
def content_type
|
55
|
+
if stat.directory?
|
56
|
+
"text/html"
|
57
|
+
else
|
58
|
+
mime_type(file_path, DefaultMimeTypes)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Return the size in bytes for this resource.
|
63
|
+
def content_length
|
64
|
+
stat.size
|
65
|
+
end
|
66
|
+
|
67
|
+
# HTTP GET request.
|
68
|
+
#
|
69
|
+
# Write the content of the resource to the response.body.
|
70
|
+
def get(request, response)
|
71
|
+
if stat.directory?
|
72
|
+
content = ""
|
73
|
+
Rack::Directory.new(root).call(request.env)[2].each { |line| content << line }
|
74
|
+
response.body = [content]
|
75
|
+
response['Content-Length'] = content.size.to_s
|
76
|
+
else
|
77
|
+
file = Rack::File.new(nil)
|
78
|
+
file.path = file_path
|
79
|
+
response.body = file
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# HTTP PUT request.
|
84
|
+
#
|
85
|
+
# Save the content of the request.body.
|
86
|
+
def put(request, response)
|
87
|
+
write(request.body)
|
88
|
+
end
|
89
|
+
|
90
|
+
# HTTP POST request.
|
91
|
+
#
|
92
|
+
# Usually forbidden.
|
93
|
+
def post(request, response)
|
94
|
+
raise HTTPStatus::Forbidden
|
95
|
+
end
|
96
|
+
|
97
|
+
# HTTP DELETE request.
|
98
|
+
#
|
99
|
+
# Delete this resource.
|
100
|
+
def delete
|
101
|
+
if stat.directory?
|
102
|
+
Dir.rmdir(file_path)
|
103
|
+
else
|
104
|
+
File.unlink(file_path)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# HTTP COPY request.
|
109
|
+
#
|
110
|
+
# Copy this resource to given destination resource.
|
111
|
+
def copy(dest)
|
112
|
+
if stat.directory?
|
113
|
+
dest.make_collection
|
114
|
+
else
|
115
|
+
open(file_path, "rb") do |file|
|
116
|
+
dest.write(file)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# HTTP MOVE request.
|
122
|
+
#
|
123
|
+
# Move this resource to given destination resource.
|
124
|
+
def move(dest)
|
125
|
+
copy(dest)
|
126
|
+
delete
|
127
|
+
end
|
128
|
+
|
129
|
+
# HTTP MKCOL request.
|
130
|
+
#
|
131
|
+
# Create this resource as collection.
|
132
|
+
def make_collection
|
133
|
+
Dir.mkdir(file_path)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Write to this resource from given IO.
|
137
|
+
def write(io)
|
138
|
+
tempfile = "#{file_path}.#{Process.pid}.#{object_id}"
|
139
|
+
|
140
|
+
open(tempfile, "wb") do |file|
|
141
|
+
while part = io.read(8192)
|
142
|
+
file << part
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
File.rename(tempfile, file_path)
|
147
|
+
ensure
|
148
|
+
File.unlink(tempfile) rescue nil
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def root
|
155
|
+
@options[:root]
|
156
|
+
end
|
157
|
+
|
158
|
+
def file_path
|
159
|
+
root + '/' + path
|
160
|
+
end
|
161
|
+
|
162
|
+
def stat
|
163
|
+
@stat ||= File.stat(file_path)
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module RackDAV
|
2
|
+
|
3
|
+
class Handler
|
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)
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(env)
|
25
|
+
request = Rack::Request.new(env)
|
26
|
+
response = Rack::Response.new
|
27
|
+
|
28
|
+
begin
|
29
|
+
controller = Controller.new(request, response, @options)
|
30
|
+
controller.send(request.request_method.downcase)
|
31
|
+
|
32
|
+
rescue HTTPStatus::Status => status
|
33
|
+
response.status = status.code
|
34
|
+
end
|
35
|
+
|
36
|
+
response.finish
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module RackDAV
|
2
|
+
|
3
|
+
module HTTPStatus
|
4
|
+
|
5
|
+
class Status < Exception
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :code, :reason_phrase
|
9
|
+
alias_method :to_i, :code
|
10
|
+
|
11
|
+
def status_line
|
12
|
+
"#{code} #{reason_phrase}"
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
def code
|
18
|
+
self.class.code
|
19
|
+
end
|
20
|
+
|
21
|
+
def reason_phrase
|
22
|
+
self.class.reason_phrase
|
23
|
+
end
|
24
|
+
|
25
|
+
def status_line
|
26
|
+
self.class.status_line
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_i
|
30
|
+
self.class.to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
StatusMessage = {
|
36
|
+
100 => 'Continue',
|
37
|
+
101 => 'Switching Protocols',
|
38
|
+
102 => 'Processing',
|
39
|
+
200 => 'OK',
|
40
|
+
201 => 'Created',
|
41
|
+
202 => 'Accepted',
|
42
|
+
203 => 'Non-Authoritative Information',
|
43
|
+
204 => 'No Content',
|
44
|
+
205 => 'Reset Content',
|
45
|
+
206 => 'Partial Content',
|
46
|
+
207 => 'Multi-Status',
|
47
|
+
300 => 'Multiple Choices',
|
48
|
+
301 => 'Moved Permanently',
|
49
|
+
302 => 'Found',
|
50
|
+
303 => 'See Other',
|
51
|
+
304 => 'Not Modified',
|
52
|
+
305 => 'Use Proxy',
|
53
|
+
307 => 'Temporary Redirect',
|
54
|
+
400 => 'Bad Request',
|
55
|
+
401 => 'Unauthorized',
|
56
|
+
402 => 'Payment Required',
|
57
|
+
403 => 'Forbidden',
|
58
|
+
404 => 'Not Found',
|
59
|
+
405 => 'Method Not Allowed',
|
60
|
+
406 => 'Not Acceptable',
|
61
|
+
407 => 'Proxy Authentication Required',
|
62
|
+
408 => 'Request Timeout',
|
63
|
+
409 => 'Conflict',
|
64
|
+
410 => 'Gone',
|
65
|
+
411 => 'Length Required',
|
66
|
+
412 => 'Precondition Failed',
|
67
|
+
413 => 'Request Entity Too Large',
|
68
|
+
414 => 'Request-URI Too Large',
|
69
|
+
415 => 'Unsupported Media Type',
|
70
|
+
416 => 'Request Range Not Satisfiable',
|
71
|
+
417 => 'Expectation Failed',
|
72
|
+
422 => 'Unprocessable Entity',
|
73
|
+
423 => 'Locked',
|
74
|
+
424 => 'Failed Dependency',
|
75
|
+
500 => 'Internal Server Error',
|
76
|
+
501 => 'Not Implemented',
|
77
|
+
502 => 'Bad Gateway',
|
78
|
+
503 => 'Service Unavailable',
|
79
|
+
504 => 'Gateway Timeout',
|
80
|
+
505 => 'HTTP Version Not Supported',
|
81
|
+
507 => 'Insufficient Storage'
|
82
|
+
}
|
83
|
+
|
84
|
+
StatusMessage.each do |code, reason_phrase|
|
85
|
+
klass = Class.new(Status)
|
86
|
+
klass.code = code
|
87
|
+
klass.reason_phrase = reason_phrase
|
88
|
+
klass_name = reason_phrase.gsub(/[ \-]/,'')
|
89
|
+
const_set(klass_name, klass)
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
module Rack
|
98
|
+
class Response
|
99
|
+
module Helpers
|
100
|
+
RackDAV::HTTPStatus::StatusMessage.each do |code, reason_phrase|
|
101
|
+
name = reason_phrase.gsub(/[ \-]/,'_').downcase
|
102
|
+
define_method(name + '?') do
|
103
|
+
@status == code
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module RackDAV
|
2
|
+
|
3
|
+
class Resource
|
4
|
+
|
5
|
+
attr_reader :path, :options
|
6
|
+
|
7
|
+
def initialize(path, options)
|
8
|
+
@path = path
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
# If this is a collection, return the child resources.
|
13
|
+
def children
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
# Is this resource a collection?
|
18
|
+
def collection?
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
# Does this recource exist?
|
23
|
+
def exist?
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return the creation time.
|
28
|
+
def creation_date
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return the time of last modification.
|
33
|
+
def last_modified
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
# Set the time of last modification.
|
38
|
+
def last_modified=(time)
|
39
|
+
raise NotImplementedError
|
40
|
+
end
|
41
|
+
|
42
|
+
# Return an Etag, an unique hash value for this resource.
|
43
|
+
def etag
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return the resource type.
|
48
|
+
#
|
49
|
+
# If this is a collection, return
|
50
|
+
# REXML::Element.new('D:collection')
|
51
|
+
def resource_type
|
52
|
+
if collection?
|
53
|
+
REXML::Element.new('D:collection')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return the mime type of this resource.
|
58
|
+
def content_type
|
59
|
+
raise NotImplementedError
|
60
|
+
end
|
61
|
+
|
62
|
+
# Return the size in bytes for this resource.
|
63
|
+
def content_length
|
64
|
+
raise NotImplementedError
|
65
|
+
end
|
66
|
+
|
67
|
+
# HTTP GET request.
|
68
|
+
#
|
69
|
+
# Write the content of the resource to the response.body.
|
70
|
+
def get(request, response)
|
71
|
+
raise NotImplementedError
|
72
|
+
end
|
73
|
+
|
74
|
+
# HTTP PUT request.
|
75
|
+
#
|
76
|
+
# Save the content of the request.body.
|
77
|
+
def put(request, response)
|
78
|
+
raise NotImplementedError
|
79
|
+
end
|
80
|
+
|
81
|
+
# HTTP POST request.
|
82
|
+
#
|
83
|
+
# Usually forbidden.
|
84
|
+
def post(request, response)
|
85
|
+
raise NotImplementedError
|
86
|
+
end
|
87
|
+
|
88
|
+
# HTTP DELETE request.
|
89
|
+
#
|
90
|
+
# Delete this resource.
|
91
|
+
def delete
|
92
|
+
raise NotImplementedError
|
93
|
+
end
|
94
|
+
|
95
|
+
# HTTP COPY request.
|
96
|
+
#
|
97
|
+
# Copy this resource to given destination resource.
|
98
|
+
def copy(dest)
|
99
|
+
raise NotImplementedError
|
100
|
+
end
|
101
|
+
|
102
|
+
# HTTP MOVE request.
|
103
|
+
#
|
104
|
+
# Move this resource to given destination resource.
|
105
|
+
def move(dest)
|
106
|
+
copy(dest)
|
107
|
+
delete
|
108
|
+
end
|
109
|
+
|
110
|
+
# HTTP MKCOL request.
|
111
|
+
#
|
112
|
+
# Create this resource as collection.
|
113
|
+
def make_collection
|
114
|
+
raise NotImplementedError
|
115
|
+
end
|
116
|
+
|
117
|
+
def ==(other)
|
118
|
+
path == other.path
|
119
|
+
end
|
120
|
+
|
121
|
+
def name
|
122
|
+
File.basename(path)
|
123
|
+
end
|
124
|
+
|
125
|
+
def display_name
|
126
|
+
name
|
127
|
+
end
|
128
|
+
|
129
|
+
def child(name, option={})
|
130
|
+
self.class.new(path + '/' + name, options)
|
131
|
+
end
|
132
|
+
|
133
|
+
def lockable?
|
134
|
+
self.respond_to?(:lock) && self.respond_to?(:unlock)
|
135
|
+
end
|
136
|
+
|
137
|
+
def property_names
|
138
|
+
%w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength)
|
139
|
+
end
|
140
|
+
|
141
|
+
def get_property(name)
|
142
|
+
case name
|
143
|
+
when 'resourcetype' then resource_type
|
144
|
+
when 'displayname' then display_name
|
145
|
+
when 'creationdate' then creation_date.xmlschema
|
146
|
+
when 'getcontentlength' then content_length.to_s
|
147
|
+
when 'getcontenttype' then content_type
|
148
|
+
when 'getetag' then etag
|
149
|
+
when 'getlastmodified' then last_modified.httpdate
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def set_property(name, value)
|
154
|
+
case name
|
155
|
+
when 'resourcetype' then self.resource_type = value
|
156
|
+
when 'getcontenttype' then self.content_type = value
|
157
|
+
when 'getetag' then self.etag = value
|
158
|
+
when 'getlastmodified' then self.last_modified = Time.httpdate(value)
|
159
|
+
end
|
160
|
+
rescue ArgumentError
|
161
|
+
raise HTTPStatus::Conflict
|
162
|
+
end
|
163
|
+
|
164
|
+
def remove_property(name)
|
165
|
+
raise HTTPStatus::Forbidden
|
166
|
+
end
|
167
|
+
|
168
|
+
def parent
|
169
|
+
elements = @path.scan(/[^\/]+/)
|
170
|
+
return nil if elements.empty?
|
171
|
+
self.class.new('/' + elements[0..-2].to_a.join('/'), @options)
|
172
|
+
end
|
173
|
+
|
174
|
+
def descendants
|
175
|
+
list = []
|
176
|
+
children.each do |child|
|
177
|
+
list << child
|
178
|
+
list.concat(child.descendants)
|
179
|
+
end
|
180
|
+
list
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|