dav4rack 0.0.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 +4 -0
- data/LICENSE +42 -0
- data/README.rdoc +137 -0
- data/bin/dav4rack +65 -0
- data/dav4rack.gemspec +31 -0
- data/lib/dav4rack.rb +13 -0
- data/lib/dav4rack/controller.rb +457 -0
- data/lib/dav4rack/file_resource.rb +176 -0
- data/lib/dav4rack/handler.rb +44 -0
- data/lib/dav4rack/http_status.rb +108 -0
- data/lib/dav4rack/interceptor.rb +20 -0
- data/lib/dav4rack/interceptor_resource.rb +119 -0
- data/lib/dav4rack/remote_file.rb +62 -0
- data/lib/dav4rack/resource.rb +250 -0
- data/spec/handler_spec.rb +270 -0
- metadata +81 -0
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'webrick/httputils'
|
2
|
+
|
3
|
+
module DAV4Rack
|
4
|
+
|
5
|
+
class FileResource < Resource
|
6
|
+
|
7
|
+
include WEBrick::HTTPUtils
|
8
|
+
|
9
|
+
def authenticate(user, pass)
|
10
|
+
if(options[:username])
|
11
|
+
options[:username] == user && options[:password] == pass
|
12
|
+
else
|
13
|
+
true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# If this is a collection, return the child resources.
|
18
|
+
def children
|
19
|
+
Dir[file_path + '/*'].map do |path|
|
20
|
+
child File.basename(path)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Is this resource a collection?
|
25
|
+
def collection?
|
26
|
+
File.directory?(file_path)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Does this recource exist?
|
30
|
+
def exist?
|
31
|
+
File.exist?(file_path)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return the creation time.
|
35
|
+
def creation_date
|
36
|
+
stat.ctime
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return the time of last modification.
|
40
|
+
def last_modified
|
41
|
+
stat.mtime
|
42
|
+
end
|
43
|
+
|
44
|
+
# Set the time of last modification.
|
45
|
+
def last_modified=(time)
|
46
|
+
File.utime(Time.now, time, file_path)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return an Etag, an unique hash value for this resource.
|
50
|
+
def etag
|
51
|
+
sprintf('%x-%x-%x', stat.ino, stat.size, stat.mtime.to_i)
|
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
|
+
# HTTP GET request.
|
69
|
+
#
|
70
|
+
# Write the content of the resource to the response.body.
|
71
|
+
def get(request, response)
|
72
|
+
raise NotFound unless exist?
|
73
|
+
if stat.directory?
|
74
|
+
response.body = ""
|
75
|
+
Rack::Directory.new(root).call(request.env)[2].each do |line|
|
76
|
+
response.body << line
|
77
|
+
end
|
78
|
+
response['Content-Length'] = response.body.size.to_s
|
79
|
+
else
|
80
|
+
file = Rack::File.new(nil)
|
81
|
+
file.path = file_path
|
82
|
+
response.body = file
|
83
|
+
end
|
84
|
+
OK
|
85
|
+
end
|
86
|
+
|
87
|
+
# HTTP PUT request.
|
88
|
+
#
|
89
|
+
# Save the content of the request.body.
|
90
|
+
def put(request, response)
|
91
|
+
write(request.body)
|
92
|
+
Created
|
93
|
+
end
|
94
|
+
|
95
|
+
# HTTP POST request.
|
96
|
+
#
|
97
|
+
# Usually forbidden.
|
98
|
+
def post(request, response)
|
99
|
+
raise HTTPStatus::Forbidden
|
100
|
+
end
|
101
|
+
|
102
|
+
# HTTP DELETE request.
|
103
|
+
#
|
104
|
+
# Delete this resource.
|
105
|
+
def delete
|
106
|
+
if stat.directory?
|
107
|
+
Dir.rmdir(file_path)
|
108
|
+
else
|
109
|
+
File.unlink(file_path)
|
110
|
+
end
|
111
|
+
NoContent
|
112
|
+
end
|
113
|
+
|
114
|
+
# HTTP COPY request.
|
115
|
+
#
|
116
|
+
# Copy this resource to given destination resource.
|
117
|
+
def copy(dest)
|
118
|
+
if stat.directory?
|
119
|
+
dest.make_collection
|
120
|
+
else
|
121
|
+
open(file_path, "rb") do |file|
|
122
|
+
dest.write(file)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
OK
|
126
|
+
end
|
127
|
+
|
128
|
+
# HTTP MOVE request.
|
129
|
+
#
|
130
|
+
# Move this resource to given destination resource.
|
131
|
+
def move(dest)
|
132
|
+
copy(dest)
|
133
|
+
delete
|
134
|
+
OK
|
135
|
+
end
|
136
|
+
|
137
|
+
# HTTP MKCOL request.
|
138
|
+
#
|
139
|
+
# Create this resource as collection.
|
140
|
+
def make_collection
|
141
|
+
Dir.mkdir(file_path)
|
142
|
+
NoContent
|
143
|
+
end
|
144
|
+
|
145
|
+
# Write to this resource from given IO.
|
146
|
+
def write(io)
|
147
|
+
tempfile = "#{file_path}.#{Process.pid}.#{object_id}"
|
148
|
+
|
149
|
+
open(tempfile, "wb") do |file|
|
150
|
+
while part = io.read(8192)
|
151
|
+
file << part
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
File.rename(tempfile, file_path)
|
156
|
+
ensure
|
157
|
+
File.unlink(tempfile) rescue nil
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def root
|
163
|
+
@options[:root]
|
164
|
+
end
|
165
|
+
|
166
|
+
def file_path
|
167
|
+
root + '/' + path
|
168
|
+
end
|
169
|
+
|
170
|
+
def stat
|
171
|
+
@stat ||= File.stat(file_path)
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module DAV4Rack
|
2
|
+
|
3
|
+
class Handler
|
4
|
+
include DAV4Rack::HTTPStatus
|
5
|
+
def initialize(options={})
|
6
|
+
@options = options.dup
|
7
|
+
unless(@options[:resource_class])
|
8
|
+
require 'dav4rack/file_resource'
|
9
|
+
@options[:resource_class] = FileResource
|
10
|
+
@options[:root] ||= Dir.pwd
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
|
16
|
+
request = Rack::Request.new(env)
|
17
|
+
response = Rack::Response.new
|
18
|
+
|
19
|
+
begin
|
20
|
+
controller = Controller.new(request, response, @options.dup)
|
21
|
+
res = controller.send(request.request_method.downcase)
|
22
|
+
response.status = res.code if res.respond_to?(:code)
|
23
|
+
rescue HTTPStatus::Status => status
|
24
|
+
response.status = status.code
|
25
|
+
end
|
26
|
+
|
27
|
+
# Strings in Ruby 1.9 are no longer enumerable. Rack still expects the response.body to be
|
28
|
+
# enumerable, however.
|
29
|
+
|
30
|
+
response['Content-Length'] = response.body.to_s.length unless response['Content-Length'] || response.body.empty?
|
31
|
+
response.body = [response.body] if not response.body.respond_to? :each
|
32
|
+
response.status = response.status ? response.status.to_i : 200
|
33
|
+
response.headers.each_pair{|k,v| response[k] = v.to_s}
|
34
|
+
|
35
|
+
# Apache wants the body dealt with, so just read it and junk it
|
36
|
+
buf = true
|
37
|
+
buf = request.body.read(8192) while buf
|
38
|
+
|
39
|
+
response.finish
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module DAV4Rack
|
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
|
+
DAV4Rack::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,20 @@
|
|
1
|
+
require 'dav4rack/interceptor_resource'
|
2
|
+
module DAV4Rack
|
3
|
+
class Interceptor
|
4
|
+
def initialize(app, args={})
|
5
|
+
@roots = args[:mappings].keys
|
6
|
+
@args = args
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
path = env['PATH_INFO'].downcase
|
12
|
+
method = env['REQUEST_METHOD'].upcase
|
13
|
+
app = nil
|
14
|
+
if(@roots.detect{|x| path =~ /^#{Regexp.escape(x.downcase)}\/?/}.nil? && %w(OPTIONS PUT PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK).include?(method))
|
15
|
+
app = DAV4Rack::Handler.new(:resource_class => InterceptorResource, :mappings => @args[:mappings])
|
16
|
+
end
|
17
|
+
app ? app.call(env) : @app.call(env)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
module DAV4Rack
|
4
|
+
|
5
|
+
class InterceptorResource < Resource
|
6
|
+
attr_reader :path, :options
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
super
|
10
|
+
@root_paths = @options[:mappings].keys
|
11
|
+
@mappings = @options[:mappings]
|
12
|
+
end
|
13
|
+
|
14
|
+
def children
|
15
|
+
childs = @root_paths.find_all{|x|x =~ /^#{Regexp.escape(@path)}/}
|
16
|
+
childs = childs.map{|a| child a.gsub(/^#{Regexp.escape(@path)}/, '').split('/').delete_if{|x|x.empty?}.first }.flatten
|
17
|
+
end
|
18
|
+
|
19
|
+
def collection?
|
20
|
+
true if exist?
|
21
|
+
end
|
22
|
+
|
23
|
+
def exist?
|
24
|
+
!@root_paths.find_all{|x| x =~ /^#{Regexp.escape(@path)}/}.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
def creation_date
|
28
|
+
Time.now
|
29
|
+
end
|
30
|
+
|
31
|
+
def last_modified
|
32
|
+
Time.now
|
33
|
+
end
|
34
|
+
|
35
|
+
def last_modified=(time)
|
36
|
+
Time.now
|
37
|
+
end
|
38
|
+
|
39
|
+
def etag
|
40
|
+
Digest::SHA1.hexdigest(@path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def content_type
|
44
|
+
'text/html'
|
45
|
+
end
|
46
|
+
|
47
|
+
def content_length
|
48
|
+
0
|
49
|
+
end
|
50
|
+
|
51
|
+
def get(request, response)
|
52
|
+
raise Forbidden
|
53
|
+
end
|
54
|
+
|
55
|
+
def put(request, response)
|
56
|
+
raise Forbidden
|
57
|
+
end
|
58
|
+
|
59
|
+
def post(request, response)
|
60
|
+
raise Forbidden
|
61
|
+
end
|
62
|
+
|
63
|
+
def delete
|
64
|
+
raise Forbidden
|
65
|
+
end
|
66
|
+
|
67
|
+
def copy(dest)
|
68
|
+
raise Forbidden
|
69
|
+
end
|
70
|
+
|
71
|
+
def move(dest)
|
72
|
+
raise Forbidden
|
73
|
+
end
|
74
|
+
|
75
|
+
def make_collection
|
76
|
+
raise Forbidden
|
77
|
+
end
|
78
|
+
|
79
|
+
def ==(other)
|
80
|
+
path == other.path
|
81
|
+
end
|
82
|
+
|
83
|
+
def name
|
84
|
+
File.basename(path)
|
85
|
+
end
|
86
|
+
|
87
|
+
def display_name
|
88
|
+
File.basename(path.to_s)
|
89
|
+
end
|
90
|
+
|
91
|
+
def child(name, option={})
|
92
|
+
new_path = path.dup
|
93
|
+
new_path = '/' + new_path unless new_path[0,1] == '/'
|
94
|
+
new_path.slice!(-1) if new_path[-1,1] == '/'
|
95
|
+
name = '/' + name unless name[-1,1] == '/'
|
96
|
+
new_path = "#{new_path}#{name}"
|
97
|
+
new_public = public_path.dup
|
98
|
+
new_public = '/' + new_public unless new_public[0,1] == '/'
|
99
|
+
new_public.slice!(-1) if new_public[-1,1] == '/'
|
100
|
+
new_public = "#{new_public}#{name}"
|
101
|
+
if(key = @root_paths.find{|x| new_path =~ /^#{Regexp.escape(x.downcase)}\/?/})
|
102
|
+
@mappings[key][:resource_class].new(new_public, new_path.gsub(key, ''), request, {:root_uri_path => key}.merge(@mappings[key][:options] ? @mappings[key][:options] : options))
|
103
|
+
else
|
104
|
+
self.class.new(new_public, new_path, request, options)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def descendants
|
109
|
+
list = []
|
110
|
+
children.each do |child|
|
111
|
+
list << child
|
112
|
+
list.concat(child.descendants)
|
113
|
+
end
|
114
|
+
list
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|