rack_dav 0.1.3 → 0.3.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 +2 -0
- data/CHANGELOG.md +27 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +31 -0
- data/README.md +14 -15
- data/Rakefile +36 -0
- data/bin/rack_dav +11 -7
- data/lib/rack_dav.rb +1 -3
- data/lib/rack_dav/controller.rb +300 -218
- data/lib/rack_dav/file_resource.rb +48 -32
- data/lib/rack_dav/handler.rb +19 -12
- data/lib/rack_dav/http_status.rb +10 -10
- data/lib/rack_dav/resource.rb +21 -17
- data/lib/rack_dav/string.rb +28 -0
- data/lib/rack_dav/version.rb +16 -0
- data/rack_dav.gemspec +22 -26
- data/spec/controller_spec.rb +498 -0
- data/spec/file_resource_spec.rb +11 -0
- data/spec/fixtures/folder/01.txt +0 -0
- data/spec/fixtures/folder/02.txt +0 -0
- data/spec/fixtures/requests/lock.xml +12 -0
- data/spec/handler_spec.rb +23 -256
- data/spec/spec_helper.rb +27 -0
- data/spec/support/lockable_file_resource.rb +30 -0
- metadata +107 -50
- data/lib/rack_dav/builder_namespace.rb +0 -22
data/.gitignore
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
= Changelog
|
2
|
+
|
3
|
+
== master
|
4
|
+
|
5
|
+
* NEW: Added Rakefile, package and test tasks.
|
6
|
+
|
7
|
+
* NEW: Added Gemfile and Bundler tasks.
|
8
|
+
|
9
|
+
* CHANGED: Upgraded to RSpec 2.0.
|
10
|
+
|
11
|
+
* CHANGED: Bump dependency to rack >= 1.2.0
|
12
|
+
|
13
|
+
* CHANGED: Removed response.status override/casting in RackDAV::Handler
|
14
|
+
because already performed in Rack::Response#finish.
|
15
|
+
|
16
|
+
* FIXED: rack_dav binary crashes if Mongrel is not installed.
|
17
|
+
|
18
|
+
|
19
|
+
== Release 0.1.3
|
20
|
+
|
21
|
+
* Unknown
|
22
|
+
|
23
|
+
|
24
|
+
== Release 0.1.2
|
25
|
+
|
26
|
+
* Unknown
|
27
|
+
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
rack_dav (0.3.0)
|
5
|
+
nokogiri
|
6
|
+
rack (~> 1.4.0)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
diff-lcs (1.1.3)
|
12
|
+
nokogiri (1.5.5)
|
13
|
+
rack (1.4.1)
|
14
|
+
rake (0.9.2.2)
|
15
|
+
rspec (2.11.0)
|
16
|
+
rspec-core (~> 2.11.0)
|
17
|
+
rspec-expectations (~> 2.11.0)
|
18
|
+
rspec-mocks (~> 2.11.0)
|
19
|
+
rspec-core (2.11.1)
|
20
|
+
rspec-expectations (2.11.2)
|
21
|
+
diff-lcs (~> 1.1.3)
|
22
|
+
rspec-mocks (2.11.1)
|
23
|
+
|
24
|
+
PLATFORMS
|
25
|
+
java
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
rack_dav!
|
30
|
+
rake (~> 0.9.0)
|
31
|
+
rspec (~> 2.11.0)
|
data/README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
---
|
2
|
-
|
2
|
+
RackDAV - Web Authoring for Rack
|
3
3
|
---
|
4
4
|
|
5
5
|
RackDAV is Handler for [Rack][1], which allows content authoring over
|
@@ -8,10 +8,9 @@ possible by subclassing RackDAV::Resource.
|
|
8
8
|
|
9
9
|
## Install
|
10
10
|
|
11
|
-
Just install the gem from
|
11
|
+
Just install the gem from RubyGems:
|
12
12
|
|
13
|
-
$ gem
|
14
|
-
$ sudo gem install georgi-rack_dav
|
13
|
+
$ gem install rack_dav
|
15
14
|
|
16
15
|
## Quickstart
|
17
16
|
|
@@ -28,14 +27,12 @@ to without authentication.
|
|
28
27
|
Using RackDAV inside a rack application is quite easy. A simple rackup
|
29
28
|
script looks like this:
|
30
29
|
|
31
|
-
@@ruby
|
32
|
-
|
33
30
|
require 'rubygems'
|
34
31
|
require 'rack_dav'
|
35
|
-
|
32
|
+
|
36
33
|
use Rack::CommonLogger
|
37
|
-
|
38
|
-
run RackDAV::Handler.new('/path/to/docs')
|
34
|
+
|
35
|
+
run RackDAV::Handler.new(:root => '/path/to/docs')
|
39
36
|
|
40
37
|
## Implementing your own WebDAV resource
|
41
38
|
|
@@ -47,23 +44,21 @@ find the real resource.
|
|
47
44
|
|
48
45
|
RackDAV::Handler needs to be initialized with the actual resource class:
|
49
46
|
|
50
|
-
@@ruby
|
51
|
-
|
52
47
|
RackDAV::Handler.new(:resource_class => MyResource)
|
53
48
|
|
54
49
|
RackDAV needs some information about the resources, so you have to
|
55
50
|
implement following methods:
|
56
|
-
|
51
|
+
|
57
52
|
* __children__: If this is a collection, return the child resources.
|
58
53
|
|
59
54
|
* __collection?__: Is this resource a collection?
|
60
55
|
|
61
56
|
* __exist?__: Does this recource exist?
|
62
|
-
|
57
|
+
|
63
58
|
* __creation\_date__: Return the creation time.
|
64
59
|
|
65
60
|
* __last\_modified__: Return the time of last modification.
|
66
|
-
|
61
|
+
|
67
62
|
* __last\_modified=(time)__: Set the time of last modification.
|
68
63
|
|
69
64
|
* __etag__: Return an Etag, an unique hash value for this resource.
|
@@ -87,9 +82,13 @@ to retrieve and change the resources:
|
|
87
82
|
* __copy(dest)__: Copy this resource to given destination resource.
|
88
83
|
|
89
84
|
* __move(dest)__: Move this resource to given destination resource.
|
90
|
-
|
85
|
+
|
91
86
|
* __make\_collection__: Create this resource as collection.
|
92
87
|
|
88
|
+
* __lock(token, timeout, scope, type, owner)__: Lock this resource.
|
89
|
+
If scope, type and owner are nil, refresh the given lock.
|
90
|
+
|
91
|
+
* __unlock(token)__: Unlock this resource
|
93
92
|
|
94
93
|
Note, that it is generally possible, that a resource object is
|
95
94
|
instantiated for a not yet existing resource.
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
|
4
|
+
Bundler::GemHelper.install_tasks
|
5
|
+
|
6
|
+
|
7
|
+
task :default => :spec
|
8
|
+
|
9
|
+
# Run all the specs in the /spec folder
|
10
|
+
RSpec::Core::RakeTask.new
|
11
|
+
|
12
|
+
|
13
|
+
namespace :spec do
|
14
|
+
desc "Run RSpec against all Ruby versions"
|
15
|
+
task :rubies => "spec:rubies:default"
|
16
|
+
|
17
|
+
namespace :rubies do
|
18
|
+
RUBIES = %w( 1.8.7-p330 1.9.2-p0 jruby-1.5.6 ree-1.8.7-2010.02 )
|
19
|
+
|
20
|
+
task :default => :ensure_rvm do
|
21
|
+
sh "rvm #{RUBIES.join(",")} rake default"
|
22
|
+
end
|
23
|
+
|
24
|
+
task :ensure_rvm do
|
25
|
+
File.exist?(File.expand_path("~/.rvm/scripts/rvm")) || abort("RVM is not available")
|
26
|
+
end
|
27
|
+
|
28
|
+
RUBIES.each do |ruby|
|
29
|
+
desc "Run RSpec against Ruby #{ruby}"
|
30
|
+
task ruby => :ensure_rvm do
|
31
|
+
sh "rvm #{ruby} rake default"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
data/bin/rack_dav
CHANGED
@@ -1,20 +1,24 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
3
|
+
$:.unshift(File.expand_path("../../lib", __FILE__))
|
4
|
+
|
4
5
|
require 'rack_dav'
|
5
6
|
|
6
|
-
|
7
|
+
root=ARGV[1] || Dir.pwd
|
8
|
+
port = ARGV[0] || 3000
|
9
|
+
|
10
|
+
app = Rack::Builder.new do
|
7
11
|
use Rack::ShowExceptions
|
8
12
|
use Rack::CommonLogger
|
9
13
|
use Rack::Reloader
|
10
14
|
use Rack::Lint
|
11
|
-
|
12
|
-
run RackDAV::Handler.new
|
15
|
+
|
16
|
+
run RackDAV::Handler.new(:root => root)
|
13
17
|
|
14
18
|
end.to_app
|
15
19
|
|
16
20
|
begin
|
17
|
-
Rack::Handler::Mongrel.run(app, :Port =>
|
18
|
-
rescue
|
19
|
-
Rack::Handler::WEBrick.run(app, :Port =>
|
21
|
+
Rack::Handler::Mongrel.run(app, :Port => port)
|
22
|
+
rescue LoadError
|
23
|
+
Rack::Handler::WEBrick.run(app, :Port => port)
|
20
24
|
end
|
data/lib/rack_dav.rb
CHANGED
@@ -1,15 +1,13 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
require 'builder'
|
3
2
|
require 'time'
|
4
3
|
require 'uri'
|
5
4
|
require 'rexml/document'
|
5
|
+
require 'nokogiri'
|
6
6
|
require 'webrick/httputils'
|
7
7
|
|
8
8
|
require 'rack'
|
9
|
-
require 'rack_dav/builder_namespace'
|
10
9
|
require 'rack_dav/http_status'
|
11
10
|
require 'rack_dav/resource'
|
12
11
|
require 'rack_dav/file_resource'
|
13
12
|
require 'rack_dav/handler'
|
14
13
|
require 'rack_dav/controller'
|
15
|
-
|
data/lib/rack_dav/controller.rb
CHANGED
@@ -1,49 +1,56 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
require_relative 'string'
|
4
|
+
|
1
5
|
module RackDAV
|
2
|
-
|
6
|
+
|
3
7
|
class Controller
|
4
8
|
include RackDAV::HTTPStatus
|
5
|
-
|
9
|
+
|
6
10
|
attr_reader :request, :response, :resource
|
7
|
-
|
11
|
+
|
8
12
|
def initialize(request, response, options)
|
9
|
-
@request
|
13
|
+
@request = request
|
10
14
|
@response = response
|
11
|
-
@options
|
15
|
+
@options = options
|
12
16
|
@resource = resource_class.new(url_unescape(request.path_info), @options)
|
13
17
|
raise Forbidden if request.path_info.include?('../')
|
14
18
|
end
|
15
|
-
|
19
|
+
|
16
20
|
def url_escape(s)
|
17
|
-
|
18
|
-
'%' + $1.unpack('H2' * $1.size).join('%').upcase
|
19
|
-
end.tr(' ', '+')
|
21
|
+
URI.escape(s)
|
20
22
|
end
|
21
23
|
|
22
24
|
def url_unescape(s)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
-
|
25
|
+
URI.unescape(s).force_valid_encoding
|
26
|
+
end
|
27
|
+
|
28
28
|
def options
|
29
|
-
response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE
|
30
|
-
response["Dav"] = "1
|
29
|
+
response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE'
|
30
|
+
response["Dav"] = "1"
|
31
|
+
|
32
|
+
if resource.lockable?
|
33
|
+
response["Allow"] << ",LOCK,UNLOCK"
|
34
|
+
response["Dav"] << ",2"
|
35
|
+
end
|
36
|
+
|
31
37
|
response["Ms-Author-Via"] = "DAV"
|
32
38
|
end
|
33
|
-
|
39
|
+
|
34
40
|
def head
|
35
41
|
raise NotFound if not resource.exist?
|
36
42
|
response['Etag'] = resource.etag
|
37
43
|
response['Content-Type'] = resource.content_type
|
44
|
+
response['Content-Length'] = resource.content_length.to_s
|
38
45
|
response['Last-Modified'] = resource.last_modified.httpdate
|
39
46
|
end
|
40
|
-
|
47
|
+
|
41
48
|
def get
|
42
49
|
raise NotFound if not resource.exist?
|
43
50
|
response['Etag'] = resource.etag
|
44
51
|
response['Content-Type'] = resource.content_type
|
45
52
|
response['Content-Length'] = resource.content_length.to_s
|
46
|
-
response['Last-Modified'] = resource.last_modified.httpdate
|
53
|
+
response['Last-Modified'] = resource.last_modified.httpdate
|
47
54
|
map_exceptions do
|
48
55
|
resource.get(request, response)
|
49
56
|
end
|
@@ -64,7 +71,7 @@ module RackDAV
|
|
64
71
|
|
65
72
|
def delete
|
66
73
|
delete_recursive(resource, errors = [])
|
67
|
-
|
74
|
+
|
68
75
|
if errors.empty?
|
69
76
|
response.status = NoContent
|
70
77
|
else
|
@@ -73,30 +80,30 @@ module RackDAV
|
|
73
80
|
end
|
74
81
|
end
|
75
82
|
end
|
76
|
-
|
83
|
+
|
77
84
|
def mkcol
|
78
85
|
map_exceptions do
|
79
86
|
resource.make_collection
|
80
87
|
end
|
81
88
|
response.status = Created
|
82
89
|
end
|
83
|
-
|
90
|
+
|
84
91
|
def copy
|
85
92
|
raise NotFound if not resource.exist?
|
86
|
-
|
93
|
+
|
87
94
|
dest_uri = URI.parse(env['HTTP_DESTINATION'])
|
88
95
|
destination = url_unescape(dest_uri.path)
|
89
96
|
|
90
97
|
raise BadGateway if dest_uri.host and dest_uri.host != request.host
|
91
98
|
raise Forbidden if destination == resource.path
|
92
|
-
|
99
|
+
|
93
100
|
dest = resource_class.new(destination, @options)
|
94
101
|
dest = dest.child(resource.name) if dest.collection?
|
95
|
-
|
102
|
+
|
96
103
|
dest_existed = dest.exist?
|
97
|
-
|
104
|
+
|
98
105
|
copy_recursive(resource, dest, depth, errors = [])
|
99
|
-
|
106
|
+
|
100
107
|
if errors.empty?
|
101
108
|
response.status = dest_existed ? NoContent : Created
|
102
109
|
else
|
@@ -113,20 +120,20 @@ module RackDAV
|
|
113
120
|
|
114
121
|
dest_uri = URI.parse(env['HTTP_DESTINATION'])
|
115
122
|
destination = url_unescape(dest_uri.path)
|
116
|
-
|
123
|
+
|
117
124
|
raise BadGateway if dest_uri.host and dest_uri.host != request.host
|
118
125
|
raise Forbidden if destination == resource.path
|
119
|
-
|
126
|
+
|
120
127
|
dest = resource_class.new(destination, @options)
|
121
128
|
dest = dest.child(resource.name) if dest.collection?
|
122
|
-
|
129
|
+
|
123
130
|
dest_existed = dest.exist?
|
124
|
-
|
131
|
+
|
125
132
|
raise Conflict if depth <= 1
|
126
|
-
|
133
|
+
|
127
134
|
copy_recursive(resource, dest, depth, errors = [])
|
128
135
|
delete_recursive(resource, errors)
|
129
|
-
|
136
|
+
|
130
137
|
if errors.empty?
|
131
138
|
response.status = dest_existed ? NoContent : Created
|
132
139
|
else
|
@@ -137,14 +144,14 @@ module RackDAV
|
|
137
144
|
rescue URI::InvalidURIError => e
|
138
145
|
raise BadRequest.new(e.message)
|
139
146
|
end
|
140
|
-
|
147
|
+
|
141
148
|
def propfind
|
142
149
|
raise NotFound if not resource.exist?
|
143
150
|
|
144
|
-
if not request_match("/propfind/allprop").empty?
|
151
|
+
if not request_match("/d:propfind/d:allprop").empty?
|
145
152
|
names = resource.property_names
|
146
153
|
else
|
147
|
-
names = request_match("/propfind/prop
|
154
|
+
names = request_match("/d:propfind/d:prop/d:*").map { |e| e.name }
|
148
155
|
names = resource.property_names if names.empty?
|
149
156
|
raise BadRequest if names.empty?
|
150
157
|
end
|
@@ -153,18 +160,18 @@ module RackDAV
|
|
153
160
|
for resource in find_resources
|
154
161
|
resource.path.gsub!(/\/\//, '/')
|
155
162
|
xml.response do
|
156
|
-
|
163
|
+
xml.href "http://#{host}#{url_escape resource.path}"
|
157
164
|
propstats xml, get_properties(resource, names)
|
158
165
|
end
|
159
166
|
end
|
160
167
|
end
|
161
168
|
end
|
162
|
-
|
169
|
+
|
163
170
|
def proppatch
|
164
171
|
raise NotFound if not resource.exist?
|
165
172
|
|
166
|
-
prop_rem = request_match("/propertyupdate/remove/prop
|
167
|
-
prop_set = request_match("/propertyupdate/set/prop
|
173
|
+
prop_rem = request_match("/d:propertyupdate/d:remove/d:prop/d:*").map { |e| [e.name] }
|
174
|
+
prop_set = request_match("/d:propertyupdate/d:set/d:prop/d:*").map { |e| [e.name, e.text] }
|
168
175
|
|
169
176
|
multistatus do |xml|
|
170
177
|
for resource in find_resources
|
@@ -179,233 +186,308 @@ module RackDAV
|
|
179
186
|
end
|
180
187
|
|
181
188
|
def lock
|
189
|
+
raise MethodNotAllowed unless resource.lockable?
|
182
190
|
raise NotFound if not resource.exist?
|
183
191
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
xml.lockdiscovery do
|
194
|
-
xml.activelock do
|
195
|
-
xml.lockscope { xml.tag! lockscope }
|
196
|
-
xml.locktype { xml.tag! locktype }
|
197
|
-
xml.depth 'Infinity'
|
198
|
-
if owner
|
199
|
-
xml.owner { xml.href owner.text }
|
200
|
-
end
|
201
|
-
xml.timeout "Second-60"
|
202
|
-
xml.locktoken do
|
203
|
-
xml.href locktoken
|
204
|
-
end
|
205
|
-
end
|
206
|
-
end
|
207
|
-
end
|
192
|
+
timeout = request_timeout
|
193
|
+
if timeout.nil? || timeout.zero?
|
194
|
+
timeout = 60
|
195
|
+
end
|
196
|
+
|
197
|
+
if request_document.content.empty?
|
198
|
+
refresh_lock timeout
|
199
|
+
else
|
200
|
+
create_lock timeout
|
208
201
|
end
|
209
202
|
end
|
210
203
|
|
211
204
|
def unlock
|
212
|
-
raise
|
205
|
+
raise MethodNotAllowed unless resource.lockable?
|
206
|
+
|
207
|
+
locktoken = request_locktoken('LOCK_TOKEN')
|
208
|
+
raise BadRequest if locktoken.nil?
|
209
|
+
|
210
|
+
response.status = resource.unlock(locktoken) ? NoContent : Forbidden
|
213
211
|
end
|
214
212
|
|
215
|
-
# ************************************************************
|
216
|
-
# private methods
|
217
|
-
|
218
213
|
private
|
219
214
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
def host
|
225
|
-
env['HTTP_HOST']
|
226
|
-
end
|
227
|
-
|
228
|
-
def resource_class
|
229
|
-
@options[:resource_class]
|
230
|
-
end
|
215
|
+
def env
|
216
|
+
@request.env
|
217
|
+
end
|
231
218
|
|
232
|
-
|
233
|
-
|
234
|
-
when '0' then 0
|
235
|
-
when '1' then 1
|
236
|
-
else 100
|
219
|
+
def host
|
220
|
+
env['HTTP_HOST']
|
237
221
|
end
|
238
|
-
end
|
239
222
|
|
240
|
-
|
241
|
-
|
242
|
-
|
223
|
+
def resource_class
|
224
|
+
@options[:resource_class]
|
225
|
+
end
|
243
226
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
else
|
251
|
-
[resource] + resource.descendants
|
227
|
+
def depth
|
228
|
+
case env['HTTP_DEPTH']
|
229
|
+
when '0' then 0
|
230
|
+
when '1' then 1
|
231
|
+
else 100
|
232
|
+
end
|
252
233
|
end
|
253
|
-
end
|
254
234
|
|
255
|
-
|
256
|
-
|
257
|
-
delete_recursive(child, errors)
|
235
|
+
def overwrite
|
236
|
+
env['HTTP_OVERWRITE'].to_s.upcase != 'F'
|
258
237
|
end
|
259
|
-
|
260
|
-
|
261
|
-
|
238
|
+
|
239
|
+
def find_resources
|
240
|
+
case env['HTTP_DEPTH']
|
241
|
+
when '0'
|
242
|
+
[resource]
|
243
|
+
when '1'
|
244
|
+
[resource] + resource.children
|
245
|
+
else
|
246
|
+
[resource] + resource.descendants
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def delete_recursive(res, errors)
|
251
|
+
for child in res.children
|
252
|
+
delete_recursive(child, errors)
|
253
|
+
end
|
254
|
+
|
255
|
+
begin
|
256
|
+
map_exceptions { res.delete } if errors.empty?
|
257
|
+
rescue Status
|
258
|
+
errors << [res.path, $!]
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def copy_recursive(res, dest, depth, errors)
|
263
|
+
map_exceptions do
|
264
|
+
if dest.exist?
|
265
|
+
if overwrite
|
266
|
+
delete_recursive(dest, errors)
|
267
|
+
else
|
268
|
+
raise PreconditionFailed
|
269
|
+
end
|
270
|
+
end
|
271
|
+
res.copy(dest)
|
272
|
+
end
|
262
273
|
rescue Status
|
263
274
|
errors << [res.path, $!]
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
if dest.exist?
|
270
|
-
if overwrite
|
271
|
-
delete_recursive(dest, errors)
|
272
|
-
else
|
273
|
-
raise PreconditionFailed
|
275
|
+
else
|
276
|
+
if depth > 0
|
277
|
+
for child in res.children
|
278
|
+
dest_child = dest.child(child.name)
|
279
|
+
copy_recursive(child, dest_child, depth - 1, errors)
|
274
280
|
end
|
275
281
|
end
|
276
|
-
res.copy(dest)
|
277
282
|
end
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
283
|
+
|
284
|
+
def map_exceptions
|
285
|
+
yield
|
286
|
+
rescue
|
287
|
+
case $!
|
288
|
+
when URI::InvalidURIError then raise BadRequest
|
289
|
+
when Errno::EACCES then raise Forbidden
|
290
|
+
when Errno::ENOENT then raise Conflict
|
291
|
+
when Errno::EEXIST then raise Conflict
|
292
|
+
when Errno::ENOSPC then raise InsufficientStorage
|
293
|
+
else
|
294
|
+
raise
|
285
295
|
end
|
286
296
|
end
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
case $!
|
293
|
-
when URI::InvalidURIError then raise BadRequest
|
294
|
-
when Errno::EACCES then raise Forbidden
|
295
|
-
when Errno::ENOENT then raise Conflict
|
296
|
-
when Errno::EEXIST then raise Conflict
|
297
|
-
when Errno::ENOSPC then raise InsufficientStorage
|
298
|
-
else
|
299
|
-
raise
|
297
|
+
|
298
|
+
def request_document
|
299
|
+
@request_document ||= Nokogiri::XML(request.body.read) {|config| config.strict }
|
300
|
+
rescue Nokogiri::XML::SyntaxError
|
301
|
+
raise BadRequest
|
300
302
|
end
|
301
|
-
end
|
302
|
-
|
303
|
-
def request_document
|
304
|
-
@request_document ||= REXML::Document.new(request.body.read)
|
305
|
-
rescue REXML::ParseException
|
306
|
-
raise BadRequest
|
307
|
-
end
|
308
303
|
|
309
|
-
|
310
|
-
|
311
|
-
|
304
|
+
def request_match(pattern)
|
305
|
+
request_document.xpath(pattern, 'd' => 'DAV:')
|
306
|
+
end
|
312
307
|
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
308
|
+
# Quick and dirty parsing of the WEBDAV Timeout header.
|
309
|
+
# Refuses infinity, rejects anything but Second- timeouts
|
310
|
+
#
|
311
|
+
# @return [nil] or [Fixnum]
|
312
|
+
#
|
313
|
+
# @api internal
|
314
|
+
#
|
315
|
+
def request_timeout
|
316
|
+
timeout = request.env['HTTP_TIMEOUT']
|
317
|
+
return if timeout.nil? || timeout.empty?
|
318
|
+
|
319
|
+
timeout = timeout.split /,\s*/
|
320
|
+
timeout.reject! {|t| t !~ /^Second-/}
|
321
|
+
timeout.first.sub('Second-', '').to_i
|
322
|
+
end
|
323
|
+
|
324
|
+
def request_locktoken(header)
|
325
|
+
token = request.env["HTTP_#{header}"]
|
326
|
+
return if token.nil? || token.empty?
|
327
|
+
token.scan /^\(?<?(.+?)>?\)?$/
|
328
|
+
return $1
|
329
|
+
end
|
330
|
+
|
331
|
+
# Creates a new XML document, yields given block
|
332
|
+
# and sets the response.body with the final XML content.
|
333
|
+
# The response length is updated accordingly.
|
334
|
+
#
|
335
|
+
# @return [void]
|
336
|
+
#
|
337
|
+
# @yield [xml] Yields the Builder XML instance.
|
338
|
+
#
|
339
|
+
# @api internal
|
340
|
+
#
|
341
|
+
def render_xml
|
342
|
+
content = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |xml|
|
329
343
|
yield xml
|
344
|
+
end.to_xml
|
345
|
+
response.body = [content]
|
346
|
+
response["Content-Type"] = 'text/xml; charset=utf-8'
|
347
|
+
response["Content-Length"] = Rack::Utils.bytesize(content).to_s
|
348
|
+
end
|
349
|
+
|
350
|
+
def multistatus
|
351
|
+
render_xml do |xml|
|
352
|
+
xml.multistatus('xmlns' => "DAV:") do
|
353
|
+
yield xml
|
354
|
+
end
|
330
355
|
end
|
356
|
+
|
357
|
+
response.status = MultiStatus
|
331
358
|
end
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
xml.href "http://#{host}#{path}"
|
340
|
-
xml.status "#{request.env['HTTP_VERSION']} #{status.status_line}"
|
359
|
+
|
360
|
+
def response_errors(xml, errors)
|
361
|
+
for path, status in errors
|
362
|
+
xml.response do
|
363
|
+
xml.href "http://#{host}#{path}"
|
364
|
+
xml.status "#{request.env['HTTP_VERSION']} #{status.status_line}"
|
365
|
+
end
|
341
366
|
end
|
342
367
|
end
|
343
|
-
end
|
344
368
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
369
|
+
def get_properties(resource, names)
|
370
|
+
stats = Hash.new { |h, k| h[k] = [] }
|
371
|
+
for name in names
|
372
|
+
begin
|
373
|
+
map_exceptions do
|
374
|
+
stats[OK] << [name, resource.get_property(name)]
|
375
|
+
end
|
376
|
+
rescue Status
|
377
|
+
stats[$!] << name
|
351
378
|
end
|
352
|
-
rescue Status
|
353
|
-
stats[$!] << name
|
354
379
|
end
|
380
|
+
stats
|
355
381
|
end
|
356
|
-
stats
|
357
|
-
end
|
358
382
|
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
383
|
+
def set_properties(resource, pairs)
|
384
|
+
stats = Hash.new { |h, k| h[k] = [] }
|
385
|
+
for name, value in pairs
|
386
|
+
begin
|
387
|
+
map_exceptions do
|
388
|
+
stats[OK] << [name, resource.set_property(name, value)]
|
389
|
+
end
|
390
|
+
rescue Status
|
391
|
+
stats[$!] << name
|
365
392
|
end
|
366
|
-
rescue Status
|
367
|
-
stats[$!] << name
|
368
393
|
end
|
394
|
+
stats
|
369
395
|
end
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
396
|
+
|
397
|
+
def propstats(xml, stats)
|
398
|
+
return if stats.empty?
|
399
|
+
for status, props in stats
|
400
|
+
xml.propstat do
|
401
|
+
xml.prop do
|
402
|
+
for name, value in props
|
403
|
+
if value.is_a?(Nokogiri::XML::Node)
|
404
|
+
xml.send(name) do
|
405
|
+
rexml_convert(xml, value)
|
406
|
+
end
|
407
|
+
else
|
408
|
+
xml.send(name, value)
|
382
409
|
end
|
383
|
-
else
|
384
|
-
xml.tag!(name, value)
|
385
410
|
end
|
386
411
|
end
|
412
|
+
xml.status "#{request.env['HTTP_VERSION']} #{status.status_line}"
|
387
413
|
end
|
388
|
-
xml.status "#{request.env['HTTP_VERSION']} #{status.status_line}"
|
389
414
|
end
|
390
415
|
end
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
416
|
+
|
417
|
+
def create_lock(timeout)
|
418
|
+
lockscope = request_match("/d:lockinfo/d:lockscope/d:*").first
|
419
|
+
lockscope = lockscope.name if lockscope
|
420
|
+
locktype = request_match("/d:lockinfo/d:locktype/d:*").first
|
421
|
+
locktype = locktype.name if locktype
|
422
|
+
owner = request_match("/d:lockinfo/d:owner/d:href").first
|
423
|
+
owner = owner.text if owner
|
424
|
+
locktoken = "opaquelocktoken:" + sprintf('%x-%x-%s', Time.now.to_i, Time.now.sec, resource.etag)
|
425
|
+
|
426
|
+
# Quick & Dirty - FIXME: Lock should become a new Class
|
427
|
+
# and this dirty parameter passing refactored.
|
428
|
+
unless resource.lock(locktoken, timeout, lockscope, locktype, owner)
|
429
|
+
raise Forbidden
|
399
430
|
end
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
431
|
+
|
432
|
+
response['Lock-Token'] = locktoken
|
433
|
+
|
434
|
+
render_lockdiscovery locktoken, lockscope, locktype, timeout, owner
|
435
|
+
end
|
436
|
+
|
437
|
+
def refresh_lock(timeout)
|
438
|
+
locktoken = request_locktoken('IF')
|
439
|
+
raise BadRequest if locktoken.nil?
|
440
|
+
|
441
|
+
timeout, lockscope, locktype, owner = resource.lock(locktoken, timeout)
|
442
|
+
unless lockscope && locktype && timeout
|
443
|
+
raise Forbidden
|
444
|
+
end
|
445
|
+
|
446
|
+
render_lockdiscovery locktoken, lockscope, locktype, timeout, owner
|
447
|
+
end
|
448
|
+
|
449
|
+
# FIXME add multiple locks support
|
450
|
+
def render_lockdiscovery(locktoken, lockscope, locktype, timeout, owner)
|
451
|
+
render_xml do |xml|
|
452
|
+
xml.prop('xmlns' => "DAV:") do
|
453
|
+
xml.lockdiscovery do
|
454
|
+
render_lock(xml, locktoken, lockscope, locktype, timeout, owner)
|
455
|
+
end
|
404
456
|
end
|
405
457
|
end
|
406
458
|
end
|
407
|
-
|
408
|
-
|
459
|
+
|
460
|
+
def render_lock(xml, locktoken, lockscope, locktype, timeout, owner)
|
461
|
+
xml.activelock do
|
462
|
+
xml.lockscope { xml.tag! lockscope }
|
463
|
+
xml.locktype { xml.tag! locktype }
|
464
|
+
xml.depth 'Infinity'
|
465
|
+
if owner
|
466
|
+
xml.owner { xml.href owner }
|
467
|
+
end
|
468
|
+
xml.timeout "Second-#{timeout}"
|
469
|
+
xml.locktoken do
|
470
|
+
xml.href locktoken
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
def rexml_convert(xml, element)
|
476
|
+
if element.elements.empty?
|
477
|
+
if element.text
|
478
|
+
xml.send(element.name.to_sym, element.text, element.attributes)
|
479
|
+
else
|
480
|
+
xml.send(element.name.to_sym, element.attributes)
|
481
|
+
end
|
482
|
+
else
|
483
|
+
xml.send(element.name.to_sym, element.attributes) do
|
484
|
+
element.elements.each do |child|
|
485
|
+
rexml_convert(xml, child)
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
409
491
|
end
|
410
492
|
|
411
|
-
end
|
493
|
+
end
|