dav4rack 0.2.11 → 0.3.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.
data/.gitignore DELETED
@@ -1,7 +0,0 @@
1
- .*
2
- !.gitignore
3
- *.gem
4
- *~
5
- doc/*
6
- pkg/*
7
- test/repo
@@ -1,257 +0,0 @@
1
- require 'webrick/httputils'
2
-
3
- module DAV4Rack
4
-
5
- class FileResource < Resource
6
-
7
- include WEBrick::HTTPUtils
8
-
9
- # If this is a collection, return the child resources.
10
- def children
11
- Dir[file_path + '/*'].map do |path|
12
- child File.basename(path)
13
- end
14
- end
15
-
16
- # Is this resource a collection?
17
- def collection?
18
- File.directory?(file_path)
19
- end
20
-
21
- # Does this recource exist?
22
- def exist?
23
- File.exist?(file_path)
24
- end
25
-
26
- # Return the creation time.
27
- def creation_date
28
- stat.ctime
29
- end
30
-
31
- # Return the time of last modification.
32
- def last_modified
33
- stat.mtime
34
- end
35
-
36
- # Set the time of last modification.
37
- def last_modified=(time)
38
- File.utime(Time.now, time, file_path)
39
- end
40
-
41
- # Return an Etag, an unique hash value for this resource.
42
- def etag
43
- sprintf('%x-%x-%x', stat.ino, stat.size, stat.mtime.to_i)
44
- end
45
-
46
- # Return the mime type of this resource.
47
- def content_type
48
- if stat.directory?
49
- "text/html"
50
- else
51
- mime_type(file_path, DefaultMimeTypes)
52
- end
53
- end
54
-
55
- # Return the size in bytes for this resource.
56
- def content_length
57
- stat.size
58
- end
59
-
60
- # HTTP GET request.
61
- #
62
- # Write the content of the resource to the response.body.
63
- def get(request, response)
64
- raise NotFound unless exist?
65
- if stat.directory?
66
- response.body = ""
67
- Rack::Directory.new(root).call(request.env)[2].each do |line|
68
- response.body << line
69
- end
70
- response['Content-Length'] = response.body.bytesize.to_s
71
- else
72
- file = Rack::File.new(root)
73
- response.body = file
74
- end
75
- OK
76
- end
77
-
78
- # HTTP PUT request.
79
- #
80
- # Save the content of the request.body.
81
- def put(request, response)
82
- write(request.body)
83
- Created
84
- end
85
-
86
- # HTTP POST request.
87
- #
88
- # Usually forbidden.
89
- def post(request, response)
90
- raise HTTPStatus::Forbidden
91
- end
92
-
93
- # HTTP DELETE request.
94
- #
95
- # Delete this resource.
96
- def delete
97
- if stat.directory?
98
- FileUtils.rm_rf(file_path)
99
- else
100
- File.unlink(file_path)
101
- end
102
- NoContent
103
- end
104
-
105
- # HTTP COPY request.
106
- #
107
- # Copy this resource to given destination resource.
108
- # Copy this resource to given destination resource.
109
- def copy(dest, overwrite)
110
- if(collection?)
111
- if(dest.exist?)
112
- if(dest.collection? && overwrite)
113
- FileUtils.cp_r(file_path, dest.send(:file_path))
114
- Created
115
- else
116
- if(overwrite)
117
- FileUtils.rm(dest.send(:file_path))
118
- FileUtils.cp_r(file_path, dest.send(:file_path))
119
- NoContent
120
- else
121
- PreconditionFailed
122
- end
123
- end
124
- else
125
- FileUtils.cp_r(file_path, dest.send(:file_path))
126
- Created
127
- end
128
- else
129
- if(dest.exist? && !overwrite)
130
- PreconditionFailed
131
- else
132
- if(File.directory?(File.dirname(dest.send(:file_path))))
133
- new = !dest.exist?
134
- if(dest.collection? && dest.exist?)
135
- FileUtils.rm_rf(dest.send(:file_path))
136
- end
137
- FileUtils.cp(file_path, dest.send(:file_path).sub(/\/$/, ''))
138
- new ? Created : NoContent
139
- else
140
- Conflict
141
- end
142
- end
143
- end
144
- end
145
-
146
- # HTTP MOVE request.
147
- #
148
- # Move this resource to given destination resource.
149
- def move(*args)
150
- result = copy(*args)
151
- delete if [Created, NoContent].include?(result)
152
- result
153
- end
154
-
155
- # HTTP MKCOL request.
156
- #
157
- # Create this resource as collection.
158
- def make_collection
159
- if(request.body.read.to_s == '')
160
- if(File.directory?(file_path))
161
- MethodNotAllowed
162
- else
163
- if(File.directory?(File.dirname(file_path)))
164
- Dir.mkdir(file_path)
165
- Created
166
- else
167
- Conflict
168
- end
169
- end
170
- else
171
- UnsupportedMediaType
172
- end
173
- end
174
-
175
- # Write to this resource from given IO.
176
- def write(io)
177
- tempfile = "#{file_path}.#{Process.pid}.#{object_id}"
178
-
179
- open(tempfile, "wb") do |file|
180
- while part = io.read(8192)
181
- file << part
182
- end
183
- end
184
-
185
- File.rename(tempfile, file_path)
186
- ensure
187
- File.unlink(tempfile) rescue nil
188
- end
189
-
190
- # name:: String - Property name
191
- # Returns the value of the given property
192
- def get_property(name)
193
- super || custom_props(name)
194
- end
195
-
196
- # name:: String - Property name
197
- # value:: New value
198
- # Set the property to the given value
199
- def set_property(name, value)
200
- super || set_custom_props(name,value)
201
- end
202
-
203
- protected
204
-
205
- def set_custom_props(key,val)
206
- prop_hash[key.to_sym] = val
207
- File.open(prop_path, 'w') do |file|
208
- file.write(YAML.dump(prop_hash))
209
- end
210
- end
211
-
212
- def custom_props(key)
213
- prop_hash[key.to_sym]
214
- end
215
-
216
- def prop_path
217
- path = File.join(root, '.props', File.dirname(file_path), File.basename(file_path))
218
- unless(File.directory?(File.dirname(path)))
219
- FileUtils.mkdir_p(File.dirname(path))
220
- end
221
- path
222
- end
223
-
224
- def prop_hash
225
- unless(@_prop_hash)
226
- if(File.exists?(prop_path))
227
- @_prop_hash = YAML.load(File.read(prop_path))
228
- else
229
- @_prop_hash = {}
230
- end
231
- end
232
- @_prop_hash
233
- end
234
-
235
- def authenticate(user, pass)
236
- if(options[:username])
237
- options[:username] == user && options[:password] == pass
238
- else
239
- true
240
- end
241
- end
242
-
243
- def root
244
- @options[:root]
245
- end
246
-
247
- def file_path
248
- File.join(root, path)
249
- end
250
-
251
- def stat
252
- @stat ||= File.stat(file_path)
253
- end
254
-
255
- end
256
-
257
- end
@@ -1,301 +0,0 @@
1
- $:.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
2
-
3
- require 'rubygems'
4
- require 'dav4rack'
5
- require 'fileutils'
6
- require 'nokogiri'
7
- require 'rspec'
8
-
9
- describe DAV4Rack::Handler do
10
- DOC_ROOT = File.expand_path(File.dirname(__FILE__) + '/htdocs')
11
- METHODS = %w(GET PUT POST DELETE PROPFIND PROPPATCH MKCOL COPY MOVE OPTIONS HEAD LOCK UNLOCK)
12
-
13
- before do
14
- FileUtils.mkdir(DOC_ROOT) unless File.exists?(DOC_ROOT)
15
- @controller = DAV4Rack::Handler.new(:root => DOC_ROOT)
16
- end
17
-
18
- after do
19
- FileUtils.rm_rf(DOC_ROOT) if File.exists?(DOC_ROOT)
20
- end
21
-
22
- attr_reader :response
23
-
24
- def request(method, uri, options={})
25
- options = {
26
- 'HTTP_HOST' => 'localhost',
27
- 'REMOTE_USER' => 'user'
28
- }.merge(options)
29
- request = Rack::MockRequest.new(@controller)
30
- @response = request.request(method, uri, options)
31
- end
32
-
33
- METHODS.each do |method|
34
- define_method(method.downcase) do |*args|
35
- request(method, *args)
36
- end
37
- end
38
-
39
- def render(root_type)
40
- raise ArgumentError.new 'Expecting block' unless block_given?
41
- doc = Nokogiri::XML::Builder.new do |xml_base|
42
- xml_base.send(root_type.to_s, 'xmlns:D' => 'D:') do
43
- xml_base.parent.namespace = xml_base.parent.namespace_definitions.first
44
- xml = xml_base['D']
45
- yield xml
46
- end
47
- end
48
- doc.to_xml
49
- end
50
-
51
- def url_escape(string)
52
- URI.escape(string)
53
- end
54
-
55
- def response_xml
56
- Nokogiri.XML(@response.body)
57
- end
58
-
59
- def multistatus_response(pattern)
60
- @response.should be_multi_status
61
- response_xml.xpath('//D:multistatus/D:response', response_xml.root.namespaces).should_not be_empty
62
- response_xml.xpath("//D:multistatus/D:response#{pattern}", response_xml.root.namespaces)
63
- end
64
-
65
- def multi_status_created
66
- response_xml.xpath('//D:multistatus/D:response/D:status').should_not be_empty
67
- response_xml.xpath('//D:multistatus/D:response/D:status').text.should =~ /Created/
68
- end
69
-
70
- def multi_status_ok
71
- response_xml.xpath('//D:multistatus/D:response/D:status').should_not be_empty
72
- response_xml.xpath('//D:multistatus/D:response/D:status').text.should =~ /OK/
73
- end
74
-
75
- def multi_status_no_content
76
- response_xml.xpath('//D:multistatus/D:response/D:status').should_not be_empty
77
- response_xml.xpath('//D:multistatus/D:response/D:status').text.should =~ /No Content/
78
- end
79
-
80
- def propfind_xml(*props)
81
- render(:propfind) do |xml|
82
- xml.prop do
83
- props.each do |prop|
84
- xml.send(prop.to_sym)
85
- end
86
- end
87
- end
88
- end
89
-
90
- it 'should return all options' do
91
- options('/').should be_ok
92
-
93
- METHODS.each do |method|
94
- response.headers['allow'].should include(method)
95
- end
96
- end
97
-
98
- it 'should return headers' do
99
- put('/test.html', :input => '<html/>').should be_created
100
- head('/test.html').should be_ok
101
-
102
- response.headers['etag'].should_not be_nil
103
- response.headers['content-type'].should match(/html/)
104
- response.headers['last-modified'].should_not be_nil
105
- end
106
-
107
- it 'should not find a nonexistent resource' do
108
- get('/not_found').should be_not_found
109
- end
110
-
111
- it 'should not allow directory traversal' do
112
- get('/../htdocs').should be_forbidden
113
- end
114
-
115
- it 'should create a resource and allow its retrieval' do
116
- put('/test', :input => 'body').should be_created
117
- get('/test').should be_ok
118
- response.body.should == 'body'
119
- end
120
-
121
- it 'should return an absolute url after a put request' do
122
- put('/test', :input => 'body').should be_created
123
- response['location'].should =~ /http:\/\/localhost(:\d+)?\/test/
124
- end
125
-
126
- it 'should create and find a url with escaped characters' do
127
- put(url_escape('/a b'), :input => 'body').should be_created
128
- get(url_escape('/a b')).should be_ok
129
- response.body.should == 'body'
130
- end
131
-
132
- it 'should delete a single resource' do
133
- put('/test', :input => 'body').should be_created
134
- delete('/test').should be_no_content
135
- end
136
-
137
- it 'should delete recursively' do
138
- mkcol('/folder').should be_created
139
- put('/folder/a', :input => 'body').should be_created
140
- put('/folder/b', :input => 'body').should be_created
141
-
142
- delete('/folder').should be_no_content
143
- get('/folder').should be_not_found
144
- get('/folder/a').should be_not_found
145
- get('/folder/b').should be_not_found
146
- end
147
-
148
- it 'should not allow copy to another domain' do
149
- put('/test', :input => 'body').should be_created
150
- copy('http://localhost/', 'HTTP_DESTINATION' => 'http://another/').should be_bad_gateway
151
- end
152
-
153
- it 'should not allow copy to the same resource' do
154
- put('/test', :input => 'body').should be_created
155
- copy('/test', 'HTTP_DESTINATION' => '/test').should be_forbidden
156
- end
157
-
158
- it 'should copy a single resource' do
159
- put('/test', :input => 'body').should be_created
160
- copy('/test', 'HTTP_DESTINATION' => '/copy').should be_created
161
- get('/copy').body.should == 'body'
162
- end
163
-
164
- it 'should copy a resource with escaped characters' do
165
- put(url_escape('/a b'), :input => 'body').should be_created
166
- copy(url_escape('/a b'), 'HTTP_DESTINATION' => url_escape('/a c')).should be_created
167
- get(url_escape('/a c')).should be_ok
168
- response.body.should == 'body'
169
- end
170
-
171
- it 'should deny a copy without overwrite' do
172
- put('/test', :input => 'body').should be_created
173
- put('/copy', :input => 'copy').should be_created
174
- copy('/test', 'HTTP_DESTINATION' => '/copy', 'HTTP_OVERWRITE' => 'F').should be_precondition_failed
175
- get('/copy').body.should == 'copy'
176
- end
177
-
178
- it 'should allow a copy with overwrite' do
179
- put('/test', :input => 'body').should be_created
180
- put('/copy', :input => 'copy').should be_created
181
- copy('/test', 'HTTP_DESTINATION' => '/copy', 'HTTP_OVERWRITE' => 'T').should be_no_content
182
- get('/copy').body.should == 'body'
183
- end
184
-
185
- it 'should copy a collection' do
186
- mkcol('/folder').should be_created
187
- copy('/folder', 'HTTP_DESTINATION' => '/copy')
188
- multi_status_created.should eq true
189
- propfind('/copy', :input => propfind_xml(:resourcetype))
190
- multistatus_response('/D:propstat/D:prop/D:resourcetype/D:collection').should_not be_empty
191
- end
192
-
193
- it 'should copy a collection resursively' do
194
- mkcol('/folder').should be_created
195
- put('/folder/a', :input => 'A').should be_created
196
- put('/folder/b', :input => 'B').should be_created
197
-
198
- copy('/folder', 'HTTP_DESTINATION' => '/copy')
199
- multi_status_created.should eq true
200
- propfind('/copy', :input => propfind_xml(:resourcetype))
201
- multistatus_response('/D:propstat/D:prop/D:resourcetype/D:collection').should_not be_empty
202
- get('/copy/a').body.should == 'A'
203
- get('/copy/b').body.should == 'B'
204
- end
205
-
206
- it 'should move a collection recursively' do
207
- mkcol('/folder').should be_created
208
- put('/folder/a', :input => 'A').should be_created
209
- put('/folder/b', :input => 'B').should be_created
210
-
211
- move('/folder', 'HTTP_DESTINATION' => '/move')
212
- multi_status_created.should eq true
213
- propfind('/move', :input => propfind_xml(:resourcetype))
214
- multistatus_response('/D:propstat/D:prop/D:resourcetype/D:collection').should_not be_empty
215
-
216
- get('/move/a').body.should == 'A'
217
- get('/move/b').body.should == 'B'
218
- get('/folder/a').should be_not_found
219
- get('/folder/b').should be_not_found
220
- end
221
-
222
- it 'should create a collection' do
223
- mkcol('/folder').should be_created
224
- propfind('/folder', :input => propfind_xml(:resourcetype))
225
- multistatus_response('/D:propstat/D:prop/D:resourcetype/D:collection').should_not be_empty
226
- end
227
-
228
- it 'should return full urls after creating a collection' do
229
- mkcol('/folder').should be_created
230
- propfind('/folder', :input => propfind_xml(:resourcetype))
231
- multistatus_response('/D:propstat/D:prop/D:resourcetype/D:collection').should_not be_empty
232
- multistatus_response('/D:href').first.text.should =~ /http:\/\/localhost(:\d+)?\/folder/
233
- end
234
-
235
- it 'should not find properties for nonexistent resources' do
236
- propfind('/non').should be_not_found
237
- end
238
-
239
- it 'should find all properties' do
240
- xml = render(:propfind) do |xml|
241
- xml.allprop
242
- end
243
-
244
- propfind('http://localhost/', :input => xml)
245
-
246
- multistatus_response('/D:href').first.text.strip.should =~ /http:\/\/localhost(:\d+)?\//
247
-
248
- props = %w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength)
249
- props.each do |prop|
250
- multistatus_response("/D:propstat/D:prop/D:#{prop}").should_not be_empty
251
- end
252
- end
253
-
254
- it 'should find named properties' do
255
- put('/test.html', :input => '<html/>').should be_created
256
- propfind('/test.html', :input => propfind_xml(:getcontenttype, :getcontentlength))
257
-
258
- multistatus_response('/D:propstat/D:prop/D:getcontenttype').first.text.should == 'text/html'
259
- multistatus_response('/D:propstat/D:prop/D:getcontentlength').first.text.should == '7'
260
- end
261
-
262
- it 'should lock a resource' do
263
- put('/test', :input => 'body').should be_created
264
-
265
- xml = render(:lockinfo) do |xml|
266
- xml.lockscope { xml.exclusive }
267
- xml.locktype { xml.write }
268
- xml.owner { xml.href "http://test.de/" }
269
- end
270
-
271
- lock('/test', :input => xml)
272
-
273
- response.should be_ok
274
-
275
- match = lambda do |pattern|
276
- response_xml.xpath "/D:prop/D:lockdiscovery/D:activelock#{pattern}"
277
- end
278
-
279
- match[''].should_not be_empty
280
-
281
- match['/D:locktype'].should_not be_empty
282
- match['/D:lockscope'].should_not be_empty
283
- match['/D:depth'].should_not be_empty
284
- match['/D:timeout'].should_not be_empty
285
- match['/D:locktoken'].should_not be_empty
286
- match['/D:owner'].should_not be_empty
287
- end
288
-
289
- context "when mapping a path" do
290
-
291
- before do
292
- @controller = DAV4Rack::Handler.new(:root => DOC_ROOT, :root_uri_path => '/webdav/')
293
- end
294
-
295
- it "should return correct urls" do
296
- # FIXME: a put to '/test' works, too -- should it?
297
- put('/webdav/test', :input => 'body').should be_created
298
- response.headers['location'].should =~ /http:\/\/localhost(:\d+)?\/webdav\/test/
299
- end
300
- end
301
- end