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
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
Current DAV4Rack license:
|
2
|
+
|
3
|
+
Copyright (c) 2010 Chris Roberts <chrisroberts.code@gmail.com>
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to
|
7
|
+
deal in the Software without restriction, including without limitation the
|
8
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
9
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
18
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
|
23
|
+
Original rack_dav source license:
|
24
|
+
|
25
|
+
Copyright (c) 2009 Matthias Georgi <http://www.matthias-georgi.de>
|
26
|
+
|
27
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
28
|
+
of this software and associated documentation files (the "Software"), to
|
29
|
+
deal in the Software without restriction, including without limitation the
|
30
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
31
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
32
|
+
furnished to do so, subject to the following conditions:
|
33
|
+
|
34
|
+
The above copyright notice and this permission notice shall be included in
|
35
|
+
all copies or substantial portions of the Software.
|
36
|
+
|
37
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
38
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
39
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
40
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
41
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
42
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
== DAV4Rack - Web Authoring for Rack
|
2
|
+
|
3
|
+
DAV4Rack is a framework for providing WebDAV via Rack allowing content
|
4
|
+
authoring over HTTP. It is based off the {original RackDAV framework}[http://github.com/georgi/rack_dav]
|
5
|
+
to provide better Resource support for building customized WebDAV resources
|
6
|
+
completely abstracted from the filesystem. Support for authentication and
|
7
|
+
locking has been added as well as a move from REXML to Nokogiri. Enjoy!
|
8
|
+
|
9
|
+
== Install
|
10
|
+
|
11
|
+
=== Via RubyGems
|
12
|
+
|
13
|
+
gem install dav4rack
|
14
|
+
|
15
|
+
== Quickstart
|
16
|
+
|
17
|
+
If you just want to share a folder over WebDAV, you can just start a
|
18
|
+
simple server with:
|
19
|
+
|
20
|
+
dav4rack
|
21
|
+
|
22
|
+
This will start a WEBrick server on port 3000, which you can connect
|
23
|
+
to without authentication. The simple file resource allows very basic authentication
|
24
|
+
which is used for an example. To enable it:
|
25
|
+
|
26
|
+
dav4rack --username=user --password=pass
|
27
|
+
|
28
|
+
== Rack Handler
|
29
|
+
|
30
|
+
Using DAV4Rack within a rack application is pretty simple. A very slim
|
31
|
+
rackup script would look something like this:
|
32
|
+
|
33
|
+
require 'rubygems'
|
34
|
+
require 'dav4rack'
|
35
|
+
|
36
|
+
use Rack::CommonLogger
|
37
|
+
run DAV4Rack::Handler.new(:root => '/path/to/public/fileshare')
|
38
|
+
|
39
|
+
This will use the included FileResource and set the share path. However,
|
40
|
+
DAV4Rack has some nifty little extras that can be enabled in the rackup script. First,
|
41
|
+
an example of how to use a custom resource:
|
42
|
+
|
43
|
+
run DAV4Rack::Handler.new(:resource_class => CustomResource, :custom => 'options', :passed => 'to resource')
|
44
|
+
|
45
|
+
Next, lets venture into mapping a path for our WebDAV access. In this example, we
|
46
|
+
will use default FileResource like in the first example, but instead of connecting
|
47
|
+
directly to the host, we will have to connect to: http://host/webdav/share/
|
48
|
+
|
49
|
+
require 'rubygems'
|
50
|
+
require 'dav4rack'
|
51
|
+
|
52
|
+
use Rack::CommonLogger
|
53
|
+
|
54
|
+
app = Rack::Builder.new{
|
55
|
+
map '/webdav/share/' do
|
56
|
+
run DAV4Rack::Handler.new(:root => '/path/to/public/fileshare', :root_uri_path => '/webdav/share/')
|
57
|
+
end
|
58
|
+
}.to_app
|
59
|
+
run app
|
60
|
+
|
61
|
+
Aside from the #map block, notice the new option passed to the Handler's initialization, :root_uri_path. When
|
62
|
+
DAV4Rack receives a request, it will automatically convert the request to the proper path and pass it to
|
63
|
+
the resource.
|
64
|
+
|
65
|
+
Another tool available when building the rackup script is the Interceptor. The Interceptor's job is to simply
|
66
|
+
intecept WebDAV requests received up the path hierarchy where no resources are currently mapped. For example,
|
67
|
+
lets continue with the last example but this time include the interceptor:
|
68
|
+
|
69
|
+
require 'rubygems'
|
70
|
+
require 'dav4rack'
|
71
|
+
|
72
|
+
use Rack::CommonLogger
|
73
|
+
app = Rack::Builder.new{
|
74
|
+
map '/webdav/share/' do
|
75
|
+
run DAV4Rack::Handler.new(:root => '/path/to/public/fileshare', :root_uri_path => '/webdav/share/')
|
76
|
+
end
|
77
|
+
map '/webdav/share2/' do
|
78
|
+
run DAV4Rack::Handler.new(:resource_class => CustomResource, :root_uri_path => '/webdav/share2/')
|
79
|
+
end
|
80
|
+
map '/' do
|
81
|
+
use DAV4Rack::Interceptor, :mappings => {
|
82
|
+
'/webdav/share/' => {:resource_class => FileResource, :options => {:custom => 'option'}},
|
83
|
+
'/webdav/share2/' => {:resource_class => CustomResource}
|
84
|
+
}
|
85
|
+
use Rails::Rack::Static
|
86
|
+
run ActionController::Dispatcher.new
|
87
|
+
end
|
88
|
+
}.to_app
|
89
|
+
run app
|
90
|
+
|
91
|
+
In this example we have two WebDAV resources restricted by path. This means those resources will handle requests to /webdav/share/*
|
92
|
+
and /webdav/share2/* but nothing above that. To allow webdav to respond, we provide the Interceptor. The Interceptor does not
|
93
|
+
provide any authentication support. It simply creates a virtual file system view to the provided mapped paths. Once the actual
|
94
|
+
resources have been reached, authentication will be enforced based on the requirements defined by the individual resource. Also
|
95
|
+
note in the root map you can see we are running a Rails application. This is how you can easily enable DAV4Rack with your Rails
|
96
|
+
application.
|
97
|
+
|
98
|
+
== Custom Resources
|
99
|
+
|
100
|
+
Creating your own resource is easy. Simply inherit the DAV4Rack::Resource class, and start redefining all the methods
|
101
|
+
you want to customize. The DAV4Rack::Resource class only has implementations for methods that can be provided extremely
|
102
|
+
generically. This means that most things will require at least some sort of implementation. However, because the Resource
|
103
|
+
is defined so generically, and the Controller simply passes the request on to the Resource, it is easy to create fully
|
104
|
+
virtualized resources.
|
105
|
+
|
106
|
+
== Helpers
|
107
|
+
|
108
|
+
There are some helpers worth mentioning that make things a little easier. DAV4Rack::Resource#accept_redirect? method is available to Resources.
|
109
|
+
If true, the currently connected client will accept and properly use a 302 redirect for a GET request. Most clients do not properly
|
110
|
+
support this, which can be a real pain when working with virtualized files that may located some where else, like S3. To deal with
|
111
|
+
those clients that don't support redirects, a helper has been provided so resources don't have to deal with proxying themselves. The
|
112
|
+
DAV4Rack::RemoteFile allows the resource to simple tell Rack to download and send the file from the provided resource and go away, allowing the
|
113
|
+
process to be freed up to deal with other waiters. A very simple example:
|
114
|
+
|
115
|
+
class MyResource < DAV4Rack::Resource
|
116
|
+
def initialize(*args)
|
117
|
+
super(*args)
|
118
|
+
@item = method_to_fill_this_properly
|
119
|
+
end
|
120
|
+
|
121
|
+
def get
|
122
|
+
if(accept_redirect?)
|
123
|
+
response.redirect item[:url]
|
124
|
+
else
|
125
|
+
response.body = DAV4Rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type)
|
126
|
+
OK
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
== Issues/Bugs/Questions
|
132
|
+
|
133
|
+
Please use the issues at github: http://github.com/chrisroberts/dav4rack
|
134
|
+
|
135
|
+
== License
|
136
|
+
|
137
|
+
Just like RackDAV before it, this software is distributed under the MIT license.
|
data/bin/dav4rack
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'dav4rack'
|
5
|
+
require 'getoptlong'
|
6
|
+
|
7
|
+
def print_help_msg
|
8
|
+
print_version_info
|
9
|
+
puts "Usage: dav4rack [opts]"
|
10
|
+
puts " --help Print help message"
|
11
|
+
puts " --version Print version information"
|
12
|
+
puts " --username name Set username"
|
13
|
+
puts " --password pass Set password"
|
14
|
+
puts " --root /share/path Set path to share directory"
|
15
|
+
end
|
16
|
+
|
17
|
+
def print_version_info
|
18
|
+
puts "DAV 4 Rack - Rack based WebDAV Framework - Version: #{DAV4Rack::VERSION}"
|
19
|
+
end
|
20
|
+
|
21
|
+
opts = GetoptLong.new(
|
22
|
+
['--username', '-u', GetoptLong::REQUIRED_ARGUMENT],
|
23
|
+
['--password', '-p', GetoptLong::REQUIRED_ARGUMENT],
|
24
|
+
['--help', '-h', GetoptLong::NO_ARGUMENT],
|
25
|
+
['--version', '-v', GetoptLong::NO_ARGUMENT],
|
26
|
+
['--root', '-r', GetoptLong::REQUIRED_ARGUMENT]
|
27
|
+
)
|
28
|
+
|
29
|
+
credentials = {}
|
30
|
+
|
31
|
+
opts.each do |opt,arg|
|
32
|
+
case opt
|
33
|
+
when '--help'
|
34
|
+
print_help_msg
|
35
|
+
exit(0)
|
36
|
+
when '--username'
|
37
|
+
credentials[:username] = arg
|
38
|
+
when '--password'
|
39
|
+
credentials[:password] = arg
|
40
|
+
when '--root'
|
41
|
+
credentials[:root] = arg
|
42
|
+
unless(File.exists?(arg) && File.directory?(arg))
|
43
|
+
puts "ERROR: Path provided is not a valid directory (#{arg})"
|
44
|
+
exit(-1)
|
45
|
+
end
|
46
|
+
when '--version'
|
47
|
+
print_version_info
|
48
|
+
exit(0)
|
49
|
+
else
|
50
|
+
puts "ERROR: Unknown option provided"
|
51
|
+
exit(-1)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
app = Rack::Builder.new do
|
56
|
+
use Rack::ShowExceptions
|
57
|
+
use Rack::CommonLogger
|
58
|
+
use Rack::Reloader
|
59
|
+
use Rack::Lint
|
60
|
+
|
61
|
+
run DAV4Rack::Handler.new(credentials)
|
62
|
+
|
63
|
+
end.to_app
|
64
|
+
|
65
|
+
Rack::Handler::WEBrick.run(app, :Port => 3000)
|
data/dav4rack.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'dav4rack'
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.name = 'dav4rack'
|
4
|
+
s.version = DAV4Rack::VERSION
|
5
|
+
s.summary = 'WebDAV handler for Rack'
|
6
|
+
s.author = 'Chris Roberts'
|
7
|
+
s.email = 'chris@chrisroberts.org'
|
8
|
+
s.homepage = 'http://github.com/dav4rack'
|
9
|
+
s.description = 'WebDAV handler for Rack'
|
10
|
+
s.require_path = 'lib'
|
11
|
+
s.executables << 'dav4rack'
|
12
|
+
s.has_rdoc = true
|
13
|
+
s.extra_rdoc_files = ['README.rdoc']
|
14
|
+
s.files = %w{
|
15
|
+
.gitignore
|
16
|
+
LICENSE
|
17
|
+
dav4rack.gemspec
|
18
|
+
lib/dav4rack.rb
|
19
|
+
lib/dav4rack/file_resource.rb
|
20
|
+
lib/dav4rack/handler.rb
|
21
|
+
lib/dav4rack/controller.rb
|
22
|
+
lib/dav4rack/http_status.rb
|
23
|
+
lib/dav4rack/resource.rb
|
24
|
+
lib/dav4rack/interceptor.rb
|
25
|
+
lib/dav4rack/interceptor_resource.rb
|
26
|
+
lib/dav4rack/remote_file.rb
|
27
|
+
bin/dav4rack
|
28
|
+
spec/handler_spec.rb
|
29
|
+
README.rdoc
|
30
|
+
}
|
31
|
+
end
|
data/lib/dav4rack.rb
ADDED
@@ -0,0 +1,457 @@
|
|
1
|
+
module DAV4Rack
|
2
|
+
|
3
|
+
class Controller
|
4
|
+
include DAV4Rack::HTTPStatus
|
5
|
+
|
6
|
+
attr_reader :request, :response, :resource
|
7
|
+
|
8
|
+
# request:: Rack::Request
|
9
|
+
# response:: Rack::Response
|
10
|
+
# options:: Options hash
|
11
|
+
# Create a new Controller.
|
12
|
+
# NOTE: options will be passed to Resource
|
13
|
+
def initialize(request, response, options={})
|
14
|
+
@request = request
|
15
|
+
@response = response
|
16
|
+
@options = options
|
17
|
+
@resource = resource_class.new(actual_path, implied_path, @request, @options)
|
18
|
+
authenticate
|
19
|
+
raise Forbidden if request.path_info.include?('../')
|
20
|
+
end
|
21
|
+
|
22
|
+
# s:: string
|
23
|
+
# Escape URL string
|
24
|
+
def url_escape(s)
|
25
|
+
s.gsub(/([^\/a-zA-Z0-9_.-]+)/n) do
|
26
|
+
'%' + $1.unpack('H2' * $1.size).join('%').upcase
|
27
|
+
end.tr(' ', '+')
|
28
|
+
end
|
29
|
+
|
30
|
+
# s:: string
|
31
|
+
# Unescape URL string
|
32
|
+
def url_unescape(s)
|
33
|
+
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) do
|
34
|
+
[$1.delete('%')].pack('H*')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return response to OPTIONS
|
39
|
+
def options
|
40
|
+
response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
|
41
|
+
response["Dav"] = "2"
|
42
|
+
response["Ms-Author-Via"] = "DAV"
|
43
|
+
NoContent
|
44
|
+
end
|
45
|
+
|
46
|
+
# Return response to HEAD
|
47
|
+
def head
|
48
|
+
raise NotFound unless resource.exist?
|
49
|
+
response['Etag'] = resource.etag
|
50
|
+
response['Content-Type'] = resource.content_type
|
51
|
+
response['Last-Modified'] = resource.last_modified.httpdate
|
52
|
+
NoContent
|
53
|
+
end
|
54
|
+
|
55
|
+
# Return response to GET
|
56
|
+
def get
|
57
|
+
raise NotFound unless resource.exist?
|
58
|
+
res = resource.get(request, response)
|
59
|
+
if(response.status == 200 && !resource.collection?)
|
60
|
+
response['Etag'] = resource.etag
|
61
|
+
response['Content-Type'] = resource.content_type
|
62
|
+
response['Content-Length'] = resource.content_length.to_s
|
63
|
+
response['Last-Modified'] = resource.last_modified.httpdate
|
64
|
+
end
|
65
|
+
res
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return response to PUT
|
69
|
+
def put
|
70
|
+
raise Forbidden if resource.collection?
|
71
|
+
resource.put(request, response)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Return response to POST
|
75
|
+
def post
|
76
|
+
resource.post(request, response)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return response to DELETE
|
80
|
+
def delete
|
81
|
+
raise NotFound unless resource.exist?
|
82
|
+
resource.delete
|
83
|
+
end
|
84
|
+
|
85
|
+
# Return response to MKCOL
|
86
|
+
def mkcol
|
87
|
+
resource.make_collection
|
88
|
+
end
|
89
|
+
|
90
|
+
# Return response to COPY
|
91
|
+
def copy
|
92
|
+
move(:copy)
|
93
|
+
end
|
94
|
+
|
95
|
+
# args:: Only argument used: :copy
|
96
|
+
# Move Resource to new location. If :copy is provided,
|
97
|
+
# Resource will be copied (implementation ease)
|
98
|
+
def move(*args)
|
99
|
+
raise NotFound unless resource.exist?
|
100
|
+
dest_uri = URI.parse(env['HTTP_DESTINATION'])
|
101
|
+
destination = url_unescape(dest_uri.path)
|
102
|
+
raise BadGateway if dest_uri.host and dest_uri.host != request.host
|
103
|
+
raise Forbidden if destination == resource.public_path
|
104
|
+
dest = resource_class.new(destination, clean_path(destination), @request, @options)
|
105
|
+
if(args.include?(:copy))
|
106
|
+
resource.copy(dest, overwrite)
|
107
|
+
else
|
108
|
+
raise Conflict unless depth.is_a?(Symbol) || depth > 1
|
109
|
+
resource.move(dest)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Return respoonse to PROPFIND
|
114
|
+
def propfind
|
115
|
+
raise NotFound unless resource.exist?
|
116
|
+
unless(request_document.xpath("//#{ns}propfind/#{ns}allprop").empty?)
|
117
|
+
names = resource.property_names
|
118
|
+
else
|
119
|
+
names = request_document.xpath("//#{ns}propfind/#{ns}prop").children.find_all{|n|n.element?}.map{|n|n.name}
|
120
|
+
names = resource.property_names if names.empty?
|
121
|
+
end
|
122
|
+
multistatus do |xml|
|
123
|
+
find_resources.each do |resource|
|
124
|
+
xml.response do
|
125
|
+
xml.href "#{scheme}://#{host}:#{port}#{url_escape(resource.public_path)}"
|
126
|
+
propstats(xml, get_properties(resource, names))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Return response to PROPPATCH
|
133
|
+
def proppatch
|
134
|
+
raise NotFound unless resource.exist?
|
135
|
+
prop_rem = request_match('/propertyupdate/remove/prop').children.map{|n| [n.name] }
|
136
|
+
prop_set = request_match('/propertyupdate/set/prop').children.map{|n| [n.name, n.text] }
|
137
|
+
multistatus do |xml|
|
138
|
+
find_resources.each do |resource|
|
139
|
+
xml.response do
|
140
|
+
xml.href "#{scheme}://#{host}:#{port}#{url_escape(resource.public_path)}"
|
141
|
+
propstats(xml, set_properties(resource, prop_set))
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
# Lock current resource
|
149
|
+
# NOTE: This will pass an argument hash to Resource#lock and
|
150
|
+
# wait for a success/failure response.
|
151
|
+
def lock
|
152
|
+
raise NotFound unless resource.exist?
|
153
|
+
lockinfo = request_document.xpath("//#{ns}lockinfo")
|
154
|
+
asked = {}
|
155
|
+
asked[:timeout] = request.env['Timeout'].split(',').map{|x|x.strip} if request.env['Timeout']
|
156
|
+
asked[:depth] = depth
|
157
|
+
raise BadRequest unless [0, :infinity].include?(asked[:depth])
|
158
|
+
asked[:scope] = lockinfo.xpath("//#{ns}lockscope").children.find_all{|n|n.element?}.map{|n|n.name}.first
|
159
|
+
asked[:type] = lockinfo.xpath("#{ns}locktype").children.find_all{|n|n.element?}.map{|n|n.name}.first
|
160
|
+
asked[:owner] = lockinfo.xpath("//#{ns}owner/#{ns}href").children.map{|n|n.text}.first
|
161
|
+
begin
|
162
|
+
lock_time, locktoken = resource.lock(asked)
|
163
|
+
render_xml(:prop) do |xml|
|
164
|
+
xml.lockdiscovery do
|
165
|
+
xml.activelock do
|
166
|
+
if(asked[:scope])
|
167
|
+
xml.lockscope do
|
168
|
+
xml.send(asked[:scope])
|
169
|
+
end
|
170
|
+
end
|
171
|
+
if(asked[:type])
|
172
|
+
xml.locktype do
|
173
|
+
xml.send(asked[:type])
|
174
|
+
end
|
175
|
+
end
|
176
|
+
xml.depth asked[:depth].to_s
|
177
|
+
xml.timeout lock_time ? "Second-#{lock_time}" : 'infinity'
|
178
|
+
xml.locktoken do
|
179
|
+
xml.href locktoken
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
response.status = resource.exist? ? OK : Created
|
185
|
+
rescue LockFailure => e
|
186
|
+
multistatus do |xml|
|
187
|
+
e.path_status.each_pair do |path, status|
|
188
|
+
xml.response do
|
189
|
+
xml.href path
|
190
|
+
xml.status "#{http_version} #{status.status_line}"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
response.status = MultiStatus
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Unlock current resource
|
199
|
+
def unlock
|
200
|
+
resource.unlock(lock_token)
|
201
|
+
end
|
202
|
+
|
203
|
+
# ************************************************************
|
204
|
+
# private methods
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
# Request environment variables
|
209
|
+
def env
|
210
|
+
@request.env
|
211
|
+
end
|
212
|
+
|
213
|
+
# Current request scheme (http/https)
|
214
|
+
def scheme
|
215
|
+
request.scheme
|
216
|
+
end
|
217
|
+
|
218
|
+
# Request host
|
219
|
+
def host
|
220
|
+
request.host
|
221
|
+
end
|
222
|
+
|
223
|
+
# Request port
|
224
|
+
def port
|
225
|
+
request.port
|
226
|
+
end
|
227
|
+
|
228
|
+
# Class of the resource in use
|
229
|
+
def resource_class
|
230
|
+
@options[:resource_class]
|
231
|
+
end
|
232
|
+
|
233
|
+
# Root URI path for the resource
|
234
|
+
def root_uri_path
|
235
|
+
@options[:root_uri_path]
|
236
|
+
end
|
237
|
+
|
238
|
+
# Returns Resource path with root URI removed
|
239
|
+
def implied_path
|
240
|
+
clean_path(@request.path.dup)
|
241
|
+
end
|
242
|
+
|
243
|
+
# x:: request path
|
244
|
+
# Unescapes path and removes root URI if applicable
|
245
|
+
def clean_path(x)
|
246
|
+
ip = url_unescape(x)
|
247
|
+
ip.gsub!(/^#{Regexp.escape(root_uri_path)}/, '') if root_uri_path
|
248
|
+
ip
|
249
|
+
end
|
250
|
+
|
251
|
+
# Unescaped request path
|
252
|
+
def actual_path
|
253
|
+
url_unescape(@request.path.dup)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Lock token if provided by client
|
257
|
+
def lock_token
|
258
|
+
env['HTTP_LOCK_TOKEN'] || nil
|
259
|
+
end
|
260
|
+
|
261
|
+
# Requested depth
|
262
|
+
def depth
|
263
|
+
d = env['HTTP_DEPTH']
|
264
|
+
if(d =~ /^\d+$/)
|
265
|
+
d = d.to_i
|
266
|
+
else
|
267
|
+
d = :infinity
|
268
|
+
end
|
269
|
+
d
|
270
|
+
end
|
271
|
+
|
272
|
+
# Current HTTP version being used
|
273
|
+
def http_version
|
274
|
+
env['HTTP_VERSION'] || env['SERVER_PROTOCOL'] || 'HTTP/1.0'
|
275
|
+
end
|
276
|
+
|
277
|
+
# Overwrite is allowed
|
278
|
+
def overwrite
|
279
|
+
env['HTTP_OVERWRITE'].to_s.upcase != 'F'
|
280
|
+
end
|
281
|
+
|
282
|
+
# Find resources at depth requested
|
283
|
+
def find_resources
|
284
|
+
ary = nil
|
285
|
+
case depth
|
286
|
+
when 0
|
287
|
+
ary = [resource]
|
288
|
+
when 1
|
289
|
+
ary = resource.children
|
290
|
+
else
|
291
|
+
ary = resource.descendants
|
292
|
+
end
|
293
|
+
ary ? ary : []
|
294
|
+
end
|
295
|
+
|
296
|
+
# XML parsed request
|
297
|
+
def request_document
|
298
|
+
@request_document ||= Nokogiri.XML(request.body.read)
|
299
|
+
rescue
|
300
|
+
raise BadRequest
|
301
|
+
end
|
302
|
+
|
303
|
+
# Namespace being used within XML document
|
304
|
+
# TODO: Make this better
|
305
|
+
def ns
|
306
|
+
_ns = ''
|
307
|
+
if(request_document && request_document.root && request_document.root.namespace_definitions.size > 0)
|
308
|
+
_ns = request_document.root.namespace_definitions.first.prefix.to_s
|
309
|
+
_ns += ':' unless _ns.empty?
|
310
|
+
end
|
311
|
+
_ns
|
312
|
+
end
|
313
|
+
|
314
|
+
# pattern:: XPath pattern
|
315
|
+
# Search XML document for given XPath
|
316
|
+
def request_match(pattern)
|
317
|
+
nil unless request_document
|
318
|
+
request_document.xpath(pattern, request_document.root.namespaces)
|
319
|
+
end
|
320
|
+
|
321
|
+
# root_type:: Root tag name
|
322
|
+
# Render XML and set Rack::Response#body= to final XML
|
323
|
+
def render_xml(root_type)
|
324
|
+
raise ArgumentError.new 'Expecting block' unless block_given?
|
325
|
+
doc = Nokogiri::XML::Builder.new do |xml|
|
326
|
+
xml.send(root_type.to_s, 'xmlns' => 'DAV:') do
|
327
|
+
# xml.parent.namespace = xml.parent.namespace_definitions.first
|
328
|
+
yield xml
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
response.body = doc.to_xml
|
333
|
+
response["Content-Type"] = 'text/xml; charset="utf-8"'
|
334
|
+
response["Content-Length"] = response.body.size.to_s
|
335
|
+
end
|
336
|
+
|
337
|
+
# block:: block
|
338
|
+
# Creates a multistatus response using #render_xml and
|
339
|
+
# returns the correct status
|
340
|
+
def multistatus(&block)
|
341
|
+
render_xml(:multistatus, &block)
|
342
|
+
MultiStatus
|
343
|
+
end
|
344
|
+
|
345
|
+
# xml:: Nokogiri::XML::Builder
|
346
|
+
# errors:: Array of errors
|
347
|
+
# Crafts responses for errors
|
348
|
+
def response_errors(xml, errors)
|
349
|
+
for path, status in errors
|
350
|
+
xml.response do
|
351
|
+
xml.href "#{scheme}://#{host}:#{port}#{path}"
|
352
|
+
xml.status "#{http_version} #{status.status_line}"
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
# resource:: Resource
|
358
|
+
# names:: Property names
|
359
|
+
# Returns array of property values for given names
|
360
|
+
def get_properties(resource, names)
|
361
|
+
stats = Hash.new { |h, k| h[k] = [] }
|
362
|
+
for name in names
|
363
|
+
begin
|
364
|
+
val = resource.get_property(name)
|
365
|
+
stats[OK].push [name, val] if val
|
366
|
+
rescue Status
|
367
|
+
stats[$!] << name
|
368
|
+
end
|
369
|
+
end
|
370
|
+
stats
|
371
|
+
end
|
372
|
+
|
373
|
+
# resource:: Resource
|
374
|
+
# pairs:: name value pairs
|
375
|
+
# Sets the given properties
|
376
|
+
def set_properties(resource, pairs)
|
377
|
+
stats = Hash.new { |h, k| h[k] = [] }
|
378
|
+
for name, value in pairs
|
379
|
+
begin
|
380
|
+
stats[OK] << [name, resource.set_property(name, value)]
|
381
|
+
rescue Status
|
382
|
+
stats[$!] << name
|
383
|
+
end
|
384
|
+
end
|
385
|
+
stats
|
386
|
+
end
|
387
|
+
|
388
|
+
# xml:: Nokogiri::XML::Builder
|
389
|
+
# stats:: Array of stats
|
390
|
+
# Build propstats response
|
391
|
+
def propstats(xml, stats)
|
392
|
+
return if stats.empty?
|
393
|
+
for status, props in stats
|
394
|
+
xml.propstat do
|
395
|
+
xml.prop do
|
396
|
+
for name, value in props
|
397
|
+
if(value.is_a?(Nokogiri::XML::Node))
|
398
|
+
xml.send(name) do
|
399
|
+
xml_convert(xml, value)
|
400
|
+
end
|
401
|
+
elsif(value.is_a?(Symbol))
|
402
|
+
xml.send(name) do
|
403
|
+
xml.send(value)
|
404
|
+
end
|
405
|
+
else
|
406
|
+
xml.send(name, value)
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
xml.status "#{http_version} #{status.status_line}"
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# xml:: Nokogiri::XML::Builder
|
416
|
+
# element:: Nokogiri::XML::Element
|
417
|
+
# Converts element into proper text
|
418
|
+
def xml_convert(xml, element)
|
419
|
+
if element.children.empty?
|
420
|
+
if element.text?
|
421
|
+
xml.send(element.name, element.text, element.attributes)
|
422
|
+
else
|
423
|
+
xml.send(element.name, element.attributes)
|
424
|
+
end
|
425
|
+
else
|
426
|
+
xml.send(element.name, element.attributes) do
|
427
|
+
element.elements.each do |child|
|
428
|
+
xml_convert(xml, child)
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
# Perform authentication
|
435
|
+
# NOTE: Authentication will only be performed if the Resource
|
436
|
+
# has defined an #authenticate method
|
437
|
+
def authenticate
|
438
|
+
authed = true
|
439
|
+
if(resource.respond_to?(:authenticate))
|
440
|
+
authed = false
|
441
|
+
if(request.env['HTTP_AUTHORIZATION'])
|
442
|
+
auth = Rack::Auth::Basic::Request.new(request.env)
|
443
|
+
if(auth.basic? && auth.credentials)
|
444
|
+
authed = resource.authenticate(auth.credentials[0], auth.credentials[1])
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
unless(authed)
|
449
|
+
response.body = resource.respond_to?(:authentication_error_msg) ? resource.authentication_error_msg : 'Not Authorized'
|
450
|
+
response['WWW-Authenticate'] = "Basic realm=\"#{resource.respond_to?(:authentication_realm) ? resource.authentication_realm : 'Locked content'}\""
|
451
|
+
raise Unauthorized.new unless authed
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
end
|
456
|
+
|
457
|
+
end
|