rack-webdav 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,40 @@
|
|
1
|
+
module RackWebDAV
|
2
|
+
class Lock
|
3
|
+
|
4
|
+
def initialize(args={})
|
5
|
+
@args = args
|
6
|
+
@store = nil
|
7
|
+
@args[:created_at] = Time.now
|
8
|
+
@args[:updated_at] = Time.now
|
9
|
+
end
|
10
|
+
|
11
|
+
def store
|
12
|
+
@store
|
13
|
+
end
|
14
|
+
|
15
|
+
def store=(s)
|
16
|
+
raise TypeError.new 'Expecting LockStore' unless s.respond_to? :remove
|
17
|
+
@store = s
|
18
|
+
end
|
19
|
+
|
20
|
+
def destroy
|
21
|
+
if(@store)
|
22
|
+
@store.remove(self)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def remaining_timeout
|
27
|
+
@args[:timeout].to_i - (Time.now.to_i - @args[:created_at].to_i)
|
28
|
+
end
|
29
|
+
|
30
|
+
def method_missing(*args)
|
31
|
+
if(@args.has_key?(args.first.to_sym))
|
32
|
+
@args[args.first.to_sym]
|
33
|
+
elsif(args.first.to_s[-1,1] == '=')
|
34
|
+
@args[args.first.to_s[0, args.first.to_s.length - 1].to_sym] = args[1]
|
35
|
+
else
|
36
|
+
raise NoMethodError.new "Undefined method #{args.first} for #{self}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'rack-webdav/lock'
|
2
|
+
module RackWebDAV
|
3
|
+
class LockStore
|
4
|
+
class << self
|
5
|
+
def create
|
6
|
+
@locks_by_path = {}
|
7
|
+
@locks_by_token = {}
|
8
|
+
end
|
9
|
+
def add(lock)
|
10
|
+
@locks_by_path[lock.path] = lock
|
11
|
+
@locks_by_token[lock.token] = lock
|
12
|
+
end
|
13
|
+
|
14
|
+
def remove(lock)
|
15
|
+
@locks_by_path.delete(lock.path)
|
16
|
+
@locks_by_token.delete(lock.token)
|
17
|
+
end
|
18
|
+
|
19
|
+
def find_by_path(path)
|
20
|
+
@locks_by_path.map do |lpath, lock|
|
21
|
+
lpath == path && lock.remaining_timeout > 0 ? lock : nil
|
22
|
+
end.compact.first
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_by_token(token)
|
26
|
+
@locks_by_token.map do |ltoken, lock|
|
27
|
+
ltoken == token && lock.remaining_timeout > 0 ? lock : nil
|
28
|
+
end.compact.first
|
29
|
+
end
|
30
|
+
|
31
|
+
def explicit_locks(path)
|
32
|
+
@locks_by_path.map do |lpath, lock|
|
33
|
+
lpath == path && lock.remaining_timeout > 0 ? lock : nil
|
34
|
+
end.compact
|
35
|
+
end
|
36
|
+
|
37
|
+
def implicit_locks(path)
|
38
|
+
@locks_by_path.map do |lpath, lock|
|
39
|
+
lpath =~ /^#{Regexp.escape(path)}/ && lock.remaining_timeout > 0 && lock.depth > 0 ? lock : nil
|
40
|
+
end.compact
|
41
|
+
end
|
42
|
+
|
43
|
+
def explicitly_locked?(path)
|
44
|
+
self.explicit_locks(path).size > 0
|
45
|
+
end
|
46
|
+
|
47
|
+
def implicitly_locked?(path)
|
48
|
+
self.implicit_locks(path).size > 0
|
49
|
+
end
|
50
|
+
|
51
|
+
def generate(path, user, token)
|
52
|
+
l = Lock.new(:path => path, :user => user, :token => token)
|
53
|
+
l.store = self
|
54
|
+
add(l)
|
55
|
+
l
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
RackWebDAV::LockStore.create
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module RackWebDAV
|
4
|
+
# This is a simple wrapper for the Logger class. It allows easy access
|
5
|
+
# to log messages from the library.
|
6
|
+
class Logger
|
7
|
+
class << self
|
8
|
+
# args:: Arguments for Logger -> [path, level] (level is optional) or a Logger instance
|
9
|
+
# Set the path to the log file.
|
10
|
+
def set(*args)
|
11
|
+
if(%w(info debug warn fatal).all?{|meth| args.first.respond_to?(meth)})
|
12
|
+
@@logger = args.first
|
13
|
+
elsif(args.first.respond_to?(:to_s) && !args.first.to_s.empty?)
|
14
|
+
@@logger = ::Logger.new(args.first.to_s, 'weekly')
|
15
|
+
elsif(args.first)
|
16
|
+
raise 'Invalid type specified for logger'
|
17
|
+
end
|
18
|
+
if(args.size > 1)
|
19
|
+
@@logger.level = args[1]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing(*args)
|
24
|
+
if(defined? @@logger)
|
25
|
+
@@logger.send *args
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
require 'digest/sha1'
|
4
|
+
require 'rack/file'
|
5
|
+
|
6
|
+
module RackWebDAV
|
7
|
+
|
8
|
+
class RemoteFile < Rack::File
|
9
|
+
|
10
|
+
attr_accessor :path
|
11
|
+
|
12
|
+
alias :to_path :path
|
13
|
+
|
14
|
+
# path:: path to file (Actual path, preferably a URL since this is a *REMOTE* file)
|
15
|
+
# args:: Hash of arguments:
|
16
|
+
# :size -> Integer - number of bytes
|
17
|
+
# :mime_type -> String - mime type
|
18
|
+
# :last_modified -> String/Time - Time of last modification
|
19
|
+
# :sendfile -> True or String to define sendfile header variation
|
20
|
+
# :cache_directory -> Where to store cached files
|
21
|
+
# :cache_ref -> Reference to be used for cache file name (useful for changing URLs like S3)
|
22
|
+
# :sendfile_prefix -> String directory prefix. Eg: 'webdav' will result in: /wedav/#{path.sub('http://', '')}
|
23
|
+
# :sendfile_fail_gracefully -> Boolean if true will simply proxy if unable to determine proper sendfile
|
24
|
+
def initialize(path, args={})
|
25
|
+
@path = path
|
26
|
+
@args = args
|
27
|
+
@heads = {}
|
28
|
+
@cache_file = args[:cache_directory] ? cache_file_path : nil
|
29
|
+
@redefine_prefix = nil
|
30
|
+
if(@cache_file && File.exists?(@cache_file))
|
31
|
+
@root = ''
|
32
|
+
@path_info = @cache_file
|
33
|
+
@path = @path_info
|
34
|
+
elsif(args[:sendfile])
|
35
|
+
@redefine_prefix = 'sendfile'
|
36
|
+
@sendfile_header = args[:sendfile].is_a?(String) ? args[:sendfile] : nil
|
37
|
+
else
|
38
|
+
setup_remote
|
39
|
+
end
|
40
|
+
do_redefines(@redefine_prefix) if @redefine_prefix
|
41
|
+
end
|
42
|
+
|
43
|
+
# env:: Environment variable hash
|
44
|
+
# Process the call
|
45
|
+
def call(env)
|
46
|
+
serving(env)
|
47
|
+
end
|
48
|
+
|
49
|
+
# env:: Environment variable hash
|
50
|
+
# Return an empty result with the proper header information
|
51
|
+
def sendfile_serving(env)
|
52
|
+
header = @sendfile_header || env['sendfile.type'] || env['HTTP_X_SENDFILE_TYPE']
|
53
|
+
unless(header)
|
54
|
+
raise 'Failed to determine proper sendfile header value' unless @args[:sendfile_fail_gracefully]
|
55
|
+
setup_remote
|
56
|
+
do_redefines('remote')
|
57
|
+
call(env)
|
58
|
+
end
|
59
|
+
prefix = (@args[:sendfile_prefix] || env['HTTP_X_ACCEL_REMOTE_MAPPING']).to_s.sub(/^\//, '').sub(/\/$/, '')
|
60
|
+
[200, {
|
61
|
+
"Last-Modified" => last_modified,
|
62
|
+
"Content-Type" => content_type,
|
63
|
+
"Content-Length" => size,
|
64
|
+
"Redirect-URL" => @path,
|
65
|
+
"Redirect-Host" => @path.scan(%r{^https?://([^/\?]+)}).first.first,
|
66
|
+
header => "/#{prefix}"
|
67
|
+
},
|
68
|
+
['']]
|
69
|
+
end
|
70
|
+
|
71
|
+
# env:: Environment variable hash
|
72
|
+
# Return self to be processed
|
73
|
+
def remote_serving(e)
|
74
|
+
[200, {
|
75
|
+
"Last-Modified" => last_modified,
|
76
|
+
"Content-Type" => content_type,
|
77
|
+
"Content-Length" => size
|
78
|
+
}, self]
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get the remote file
|
82
|
+
def remote_each
|
83
|
+
if(@store)
|
84
|
+
yield @store
|
85
|
+
else
|
86
|
+
@con.request_get(@call_path) do |res|
|
87
|
+
res.read_body(@store) do |part|
|
88
|
+
@cf.write part if @cf
|
89
|
+
yield part
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Size based on remote headers or given size
|
96
|
+
def size
|
97
|
+
@heads['content-length'] || @size
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
# Content type based on provided or remote headers
|
103
|
+
def content_type
|
104
|
+
@mime_type || @heads['content-type']
|
105
|
+
end
|
106
|
+
|
107
|
+
# Last modified type based on provided, remote headers or current time
|
108
|
+
def last_modified
|
109
|
+
@heads['last-modified'] || @modified || Time.now.httpdate
|
110
|
+
end
|
111
|
+
|
112
|
+
# Builds the path for the cached file
|
113
|
+
def cache_file_path
|
114
|
+
raise IOError.new 'Write permission is required for cache directory' unless File.writable?(@args[:cache_directory])
|
115
|
+
"#{@args[:cache_directory]}/#{Digest::SHA1.hexdigest((@args[:cache_ref] || @path).to_s + size.to_s + last_modified.to_s)}.cache"
|
116
|
+
end
|
117
|
+
|
118
|
+
# prefix:: prefix of methods to be redefined
|
119
|
+
# Redefine methods to do what we want in the proper situation
|
120
|
+
def do_redefines(prefix)
|
121
|
+
self.public_methods.each do |method|
|
122
|
+
m = method.to_s.dup
|
123
|
+
next unless m.slice!(0, prefix.to_s.length + 1) == "#{prefix}_"
|
124
|
+
self.class.class_eval "undef :'#{m}'"
|
125
|
+
self.class.class_eval "alias :'#{m}' :'#{method}'"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Sets up all the requirements for proxying a remote file
|
130
|
+
def setup_remote
|
131
|
+
if(@cache_file)
|
132
|
+
begin
|
133
|
+
@cf = File.open(@cache_file, 'w+')
|
134
|
+
rescue
|
135
|
+
@cf = nil
|
136
|
+
end
|
137
|
+
end
|
138
|
+
@uri = URI.parse(@path)
|
139
|
+
@con = Net::HTTP.new(@uri.host, @uri.port)
|
140
|
+
@call_path = @uri.path + (@uri.query ? "?#{@uri.query}" : '')
|
141
|
+
res = @con.request_get(@call_path)
|
142
|
+
@heads = res.to_hash
|
143
|
+
res.value
|
144
|
+
@store = nil
|
145
|
+
@redefine_prefix = 'remote'
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,485 @@
|
|
1
|
+
require 'uuidtools'
|
2
|
+
require 'rack-webdav/http_status'
|
3
|
+
|
4
|
+
module RackWebDAV
|
5
|
+
|
6
|
+
class LockFailure < RuntimeError
|
7
|
+
attr_reader :path_status
|
8
|
+
def initialize(*args)
|
9
|
+
super(*args)
|
10
|
+
@path_status = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_failure(path, status)
|
14
|
+
@path_status[path] = status
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Resource
|
19
|
+
attr_reader :path, :options, :public_path, :request,
|
20
|
+
:response, :propstat_relative_path, :root_xml_attributes
|
21
|
+
attr_accessor :user
|
22
|
+
@@blocks = {}
|
23
|
+
|
24
|
+
class << self
|
25
|
+
|
26
|
+
# This lets us define a bunch of before and after blocks that are
|
27
|
+
# either called before all methods on the resource, or only specific
|
28
|
+
# methods on the resource
|
29
|
+
def method_missing(*args, &block)
|
30
|
+
class_sym = self.name.to_sym
|
31
|
+
@@blocks[class_sym] ||= {:before => {}, :after => {}}
|
32
|
+
m = args.shift
|
33
|
+
parts = m.to_s.split('_')
|
34
|
+
type = parts.shift.to_s.to_sym
|
35
|
+
method = parts.empty? ? nil : parts.join('_').to_sym
|
36
|
+
if(@@blocks[class_sym][type] && block_given?)
|
37
|
+
if(method)
|
38
|
+
@@blocks[class_sym][type][method] ||= []
|
39
|
+
@@blocks[class_sym][type][method] << block
|
40
|
+
else
|
41
|
+
@@blocks[class_sym][type][:'__all__'] ||= []
|
42
|
+
@@blocks[class_sym][type][:'__all__'] << block
|
43
|
+
end
|
44
|
+
else
|
45
|
+
raise NoMethodError.new("Undefined method #{m} for class #{self}")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
include RackWebDAV::HTTPStatus
|
52
|
+
|
53
|
+
# public_path:: Path received via request
|
54
|
+
# path:: Internal resource path (Only different from public path when using root_uri's for webdav)
|
55
|
+
# request:: Rack::Request
|
56
|
+
# options:: Any options provided for this resource
|
57
|
+
# Creates a new instance of the resource.
|
58
|
+
# NOTE: path and public_path will only differ if the root_uri has been set for the resource. The
|
59
|
+
# controller will strip out the starting path so the resource can easily determine what
|
60
|
+
# it is working on. For example:
|
61
|
+
# request -> /my/webdav/directory/actual/path
|
62
|
+
# public_path -> /my/webdav/directory/actual/path
|
63
|
+
# path -> /actual/path
|
64
|
+
# NOTE: Customized Resources should not use initialize for setup. Instead
|
65
|
+
# use the #setup method
|
66
|
+
def initialize(public_path, path, request, response, options)
|
67
|
+
@skip_alias = [
|
68
|
+
:authenticate, :authentication_error_msg,
|
69
|
+
:authentication_realm, :path, :options,
|
70
|
+
:public_path, :request, :response, :user,
|
71
|
+
:user=, :setup
|
72
|
+
]
|
73
|
+
@public_path = public_path.dup
|
74
|
+
@path = path.dup
|
75
|
+
@propstat_relative_path = !!options.delete(:propstat_relative_path)
|
76
|
+
@root_xml_attributes = options.delete(:root_xml_attributes) || {}
|
77
|
+
@request = request
|
78
|
+
@response = response
|
79
|
+
unless(options.has_key?(:lock_class))
|
80
|
+
require 'rack-webdav/lock_store'
|
81
|
+
@lock_class = LockStore
|
82
|
+
else
|
83
|
+
@lock_class = options[:lock_class]
|
84
|
+
raise NameError.new("Unknown lock type constant provided: #{@lock_class}") unless @lock_class.nil? || defined?(@lock_class)
|
85
|
+
end
|
86
|
+
@options = options.dup
|
87
|
+
@max_timeout = options[:max_timeout] || 86400
|
88
|
+
@default_timeout = options[:default_timeout] || 60
|
89
|
+
@user = @options[:user] || request.ip
|
90
|
+
setup if respond_to?(:setup)
|
91
|
+
public_methods(false).each do |method|
|
92
|
+
next if @skip_alias.include?(method.to_sym) || method[0,4] == 'DAV_' || method[0,5] == '_DAV_'
|
93
|
+
self.class.class_eval "alias :'_DAV_#{method}' :'#{method}'"
|
94
|
+
self.class.class_eval "undef :'#{method}'"
|
95
|
+
end
|
96
|
+
@runner = lambda do |class_sym, kind, method_name|
|
97
|
+
[:'__all__', method_name.to_sym].each do |sym|
|
98
|
+
if(@@blocks[class_sym] && @@blocks[class_sym][kind] && @@blocks[class_sym][kind][sym])
|
99
|
+
@@blocks[class_sym][kind][sym].each do |b|
|
100
|
+
args = [self, sym == :'__all__' ? method_name : nil].compact
|
101
|
+
b.call(*args)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# This allows us to call before and after blocks
|
109
|
+
def method_missing(*args)
|
110
|
+
result = nil
|
111
|
+
orig = args.shift
|
112
|
+
class_sym = self.class.name.to_sym
|
113
|
+
m = orig.to_s[0,5] == '_DAV_' ? orig : "_DAV_#{orig}" # If hell is doing the same thing over and over and expecting a different result this is a hell preventer
|
114
|
+
raise NoMethodError.new("Undefined method: #{orig} for class #{self}.") unless respond_to?(m)
|
115
|
+
@runner.call(class_sym, :before, orig)
|
116
|
+
result = send m, *args
|
117
|
+
@runner.call(class_sym, :after, orig)
|
118
|
+
result
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns if resource supports locking
|
122
|
+
def supports_locking?
|
123
|
+
false #true
|
124
|
+
end
|
125
|
+
|
126
|
+
# If this is a collection, return the child resources.
|
127
|
+
def children
|
128
|
+
NotImplemented
|
129
|
+
end
|
130
|
+
|
131
|
+
# Is this resource a collection?
|
132
|
+
def collection?
|
133
|
+
NotImplemented
|
134
|
+
end
|
135
|
+
|
136
|
+
# Does this resource exist?
|
137
|
+
def exist?
|
138
|
+
NotImplemented
|
139
|
+
end
|
140
|
+
|
141
|
+
# Does the parent resource exist?
|
142
|
+
def parent_exists?
|
143
|
+
parent.exist?
|
144
|
+
end
|
145
|
+
|
146
|
+
# Is the parent resource a collection?
|
147
|
+
def parent_collection?
|
148
|
+
parent.collection?
|
149
|
+
end
|
150
|
+
|
151
|
+
# Return the creation time.
|
152
|
+
def creation_date
|
153
|
+
raise NotImplemented
|
154
|
+
end
|
155
|
+
|
156
|
+
# Return the time of last modification.
|
157
|
+
def last_modified
|
158
|
+
raise NotImplemented
|
159
|
+
end
|
160
|
+
|
161
|
+
# Set the time of last modification.
|
162
|
+
def last_modified=(time)
|
163
|
+
# Is this correct?
|
164
|
+
raise NotImplemented
|
165
|
+
end
|
166
|
+
|
167
|
+
# Return an Etag, an unique hash value for this resource.
|
168
|
+
def etag
|
169
|
+
raise NotImplemented
|
170
|
+
end
|
171
|
+
|
172
|
+
# Return the resource type. Generally only used to specify
|
173
|
+
# resource is a collection.
|
174
|
+
def resource_type
|
175
|
+
:collection if collection?
|
176
|
+
end
|
177
|
+
|
178
|
+
# Return the mime type of this resource.
|
179
|
+
def content_type
|
180
|
+
raise NotImplemented
|
181
|
+
end
|
182
|
+
|
183
|
+
# Return the size in bytes for this resource.
|
184
|
+
def content_length
|
185
|
+
raise NotImplemented
|
186
|
+
end
|
187
|
+
|
188
|
+
# HTTP GET request.
|
189
|
+
#
|
190
|
+
# Write the content of the resource to the response.body.
|
191
|
+
def get(request, response)
|
192
|
+
NotImplemented
|
193
|
+
end
|
194
|
+
|
195
|
+
# HTTP PUT request.
|
196
|
+
#
|
197
|
+
# Save the content of the request.body.
|
198
|
+
def put(request, response)
|
199
|
+
NotImplemented
|
200
|
+
end
|
201
|
+
|
202
|
+
# HTTP POST request.
|
203
|
+
#
|
204
|
+
# Usually forbidden.
|
205
|
+
def post(request, response)
|
206
|
+
NotImplemented
|
207
|
+
end
|
208
|
+
|
209
|
+
# HTTP DELETE request.
|
210
|
+
#
|
211
|
+
# Delete this resource.
|
212
|
+
def delete
|
213
|
+
NotImplemented
|
214
|
+
end
|
215
|
+
|
216
|
+
# HTTP COPY request.
|
217
|
+
#
|
218
|
+
# Copy this resource to given destination resource.
|
219
|
+
def copy(dest, overwrite=false)
|
220
|
+
NotImplemented
|
221
|
+
end
|
222
|
+
|
223
|
+
# HTTP MOVE request.
|
224
|
+
#
|
225
|
+
# Move this resource to given destination resource.
|
226
|
+
def move(dest, overwrite=false)
|
227
|
+
NotImplemented
|
228
|
+
end
|
229
|
+
|
230
|
+
# args:: Hash of lock arguments
|
231
|
+
# Request for a lock on the given resource. A valid lock should lock
|
232
|
+
# all descendents. Failures should be noted and returned as an exception
|
233
|
+
# using LockFailure.
|
234
|
+
# Valid args keys: :timeout -> requested timeout
|
235
|
+
# :depth -> lock depth
|
236
|
+
# :scope -> lock scope
|
237
|
+
# :type -> lock type
|
238
|
+
# :owner -> lock owner
|
239
|
+
# Should return a tuple: [lock_time, locktoken] where lock_time is the
|
240
|
+
# given timeout
|
241
|
+
# NOTE: See section 9.10 of RFC 4918 for guidance about
|
242
|
+
# how locks should be generated and the expected responses
|
243
|
+
# (http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10)
|
244
|
+
|
245
|
+
def lock(args)
|
246
|
+
unless(@lock_class)
|
247
|
+
NotImplemented
|
248
|
+
else
|
249
|
+
unless(parent_exists?)
|
250
|
+
Conflict
|
251
|
+
else
|
252
|
+
lock_check(args[:scope])
|
253
|
+
lock = @lock_class.explicit_locks(@path).find{|l| l.scope == args[:scope] && l.kind == args[:type] && l.user == @user}
|
254
|
+
unless(lock)
|
255
|
+
token = UUIDTools::UUID.random_create.to_s
|
256
|
+
lock = @lock_class.generate(@path, @user, token)
|
257
|
+
lock.scope = args[:scope]
|
258
|
+
lock.kind = args[:type]
|
259
|
+
lock.owner = args[:owner]
|
260
|
+
lock.depth = args[:depth].is_a?(Symbol) ? args[:depth] : args[:depth].to_i
|
261
|
+
if(args[:timeout])
|
262
|
+
lock.timeout = args[:timeout] <= @max_timeout && args[:timeout] > 0 ? args[:timeout] : @max_timeout
|
263
|
+
else
|
264
|
+
lock.timeout = @default_timeout
|
265
|
+
end
|
266
|
+
lock.save if lock.respond_to? :save
|
267
|
+
end
|
268
|
+
begin
|
269
|
+
lock_check(args[:type])
|
270
|
+
rescue RackWebDAV::LockFailure => lock_failure
|
271
|
+
lock.destroy
|
272
|
+
raise lock_failure
|
273
|
+
rescue HTTPStatus::Status => status
|
274
|
+
status
|
275
|
+
end
|
276
|
+
[lock.remaining_timeout, lock.token]
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# lock_scope:: scope of lock
|
282
|
+
# Check if resource is locked. Raise RackWebDAV::LockFailure if locks are in place.
|
283
|
+
def lock_check(lock_scope=nil)
|
284
|
+
return unless @lock_class
|
285
|
+
if(@lock_class.explicitly_locked?(@path))
|
286
|
+
raise Locked if @lock_class.explicit_locks(@path).find_all{|l|l.scope == 'exclusive' && l.user != @user}.size > 0
|
287
|
+
elsif(@lock_class.implicitly_locked?(@path))
|
288
|
+
if(lock_scope.to_s == 'exclusive')
|
289
|
+
locks = @lock_class.implicit_locks(@path)
|
290
|
+
failure = RackWebDAV::LockFailure.new("Failed to lock: #{@path}")
|
291
|
+
locks.each do |lock|
|
292
|
+
failure.add_failure(@path, Locked)
|
293
|
+
end
|
294
|
+
raise failure
|
295
|
+
else
|
296
|
+
locks = @lock_class.implict_locks(@path).find_all{|l| l.scope == 'exclusive' && l.user != @user}
|
297
|
+
if(locks.size > 0)
|
298
|
+
failure = LockFailure.new("Failed to lock: #{@path}")
|
299
|
+
locks.each do |lock|
|
300
|
+
failure.add_failure(@path, Locked)
|
301
|
+
end
|
302
|
+
raise failure
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# token:: Lock token
|
309
|
+
# Remove the given lock
|
310
|
+
def unlock(token)
|
311
|
+
unless(@lock_class)
|
312
|
+
NotImplemented
|
313
|
+
else
|
314
|
+
token = token.slice(1, token.length - 2)
|
315
|
+
if(token.nil? || token.empty?)
|
316
|
+
BadRequest
|
317
|
+
else
|
318
|
+
lock = @lock_class.find_by_token(token)
|
319
|
+
if(lock.nil? || lock.user != @user)
|
320
|
+
Forbidden
|
321
|
+
elsif(lock.path !~ /^#{Regexp.escape(@path)}.*$/)
|
322
|
+
Conflict
|
323
|
+
else
|
324
|
+
lock.destroy
|
325
|
+
NoContent
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
|
332
|
+
# Create this resource as collection.
|
333
|
+
def make_collection
|
334
|
+
NotImplemented
|
335
|
+
end
|
336
|
+
|
337
|
+
# other:: Resource
|
338
|
+
# Returns if current resource is equal to other resource
|
339
|
+
def ==(other)
|
340
|
+
path == other.path
|
341
|
+
end
|
342
|
+
|
343
|
+
# Name of the resource
|
344
|
+
def name
|
345
|
+
File.basename(path)
|
346
|
+
end
|
347
|
+
|
348
|
+
# Name of the resource to be displayed to the client
|
349
|
+
def display_name
|
350
|
+
name
|
351
|
+
end
|
352
|
+
|
353
|
+
# Available properties
|
354
|
+
def properties
|
355
|
+
%w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength).collect do |prop|
|
356
|
+
{:name => prop, :ns_href => 'DAV:'}
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
# name:: String - Property name
|
361
|
+
# Returns the value of the given property
|
362
|
+
def get_property(element)
|
363
|
+
raise NotImplemented if (element[:ns_href] != 'DAV:')
|
364
|
+
case element[:name]
|
365
|
+
when 'resourcetype' then resource_type
|
366
|
+
when 'displayname' then display_name
|
367
|
+
when 'creationdate' then use_ms_compat_creationdate? ? creation_date.httpdate : creation_date.xmlschema
|
368
|
+
when 'getcontentlength' then content_length.to_s
|
369
|
+
when 'getcontenttype' then content_type
|
370
|
+
when 'getetag' then etag
|
371
|
+
when 'getlastmodified' then last_modified.httpdate
|
372
|
+
else raise NotImplemented
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
# name:: String - Property name
|
377
|
+
# value:: New value
|
378
|
+
# Set the property to the given value
|
379
|
+
def set_property(element, value)
|
380
|
+
raise NotImplemented if (element[:ns_href] != 'DAV:')
|
381
|
+
case element[:name]
|
382
|
+
when 'resourcetype' then self.resource_type = value
|
383
|
+
when 'getcontenttype' then self.content_type = value
|
384
|
+
when 'getetag' then self.etag = value
|
385
|
+
when 'getlastmodified' then self.last_modified = Time.httpdate(value)
|
386
|
+
else raise NotImplemented
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
# name:: Property name
|
391
|
+
# Remove the property from the resource
|
392
|
+
def remove_property(element)
|
393
|
+
Forbidden
|
394
|
+
end
|
395
|
+
|
396
|
+
# name:: Name of child
|
397
|
+
# Create a new child with the given name
|
398
|
+
# NOTE:: Include trailing '/' if child is collection
|
399
|
+
def child(name)
|
400
|
+
new_public = public_path.dup
|
401
|
+
new_public = new_public + '/' unless new_public[-1,1] == '/'
|
402
|
+
new_public = '/' + new_public unless new_public[0,1] == '/'
|
403
|
+
new_path = path.dup
|
404
|
+
new_path = new_path + '/' unless new_path[-1,1] == '/'
|
405
|
+
new_path = '/' + new_path unless new_path[0,1] == '/'
|
406
|
+
self.class.new("#{new_public}#{name}", "#{new_path}#{name}", request, response, options.merge(:user => @user))
|
407
|
+
end
|
408
|
+
|
409
|
+
# Return parent of this resource
|
410
|
+
def parent
|
411
|
+
unless(@path.to_s.empty?)
|
412
|
+
self.class.new(
|
413
|
+
File.split(@public_path).first,
|
414
|
+
File.split(@path).first,
|
415
|
+
@request,
|
416
|
+
@response,
|
417
|
+
@options.merge(
|
418
|
+
:user => @user
|
419
|
+
)
|
420
|
+
)
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
# Return list of descendants
|
425
|
+
def descendants
|
426
|
+
list = []
|
427
|
+
children.each do |child|
|
428
|
+
list << child
|
429
|
+
list.concat(child.descendants)
|
430
|
+
end
|
431
|
+
list
|
432
|
+
end
|
433
|
+
|
434
|
+
# Index page template for GETs on collection
|
435
|
+
def index_page
|
436
|
+
'<html><head> <title>%s</title>
|
437
|
+
<meta http-equiv="content-type" content="text/html; charset=utf-8" /></head>
|
438
|
+
<body> <h1>%s</h1> <hr /> <table> <tr> <th class="name">Name</th>
|
439
|
+
<th class="size">Size</th> <th class="type">Type</th>
|
440
|
+
<th class="mtime">Last Modified</th> </tr> %s </table> <hr /> </body></html>'
|
441
|
+
end
|
442
|
+
|
443
|
+
# Does client allow GET redirection
|
444
|
+
# TODO: Get a comprehensive list in here.
|
445
|
+
# TODO: Allow this to be dynamic so users can add regexes to match if they know of a client
|
446
|
+
# that can be supported that is not listed.
|
447
|
+
def allows_redirect?
|
448
|
+
[
|
449
|
+
%r{cyberduck}i,
|
450
|
+
%r{konqueror}i
|
451
|
+
].any? do |regexp|
|
452
|
+
(request.respond_to?(:user_agent) ? request.user_agent : request.env['HTTP_USER_AGENT']).to_s =~ regexp
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
def use_compat_mkcol_response?
|
457
|
+
@options[:compat_mkcol] || @options[:compat_all]
|
458
|
+
end
|
459
|
+
|
460
|
+
# Returns true if using an MS client
|
461
|
+
def use_ms_compat_creationdate?
|
462
|
+
if(@options[:compat_ms_mangled_creationdate] || @options[:compat_all])
|
463
|
+
is_ms_client?
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
# Basic user agent testing for MS authored client
|
468
|
+
def is_ms_client?
|
469
|
+
[%r{microsoft-webdav}i, %r{microsoft office}i].any? do |regexp|
|
470
|
+
(request.respond_to?(:user_agent) ? request.user_agent : request.env['HTTP_USER_AGENT']).to_s =~ regexp
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
protected
|
475
|
+
|
476
|
+
# Returns authentication credentials if available in form of [username,password]
|
477
|
+
# TODO: Add support for digest
|
478
|
+
def auth_credentials
|
479
|
+
auth = Rack::Auth::Basic::Request.new(request.env)
|
480
|
+
auth.provided? && auth.basic? ? auth.credentials : [nil,nil]
|
481
|
+
end
|
482
|
+
|
483
|
+
end
|
484
|
+
|
485
|
+
end
|