webdav.rb 0.2.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.
- checksums.yaml +7 -0
- data/CHANGELOG +67 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +305 -0
- data/Rakefile +9 -0
- data/lib/Net/HTTP/Report.rb +16 -0
- data/lib/String/to_const.rb +7 -0
- data/lib/Thoran/Array/AllButFirst/all_but_first.rb +28 -0
- data/lib/Thoran/Array/FirstX/firstX.rb +32 -0
- data/lib/Thoran/String/ToConst/to_const.rb +47 -0
- data/lib/WebDAV/Error.rb +17 -0
- data/lib/WebDAV/MultiStatus.rb +56 -0
- data/lib/WebDAV/Response.rb +29 -0
- data/lib/WebDAV/VERSION.rb +6 -0
- data/lib/webdav.rb +157 -0
- data/test/Net/HTTP/Report_test.rb +21 -0
- data/test/WebDAV/Error_test.rb +85 -0
- data/test/WebDAV/MultiStatus_test.rb +231 -0
- data/test/WebDAV/Response_test.rb +72 -0
- data/test/helper.rb +13 -0
- data/test/webdav_test.rb +254 -0
- data/webdav.rb.gemspec +49 -0
- metadata +131 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 11276a2f0788ae73a2174746bc48900ef91355d946f0af27b7bd36b00674e8b8
|
|
4
|
+
data.tar.gz: '08912af75301ee512c4d9c08b52f836e94f055e255762890a5ea03b604cd379c'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3997386c5837b0070a49b5a01901e3cc54e2bca7860668375a863753b2b7a12b7ccebac405d327b4349de6a27b3c1b5256eb480b072b3c6ed5bc58ff7d5435c8
|
|
7
|
+
data.tar.gz: 6a74951f911f9ba0d8e9463f4a17da9bb0348e761728a23fa3e9f93d5afb9935ba4923b1f3cf5407193dcadc04385159b3328bbcea213e56d83ad6b35d33cb22
|
data/CHANGELOG
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 20260821
|
|
4
|
+
|
|
5
|
+
0.2.1: + webdav.rb.gemspec, so that the gem resolves under both names.
|
|
6
|
+
|
|
7
|
+
1. + webdav.rb.gemspec: the same code published as webdav.rb, caldav being published as caldav.rb. rubygems has no notion of an alias, so the two are separate gems which must be kept at the same version by hand.
|
|
8
|
+
2. ~ webdav.gemspec: + a comment naming the second gemspec, this being the gem which already exists on rubygems.
|
|
9
|
+
3. gem-publish 0.2.0 is what makes this work: gem build and gem push each take one file, so the earlier `gem build *.gemspec` built whichever the shell listed first and webdav.rb was never built at all.
|
|
10
|
+
4. ~ WebDAV::VERSION: /0.2.0/0.2.1/
|
|
11
|
+
|
|
12
|
+
## 20260522
|
|
13
|
+
|
|
14
|
+
0.2.0: Rewrite MultiStatus#parse; align dependencies with http.rb 1.0.0; document WebDAV concepts.
|
|
15
|
+
|
|
16
|
+
1. ~ WebDAV::MultiStatus#parse: Walk propstat blocks individually; preserve per-propstat status; detect response-level status (for COPY/MOVE/DELETE); preserve namespace URIs on properties. Now private; resources remains the public accessor.
|
|
17
|
+
2. ~ WebDAV::MultiStatus resource shape: Properties move from flat hash keyed by local name to nested hash keyed by namespace URI then local name. Resources now have :propstats (array of {properties:, status:}) and :status (response-level, may be nil) instead of a single :properties and :status pair.
|
|
18
|
+
3. ~ test/WebDAV/MultiStatus_test.rb: Update existing tests for new shape; + tests for per-propstat status, response-level status, namespace preservation, and same-local-name collision across namespaces.
|
|
19
|
+
4. ~ README.md: Document new MultiStatus#resources shape, with paired wire-XML and parsed-Ruby examples for both PROPFIND and collection-cascade Multi-Status; + WebDAV::Response wire example; + Concepts section (properties vs content, collections and the trailing slash, why 207 Multi-Status exists, namespaces).
|
|
20
|
+
5. ~ webdav.gemspec: required_ruby_version />= 2.7/>= 3.2/; http.rb dependency /[]/['~> 1.0']/; minitest pin /'~> 5.27'/'~> 6.0'/; + minitest-mock (minitest 6 split it out).
|
|
21
|
+
6. ~ WebDAV::VERSION: /0.1.2/0.2.0/
|
|
22
|
+
7. ~ CHANGELOG: + 0.2.0 entry
|
|
23
|
+
|
|
24
|
+
## 20260520
|
|
25
|
+
|
|
26
|
+
0.1.2: Fix Response#success? to return true only for 2xx responses.
|
|
27
|
+
|
|
28
|
+
1. ~ WebDAV::Response#success?: Was `code < 400` which incorrectly reported 1xx and 3xx as successful. Now `code >= 200 && code < 300`.
|
|
29
|
+
2. ~ test/WebDAV/Response_test.rb: + tests for 3xx and 5xx behaviour.
|
|
30
|
+
3. ~ webdav.gemspec: Remove http.rb version constraint (was `>= 0.18.0`; the original rationale, Net::HTTP::Report being in http.rb 0.18.0, was reverted in webdav 0.1.1 and http.rb 0.18.1).
|
|
31
|
+
4. ~ webdav.gemspec: + rexml runtime dependency (default gem on Ruby 3+; required by WebDAV::MultiStatus).
|
|
32
|
+
5. ~ webdav.gemspec: Pin minitest development dependency to `~> 5.27` (Minitest 6 split `minitest/mock` into a separate gem).
|
|
33
|
+
6. ~ WebDAV::VERSION: /0.1.1/0.1.2/
|
|
34
|
+
7. ~ CHANGELOG: + 0.1.2 entry
|
|
35
|
+
|
|
36
|
+
## 20260508
|
|
37
|
+
|
|
38
|
+
0.1.1: Reinstate Net::HTTP::Report.
|
|
39
|
+
|
|
40
|
+
1. + lib/Net/HTTP/Report.rb: Reinstated after checking after it was incorrectly removed in 0.1.0. REPORT is from RFC 3253 (WebDAV versioning) and is not a core HTTP method.
|
|
41
|
+
2. ~ lib/webdav.rb: + require_relative './Net/HTTP/Report'
|
|
42
|
+
3. + test/Net/HTTP/Report_test.rb
|
|
43
|
+
4. ~ WebDAV::VERSION: /0.1.0/0.1.1/
|
|
44
|
+
5. ~ CHANGELOG: + 0.1.1
|
|
45
|
+
|
|
46
|
+
## 20260508
|
|
47
|
+
|
|
48
|
+
0.1.0: Remove Net::HTTP::Report; now provided by http.rb 0.18.0.
|
|
49
|
+
|
|
50
|
+
1. - lib/Net/HTTP/Report.rb: Now provided by http.rb 0.18.0.
|
|
51
|
+
2. ~ lib/webdav.rb: - require_relative './Net/HTTP/Report'
|
|
52
|
+
3. ~ webdav.gemspec: Pin http.rb dependency to >= 0.18.0; + development dependencies; + CHANGELOG and Rakefile to files; reformat.
|
|
53
|
+
4. ~ .gitignore: Previously empty. Now with a default and comprehensive set of defaults.
|
|
54
|
+
5. ~ WebDAV::VERSION: /0.0.0/0.1.0/
|
|
55
|
+
6. ~ lib/webdav.rb: - require 'rexml/document'
|
|
56
|
+
7. ~ lib/WebDAV/MultiStatus.rb: + require 'rexml/document'
|
|
57
|
+
|
|
58
|
+
## 20260325
|
|
59
|
+
|
|
60
|
+
0.0.0: Initial release.
|
|
61
|
+
|
|
62
|
+
1. + WebDAV client with 16 verbs
|
|
63
|
+
a. 8 WebDAV-specific verbs: copy, lock, mkcol, move, propfind, proppatch, report, unlock.
|
|
64
|
+
b. 8 standard HTTP verbs: delete, get, head, options, patch, post, put, and trace.
|
|
65
|
+
2. + WebDAV::Response response object
|
|
66
|
+
3. + WebDAV::MultiStatus response object
|
|
67
|
+
4. + WebDAV::Error for 4xx/5xx responses
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 thoran
|
|
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 deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
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 all
|
|
13
|
+
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 THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# webdav
|
|
2
|
+
|
|
3
|
+
A WebDAV client library for Ruby.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
gem install webdav
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Or in your Gemfile:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem 'webdav'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require 'webdav'
|
|
23
|
+
|
|
24
|
+
dav = WebDAV.new('https://dav.example.com/files/', username: 'user', password: 'pass')
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Discovering resources
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
response = dav.propfind('/', depth: '1')
|
|
31
|
+
response.resources.each do |resource|
|
|
32
|
+
puts resource[:href]
|
|
33
|
+
resource[:propstats].each do |propstat|
|
|
34
|
+
puts propstat[:status]
|
|
35
|
+
propstat[:properties].each do |namespace, properties|
|
|
36
|
+
properties.each do |name, value|
|
|
37
|
+
puts " #{namespace} #{name} = #{value}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Reading a resource
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
response = dav.get('/documents/report.txt')
|
|
48
|
+
puts response.body
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Writing a resource
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
dav.put('/documents/report.txt', body: 'Hello, world.', content_type: 'text/plain')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Deleting a resource
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
dav.delete('/documents/report.txt')
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Creating a collection
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
dav.mkcol('/documents/archive/')
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Copying and moving
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
dav.copy('/documents/report.txt', to: '/archive/report.txt')
|
|
73
|
+
dav.move('/documents/draft.txt', to: '/documents/final.txt')
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Locking and unlocking
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
lock_body = <<~XML
|
|
80
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
81
|
+
<d:lockinfo xmlns:d="DAV:">
|
|
82
|
+
<d:lockscope><d:exclusive/></d:lockscope>
|
|
83
|
+
<d:locktype><d:write/></d:locktype>
|
|
84
|
+
</d:lockinfo>
|
|
85
|
+
XML
|
|
86
|
+
|
|
87
|
+
response = dav.lock('/documents/report.txt', body: lock_body)
|
|
88
|
+
# ...
|
|
89
|
+
dav.unlock('/documents/report.txt', token: 'urn:uuid:...')
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Reporting
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
response = dav.report('/calendars/user/', body: report_xml, depth: '1')
|
|
96
|
+
response.resources.each do |resource|
|
|
97
|
+
puts resource[:href]
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Concepts
|
|
103
|
+
|
|
104
|
+
WebDAV extends HTTP with a few ideas that don't have direct REST analogues. The ones below explain why the API is shaped the way it is.
|
|
105
|
+
|
|
106
|
+
### Properties vs content
|
|
107
|
+
|
|
108
|
+
A WebDAV resource has two faces. **Content** is what GET returns — the bytes of the file. **Properties** are metadata associated with the resource: display name, creation date, lock state, content type, and any custom properties the server defines. The same URL identifies both, but different verbs reach them — GET/PUT for content, PROPFIND/PROPPATCH for properties.
|
|
109
|
+
|
|
110
|
+
### Collections and the trailing slash
|
|
111
|
+
|
|
112
|
+
A **collection** is WebDAV's directory: a resource that contains other resources. MKCOL creates one. By convention, collection URLs end in `/` and ordinary resources don't; servers that care about the distinction will redirect or 404 if you get it wrong. The distinction matters because COPY, MOVE, and DELETE on a collection cascade to its children — which is also why those verbs can return 207 Multi-Status when children succeed and fail independently.
|
|
113
|
+
|
|
114
|
+
### Why 207 Multi-Status exists
|
|
115
|
+
|
|
116
|
+
HTTP assumes one request maps to one status. WebDAV breaks that assumption: PROPFIND on a folder asks about many resources at once; COPY of a tree may succeed on some children and fail on others. The 207 Multi-Status response code says "the request touched many things; here are per-thing outcomes." The XML body carries one `<d:response>` per affected resource. This gem returns those as `WebDAV::MultiStatus`; the `resources` accessor exposes the per-resource detail (see [Responses](#responses)).
|
|
117
|
+
|
|
118
|
+
### Namespaces
|
|
119
|
+
|
|
120
|
+
WebDAV properties are XML elements, and XML elements belong to namespaces. The core RFC 4918 properties live in the `DAV:` namespace. Extensions — CalDAV (`urn:ietf:params:xml:ns:caldav`), CardDAV, Exchange, ownCloud, custom server vocabularies — each define their own. Properties from different namespaces can share local names (`<d:displayname>` and `<x:displayname>` are different properties), so the parser preserves namespace URIs as the outer key in the properties hash.
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## Methods
|
|
124
|
+
|
|
125
|
+
WebDAV extends HTTP with additional methods for distributed authoring. This gem provides all the methods defined in RFC 4918 ("HTTP Extensions for Web Distributed Authoring and Versioning") and the REPORT method from RFC 3253 ("Versioning Extensions to WebDAV"), which is essential for CalDAV and CardDAV queries.
|
|
126
|
+
|
|
127
|
+
Ruby's standard library includes request classes for the RFC 4918 methods (Propfind, Proppatch, Mkcol, Copy, Move, Lock, Unlock) but not for REPORT. This gem defines `Net::HTTP::Report` to fill that gap.
|
|
128
|
+
|
|
129
|
+
These methods are not provided by the `http.rb` gem, which deliberately limits itself to the core HTTP methods defined in RFC 9110 ("HTTP Semantics") and RFC 5789 ("PATCH Method for HTTP").
|
|
130
|
+
|
|
131
|
+
### Properties (RFC 4918)
|
|
132
|
+
|
|
133
|
+
- `propfind(path, body:, depth:)` — retrieve properties from a resource
|
|
134
|
+
- `proppatch(path, body:)` — set or remove properties on a resource
|
|
135
|
+
|
|
136
|
+
### Versioning (RFC 3253)
|
|
137
|
+
|
|
138
|
+
- `report(path, body:, depth:)` — query for information about a resource; used by CalDAV and CardDAV
|
|
139
|
+
|
|
140
|
+
### Collections (RFC 4918)
|
|
141
|
+
|
|
142
|
+
- `mkcol(path)` — create a new collection (directory)
|
|
143
|
+
|
|
144
|
+
### Namespace (RFC 4918)
|
|
145
|
+
|
|
146
|
+
- `copy(path, to:, depth:, overwrite:)` — copy a resource
|
|
147
|
+
- `move(path, to:, overwrite:)` — move a resource
|
|
148
|
+
|
|
149
|
+
### Locking (RFC 4918)
|
|
150
|
+
|
|
151
|
+
- `lock(path, body:)` — lock a resource
|
|
152
|
+
- `unlock(path, token:)` — unlock a resource
|
|
153
|
+
|
|
154
|
+
### Standard HTTP
|
|
155
|
+
|
|
156
|
+
- `get(path)`
|
|
157
|
+
- `head(path)`
|
|
158
|
+
- `post(path, body:, content_type:)`
|
|
159
|
+
- `put(path, body:, content_type:)`
|
|
160
|
+
- `patch(path, body:, content_type:)`
|
|
161
|
+
- `delete(path)`
|
|
162
|
+
- `options(path)`
|
|
163
|
+
- `trace(path)`
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
## Responses
|
|
167
|
+
|
|
168
|
+
All methods return either a `WebDAV::Response` or a `WebDAV::MultiStatus`.
|
|
169
|
+
|
|
170
|
+
`WebDAV::Response` provides
|
|
171
|
+
|
|
172
|
+
- `code`
|
|
173
|
+
- `message`
|
|
174
|
+
- `headers`
|
|
175
|
+
- `body`
|
|
176
|
+
- `etag`
|
|
177
|
+
- `content_type`
|
|
178
|
+
- `success?`
|
|
179
|
+
|
|
180
|
+
A `GET` response on the wire:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
HTTP/1.1 200 OK
|
|
184
|
+
Content-Type: text/plain
|
|
185
|
+
Content-Length: 13
|
|
186
|
+
ETag: "5d41402a"
|
|
187
|
+
|
|
188
|
+
Hello, world.
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Parses to:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
response.code # => 200
|
|
195
|
+
response.message # => "OK"
|
|
196
|
+
response.body # => "Hello, world."
|
|
197
|
+
response.etag # => "\"5d41402a\""
|
|
198
|
+
response.content_type # => "text/plain"
|
|
199
|
+
response.success? # => true
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`WebDAV::MultiStatus` additionally provides:
|
|
203
|
+
|
|
204
|
+
- `resources` — an array of hashes, each with:
|
|
205
|
+
- `href` — the resource URL
|
|
206
|
+
- `propstats` — an array of `{properties:, status:}` hashes (PROPFIND / PROPPATCH / REPORT). May be empty when the response carries a response-level status instead.
|
|
207
|
+
- `status` — the response-level status string (COPY / MOVE / DELETE). `nil` when the response has propstats instead.
|
|
208
|
+
|
|
209
|
+
Within a propstat, `properties` is a nested hash keyed first by XML namespace URI, then by local name. For example, a CalDAV calendar property appears as `propstat[:properties]['urn:ietf:params:xml:ns:caldav']['calendar-data']` and a DAV property as `propstat[:properties]['DAV:']['getetag']`. Keeping the namespace explicit prevents collisions between properties from different namespaces that share a local name.
|
|
210
|
+
|
|
211
|
+
A PROPFIND response — properties grouped by namespace, status per propstat, response-level status `nil`. The wire XML:
|
|
212
|
+
|
|
213
|
+
```xml
|
|
214
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
215
|
+
<d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
|
216
|
+
<d:response>
|
|
217
|
+
<d:href>/calendar/event.ics</d:href>
|
|
218
|
+
<d:propstat>
|
|
219
|
+
<d:prop>
|
|
220
|
+
<d:getetag>"abc123"</d:getetag>
|
|
221
|
+
<c:calendar-data>BEGIN:VCALENDAR...</c:calendar-data>
|
|
222
|
+
</d:prop>
|
|
223
|
+
<d:status>HTTP/1.1 200 OK</d:status>
|
|
224
|
+
</d:propstat>
|
|
225
|
+
<d:propstat>
|
|
226
|
+
<d:prop>
|
|
227
|
+
<d:getctag/>
|
|
228
|
+
</d:prop>
|
|
229
|
+
<d:status>HTTP/1.1 404 Not Found</d:status>
|
|
230
|
+
</d:propstat>
|
|
231
|
+
</d:response>
|
|
232
|
+
</d:multistatus>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Parses to:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
[
|
|
239
|
+
{
|
|
240
|
+
href: '/calendar/event.ics',
|
|
241
|
+
propstats: [
|
|
242
|
+
{
|
|
243
|
+
properties: {
|
|
244
|
+
'DAV:' => {'getetag' => '"abc123"'},
|
|
245
|
+
'urn:ietf:params:xml:ns:caldav' => {'calendar-data' => 'BEGIN:VCALENDAR...'}
|
|
246
|
+
},
|
|
247
|
+
status: 'HTTP/1.1 200 OK'
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
properties: {'DAV:' => {'getctag' => ''}},
|
|
251
|
+
status: 'HTTP/1.1 404 Not Found'
|
|
252
|
+
}
|
|
253
|
+
],
|
|
254
|
+
status: nil
|
|
255
|
+
}
|
|
256
|
+
]
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
A COPY / MOVE / DELETE on a collection where a child resource failed — the server returns 207 Multi-Status with one `<d:response>` per affected child, each carrying a response-level status rather than propstats. Single-resource lifecycle operations don't go through this path; they return a plain `WebDAV::Response` with the status code as the whole story. The wire XML:
|
|
260
|
+
|
|
261
|
+
```xml
|
|
262
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
263
|
+
<d:multistatus xmlns:d="DAV:">
|
|
264
|
+
<d:response>
|
|
265
|
+
<d:href>/dir/file.txt</d:href>
|
|
266
|
+
<d:status>HTTP/1.1 403 Forbidden</d:status>
|
|
267
|
+
</d:response>
|
|
268
|
+
</d:multistatus>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Parses to:
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
[
|
|
275
|
+
{
|
|
276
|
+
href: '/dir/file.txt',
|
|
277
|
+
propstats: [],
|
|
278
|
+
status: 'HTTP/1.1 403 Forbidden'
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
## Errors
|
|
285
|
+
|
|
286
|
+
Responses with status >= 400 raise `WebDAV::Error`, which has `code`, `message`, and `body`.
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
## Dependencies
|
|
290
|
+
|
|
291
|
+
- [http.rb](https://github.com/thoran/http.rb)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
## Contributing
|
|
295
|
+
|
|
296
|
+
1. Fork it [https://github.com/thoran/webdav/fork](https://github.com/thoran/webdav/fork)
|
|
297
|
+
2. Create your feature branch (git checkout -b my-new-feature)
|
|
298
|
+
3. Commit your changes (git commit -am 'Add some feature')
|
|
299
|
+
4. Push to the branch (git push origin my-new-feature)
|
|
300
|
+
5. Create a new pull request
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
## Licence
|
|
304
|
+
|
|
305
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Thoran/Array/AllButFirst/all_but_first.rb
|
|
2
|
+
# Thoran::Array::AllButFirst#all_but_first
|
|
3
|
+
|
|
4
|
+
# 20141223
|
|
5
|
+
# 0.1.0
|
|
6
|
+
|
|
7
|
+
# Description: This returns a copy of the receiving array with the first element removed.
|
|
8
|
+
|
|
9
|
+
# Changes:
|
|
10
|
+
# 1. + Thoran namespace.
|
|
11
|
+
|
|
12
|
+
require 'Thoran/Array/FirstX/firstX'
|
|
13
|
+
|
|
14
|
+
module Thoran
|
|
15
|
+
module Array
|
|
16
|
+
module AllButFirst
|
|
17
|
+
|
|
18
|
+
def all_but_first
|
|
19
|
+
d = self.dup
|
|
20
|
+
d.first!
|
|
21
|
+
d
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Array.send(:include, Thoran::Array::AllButFirst)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Thoran/Array/FirstX/firstX.rb
|
|
2
|
+
# Thoran::Array::FirstX#first!
|
|
3
|
+
|
|
4
|
+
# 20180804
|
|
5
|
+
# 0.3.3
|
|
6
|
+
|
|
7
|
+
# Description: Sometimes it makes more sense to treat arrays this way.
|
|
8
|
+
|
|
9
|
+
# Changes since 0.2:
|
|
10
|
+
# 1. Added the original version 0.1.0 of the implementation to the later 0.1.0!
|
|
11
|
+
# 0/1
|
|
12
|
+
# 2. Switched the tests to spec-style.
|
|
13
|
+
# 1/2
|
|
14
|
+
# 3. Added a test for the state of the array afterward, since this is meant to be an in place change.
|
|
15
|
+
# 2/3
|
|
16
|
+
# 4. Added tests for the extended functionality introduced in the first version 0.1.0.
|
|
17
|
+
|
|
18
|
+
module Thoran
|
|
19
|
+
module Array
|
|
20
|
+
module FirstX
|
|
21
|
+
|
|
22
|
+
def first!(n = 1)
|
|
23
|
+
return_value = []
|
|
24
|
+
n.times{return_value << self.shift}
|
|
25
|
+
return_value.size == 1 ? return_value[0] : return_value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Array.send(:include, Thoran::Array::FirstX)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Thoran/String/ToConst/to_const.rb
|
|
2
|
+
# Thoran::String::ToConst#to_const
|
|
3
|
+
|
|
4
|
+
# 20141223
|
|
5
|
+
# 0.2.0
|
|
6
|
+
|
|
7
|
+
# Description: This takes a string and returns a constant, with unlimited namespacing.
|
|
8
|
+
|
|
9
|
+
# History: Derived from Object#to_const 0.3.0, and superceding Object#to_const.
|
|
10
|
+
|
|
11
|
+
# Changes:
|
|
12
|
+
# 1. + Thoran namespace.
|
|
13
|
+
|
|
14
|
+
# Todo:
|
|
15
|
+
# 1. This only works for two levels of constants. Three and you're stuffed. So, this needs to be recursive...
|
|
16
|
+
# Done iteratively as of 0.1.0.
|
|
17
|
+
# 2. Make this work for symbols. However, this will only work if there's no namespacing. ie. :A is OK, but :A::B is not.
|
|
18
|
+
|
|
19
|
+
# Discussion:
|
|
20
|
+
# 1. Should this go separately into classes for which ::const_get will work and be removed from Object? Done as of 0.1.0.
|
|
21
|
+
|
|
22
|
+
require 'Thoran/Array/AllButFirst/all_but_first'
|
|
23
|
+
|
|
24
|
+
module Thoran
|
|
25
|
+
module String
|
|
26
|
+
module ToConst
|
|
27
|
+
|
|
28
|
+
def to_const
|
|
29
|
+
if self =~ /::/
|
|
30
|
+
constants = self.split('::')
|
|
31
|
+
constant = Object.const_get(constants.first)
|
|
32
|
+
constants = constants.all_but_first
|
|
33
|
+
until constants.empty? do
|
|
34
|
+
constant = constant.const_get(constants.shift)
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
constant = Object.const_get(self)
|
|
38
|
+
end
|
|
39
|
+
constant
|
|
40
|
+
end
|
|
41
|
+
alias_method :to_constant, :to_const
|
|
42
|
+
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
String.send(:include, Thoran::String::ToConst)
|
data/lib/WebDAV/Error.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# WebDAV/Error.rb
|
|
2
|
+
# WebDAV::Error.rb
|
|
3
|
+
|
|
4
|
+
class WebDAV
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
attr_reader :code, :message, :body
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def initialize(response)
|
|
11
|
+
@code = response.code.to_i
|
|
12
|
+
@message = response.message
|
|
13
|
+
@body = response.body
|
|
14
|
+
super("HTTP #{@code}: #{@message}")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# WebDAV/MultiStatus.rb
|
|
2
|
+
# WebDAV::MultiStatus
|
|
3
|
+
|
|
4
|
+
require 'rexml/document'
|
|
5
|
+
|
|
6
|
+
require_relative './Response'
|
|
7
|
+
|
|
8
|
+
class WebDAV
|
|
9
|
+
class MultiStatus < Response
|
|
10
|
+
attr_reader :resources
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def initialize(response)
|
|
15
|
+
super
|
|
16
|
+
@resources = parse
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parse
|
|
20
|
+
doc = REXML::Document.new(body)
|
|
21
|
+
doc.elements.collect('//d:response'){|response_element| parse_response(response_element)}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parse_response(response_element)
|
|
25
|
+
{
|
|
26
|
+
href: response_element.elements['d:href']&.text,
|
|
27
|
+
propstats: parse_propstats(response_element),
|
|
28
|
+
status: parse_response_status(response_element)
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def parse_propstats(response_element)
|
|
33
|
+
response_element.elements.collect('d:propstat'){|propstat_element| parse_propstat(propstat_element)}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def parse_propstat(propstat_element)
|
|
37
|
+
{
|
|
38
|
+
properties: parse_properties(propstat_element.elements['d:prop']),
|
|
39
|
+
status: propstat_element.elements['d:status']&.text
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parse_properties(prop_element)
|
|
44
|
+
return {} unless prop_element
|
|
45
|
+
prop_element.elements.to_a.each_with_object({}) do |property_element, result|
|
|
46
|
+
result[property_element.namespace] ||= {}
|
|
47
|
+
result[property_element.namespace][property_element.name] = property_element.text || property_element.to_s
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_response_status(response_element)
|
|
52
|
+
return nil if response_element.elements['d:propstat']
|
|
53
|
+
response_element.elements['d:status']&.text
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# WebDAV/Response.rb
|
|
2
|
+
# WebDAV::Response.rb
|
|
3
|
+
|
|
4
|
+
class WebDAV
|
|
5
|
+
class Response
|
|
6
|
+
attr_reader :code, :message, :headers, :body
|
|
7
|
+
|
|
8
|
+
def success?
|
|
9
|
+
code >= 200 && code < 300
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def etag
|
|
13
|
+
headers['ETag']
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def content_type
|
|
17
|
+
headers['Content-Type']
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def initialize(response)
|
|
23
|
+
@code = response.code.to_i
|
|
24
|
+
@message = response.message
|
|
25
|
+
@headers = response
|
|
26
|
+
@body = response.body
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|