rack-webdav 0.4.0
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 +7 -0
- data/.gitignore +8 -0
- data/LICENSE +42 -0
- data/README.rdoc +336 -0
- data/bin/rack-webdav +126 -0
- data/lib/rack-webdav.rb +14 -0
- data/lib/rack-webdav/controller.rb +601 -0
- data/lib/rack-webdav/file.rb +37 -0
- data/lib/rack-webdav/file_resource_lock.rb +169 -0
- data/lib/rack-webdav/handler.rb +63 -0
- data/lib/rack-webdav/http_status.rb +108 -0
- data/lib/rack-webdav/interceptor.rb +22 -0
- data/lib/rack-webdav/interceptor_resource.rb +119 -0
- data/lib/rack-webdav/lock.rb +40 -0
- data/lib/rack-webdav/lock_store.rb +61 -0
- data/lib/rack-webdav/logger.rb +30 -0
- data/lib/rack-webdav/remote_file.rb +148 -0
- data/lib/rack-webdav/resource.rb +485 -0
- data/lib/rack-webdav/resources/file_resource.rb +379 -0
- data/lib/rack-webdav/resources/mongo_resource.rb +341 -0
- data/lib/rack-webdav/utils.rb +40 -0
- data/lib/rack-webdav/version.rb +17 -0
- data/rack-webdav.gemspec +31 -0
- metadata +206 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'rack/utils'
|
3
|
+
require 'rack/mime'
|
4
|
+
|
5
|
+
module RackWebDAV
|
6
|
+
# RackWebDAV::File simply allows us to use Rack::File but with the
|
7
|
+
# specific location we deem appropriate
|
8
|
+
class File < Rack::File
|
9
|
+
attr_accessor :path
|
10
|
+
|
11
|
+
alias :to_path :path
|
12
|
+
|
13
|
+
def initialize(path)
|
14
|
+
@path = path
|
15
|
+
end
|
16
|
+
|
17
|
+
def _call(env)
|
18
|
+
begin
|
19
|
+
if F.file?(@path) && F.readable?(@path)
|
20
|
+
serving(env)
|
21
|
+
else
|
22
|
+
raise Errno::EPERM
|
23
|
+
end
|
24
|
+
rescue SystemCallError
|
25
|
+
not_found
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def not_found
|
30
|
+
body = "File not found: #{Rack::Utils.unescape(env["PATH_INFO"])}\n"
|
31
|
+
[404, {"Content-Type" => "text/plain",
|
32
|
+
"Content-Length" => body.size.to_s,
|
33
|
+
"X-Cascade" => "pass"},
|
34
|
+
[body]]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'pstore'
|
2
|
+
|
3
|
+
module RackWebDAV
|
4
|
+
class FileResourceLock
|
5
|
+
attr_accessor :path
|
6
|
+
attr_accessor :token
|
7
|
+
attr_accessor :timeout
|
8
|
+
attr_accessor :depth
|
9
|
+
attr_accessor :user
|
10
|
+
attr_accessor :scope
|
11
|
+
attr_accessor :kind
|
12
|
+
attr_accessor :owner
|
13
|
+
attr_reader :created_at
|
14
|
+
attr_reader :root
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def explicitly_locked?(path, croot=nil)
|
18
|
+
store = init_pstore(croot)
|
19
|
+
!!store.transaction(true){
|
20
|
+
store[:paths][path]
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def implicitly_locked?(path, croot=nil)
|
25
|
+
store = init_pstore(croot)
|
26
|
+
!!store.transaction(true){
|
27
|
+
store[:paths].keys.detect do |check|
|
28
|
+
check.start_with?(path)
|
29
|
+
end
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def explicit_locks(path, croot, args={})
|
34
|
+
end
|
35
|
+
|
36
|
+
def implict_locks(path)
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_by_path(path, croot=nil)
|
40
|
+
lock = self.class.new(:path => path, :root => croot)
|
41
|
+
lock.token.nil? ? nil : lock
|
42
|
+
end
|
43
|
+
|
44
|
+
def find_by_token(token, croot=nil)
|
45
|
+
store = init_pstore(croot)
|
46
|
+
struct = store.transaction(true){
|
47
|
+
store[:tokens][token]
|
48
|
+
}
|
49
|
+
if !struct
|
50
|
+
struct = store.transaction(true) {
|
51
|
+
store[:tokens].keys.each { |k| token = k if k.include?(token) }
|
52
|
+
store[:tokens][token]
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
if struct
|
57
|
+
self.new(:path => struct[:path], :root => croot)
|
58
|
+
else
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def generate(path, user, token, croot)
|
64
|
+
lock = self.new(:root => croot)
|
65
|
+
lock.user = user
|
66
|
+
lock.path = path
|
67
|
+
lock.token = token
|
68
|
+
lock.save
|
69
|
+
lock
|
70
|
+
end
|
71
|
+
|
72
|
+
def root=(path)
|
73
|
+
@root = path
|
74
|
+
end
|
75
|
+
|
76
|
+
def root
|
77
|
+
@root || '/tmp/rack-webdav'
|
78
|
+
end
|
79
|
+
|
80
|
+
def init_pstore(croot)
|
81
|
+
path = File.join(croot, '.attribs', 'locks.pstore')
|
82
|
+
FileUtils.mkdir_p(File.dirname(path)) unless File.directory?(File.dirname(path))
|
83
|
+
store = IS_18 ? PStore.new(path) : PStore.new(path, true)
|
84
|
+
store.transaction do
|
85
|
+
unless(store[:paths])
|
86
|
+
store[:paths] = {}
|
87
|
+
store[:tokens] = {}
|
88
|
+
store.commit
|
89
|
+
end
|
90
|
+
end
|
91
|
+
store
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def initialize(args={})
|
96
|
+
@path = args[:path]
|
97
|
+
@root = args[:root]
|
98
|
+
@owner = args[:owner]
|
99
|
+
@store = init_pstore(@root)
|
100
|
+
@max_timeout = args[:max_timeout] || 86400
|
101
|
+
@default_timeout = args[:max_timeout] || 60
|
102
|
+
load_if_exists!
|
103
|
+
@new_record = true if token.nil?
|
104
|
+
end
|
105
|
+
|
106
|
+
def owner?(user)
|
107
|
+
user == owner
|
108
|
+
end
|
109
|
+
|
110
|
+
def reload
|
111
|
+
load_if_exists
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
def remaining_timeout
|
116
|
+
t = timeout.to_i - (Time.now.to_i - created_at.to_i)
|
117
|
+
t < 0 ? 0 : t
|
118
|
+
end
|
119
|
+
|
120
|
+
def save
|
121
|
+
struct = {
|
122
|
+
:path => path,
|
123
|
+
:token => token,
|
124
|
+
:timeout => timeout,
|
125
|
+
:depth => depth,
|
126
|
+
:created_at => Time.now,
|
127
|
+
:owner => owner
|
128
|
+
}
|
129
|
+
@store.transaction do
|
130
|
+
@store[:paths][path] = struct
|
131
|
+
@store[:tokens][token] = struct
|
132
|
+
@store.commit
|
133
|
+
end
|
134
|
+
@new_record = false
|
135
|
+
self
|
136
|
+
end
|
137
|
+
|
138
|
+
def destroy
|
139
|
+
@store.transaction do
|
140
|
+
@store[:paths].delete(path)
|
141
|
+
@store[:tokens].delete(token)
|
142
|
+
@store.commit
|
143
|
+
end
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def load_if_exists!
|
150
|
+
struct = @store.transaction do
|
151
|
+
@store[:paths][path]
|
152
|
+
end
|
153
|
+
if(struct)
|
154
|
+
@path = struct[:path]
|
155
|
+
@token = struct[:token]
|
156
|
+
@timeout = struct[:timeout]
|
157
|
+
@depth = struct[:depth]
|
158
|
+
@created_at = struct[:created_at]
|
159
|
+
@owner = struct[:owner]
|
160
|
+
end
|
161
|
+
self
|
162
|
+
end
|
163
|
+
|
164
|
+
def init_pstore(croot=nil)
|
165
|
+
self.class.init_pstore(croot || @root)
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'rack-webdav/logger'
|
2
|
+
|
3
|
+
module RackWebDAV
|
4
|
+
|
5
|
+
class Handler
|
6
|
+
include RackWebDAV::HTTPStatus
|
7
|
+
def initialize(options={})
|
8
|
+
@options = options.dup
|
9
|
+
unless(@options[:resource_class])
|
10
|
+
require 'rack-webdav/resources/file_resource'
|
11
|
+
@options[:resource_class] = FileResource
|
12
|
+
@options[:root] ||= Dir.pwd
|
13
|
+
end
|
14
|
+
Logger.set(*@options[:log_to])
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
begin
|
19
|
+
start = Time.now
|
20
|
+
request = Rack::Request.new(env)
|
21
|
+
response = Rack::Response.new
|
22
|
+
|
23
|
+
Logger.info "Processing WebDAV request: #{request.path} (for #{request.ip} at #{Time.now}) [#{request.request_method}]"
|
24
|
+
|
25
|
+
controller = nil
|
26
|
+
begin
|
27
|
+
controller_class = @options[:controller_class] || Controller
|
28
|
+
controller = controller_class.new(request, response, @options.dup)
|
29
|
+
controller.authenticate
|
30
|
+
res = controller.send(request.request_method.downcase)
|
31
|
+
response.status = res.code if res.respond_to?(:code)
|
32
|
+
rescue HTTPStatus::Unauthorized => status
|
33
|
+
response.body = controller.resource.respond_to?(:authentication_error_msg) ? controller.resource.authentication_error_msg : 'Not Authorized'
|
34
|
+
response['WWW-Authenticate'] = "Basic realm=\"#{controller.resource.respond_to?(:authentication_realm) ? controller.resource.authentication_realm : 'Locked content'}\""
|
35
|
+
response.status = status.code
|
36
|
+
rescue HTTPStatus::Status => status
|
37
|
+
response.status = status.code
|
38
|
+
end
|
39
|
+
|
40
|
+
# Strings in Ruby 1.9 are no longer enumerable. Rack still expects the response.body to be
|
41
|
+
# enumerable, however.
|
42
|
+
|
43
|
+
response['Content-Length'] = response.body.to_s.length unless response['Content-Length'] || !response.body.is_a?(String)
|
44
|
+
response.body = [response.body] unless response.body.respond_to? :each
|
45
|
+
response.status = response.status ? response.status.to_i : 200
|
46
|
+
response.headers.keys.each{|k| response.headers[k] = response[k].to_s}
|
47
|
+
|
48
|
+
# Apache wants the body dealt with, so just read it and junk it
|
49
|
+
buf = true
|
50
|
+
buf = request.body.read(8192) while buf
|
51
|
+
|
52
|
+
Logger.debug "Response in string form. Outputting contents: \n#{response.body}" if response.body.is_a?(String)
|
53
|
+
Logger.info "Completed in: #{((Time.now.to_f - start.to_f) * 1000).to_i} ms | #{response.status} [#{request.url}]"
|
54
|
+
response
|
55
|
+
rescue Exception => e
|
56
|
+
Logger.error "WebDAV Error: #{e}\n#{e.backtrace.join("\n")}"
|
57
|
+
raise e
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module RackWebDAV
|
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
|
+
RackWebDAV::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,22 @@
|
|
1
|
+
require 'rack-webdav/interceptor_resource'
|
2
|
+
module RackWebDAV
|
3
|
+
class Interceptor
|
4
|
+
def initialize(app, args={})
|
5
|
+
@roots = args[:mappings].keys
|
6
|
+
@args = args
|
7
|
+
@app = app
|
8
|
+
@intercept_methods = %w(OPTIONS PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
|
9
|
+
@intercept_methods -= args[:ignore_methods] if args[:ignore_methods]
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
path = env['PATH_INFO'].downcase
|
14
|
+
method = env['REQUEST_METHOD'].upcase
|
15
|
+
app = nil
|
16
|
+
if(@roots.detect{|x| path =~ /^#{Regexp.escape(x.downcase)}\/?/}.nil? && @intercept_methods.include?(method))
|
17
|
+
app = RackWebDAV::Handler.new(:resource_class => InterceptorResource, :mappings => @args[:mappings], :log_to => @args[:log_to])
|
18
|
+
end
|
19
|
+
app ? app.call(env) : @app.call(env)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
module RackWebDAV
|
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, response, {:root_uri_path => key, :user => @user}.merge(options).merge(@mappings[key]))
|
103
|
+
else
|
104
|
+
self.class.new(new_public, new_path, request, response, {:user => @user}.merge(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
|