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,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
|