calligraphy 0.2.1 → 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.
- checksums.yaml +5 -5
- data/README.md +1 -1
- data/lib/calligraphy.rb +23 -17
- data/lib/calligraphy/rails/mapper.rb +150 -142
- data/lib/calligraphy/rails/web_dav_methods.rb +67 -0
- data/lib/calligraphy/rails/web_dav_preconditions.rb +114 -0
- data/lib/calligraphy/rails/web_dav_requests_controller.rb +72 -180
- data/lib/calligraphy/resource/file_resource.rb +377 -194
- data/lib/calligraphy/resource/resource.rb +192 -32
- data/lib/calligraphy/utils.rb +23 -6
- data/lib/calligraphy/version.rb +3 -1
- data/lib/calligraphy/{copy.rb → web_dav_request/copy.rb} +13 -8
- data/lib/calligraphy/{delete.rb → web_dav_request/delete.rb} +6 -2
- data/lib/calligraphy/web_dav_request/get.rb +18 -0
- data/lib/calligraphy/web_dav_request/lock.rb +89 -0
- data/lib/calligraphy/{mkcol.rb → web_dav_request/mkcol.rb} +7 -2
- data/lib/calligraphy/web_dav_request/move.rb +56 -0
- data/lib/calligraphy/web_dav_request/propfind.rb +24 -0
- data/lib/calligraphy/web_dav_request/proppatch.rb +29 -0
- data/lib/calligraphy/web_dav_request/put.rb +16 -0
- data/lib/calligraphy/{unlock.rb → web_dav_request/unlock.rb} +6 -1
- data/lib/calligraphy/web_dav_request/web_dav_request.rb +43 -0
- data/lib/calligraphy/xml/builder.rb +83 -117
- data/lib/calligraphy/xml/namespace.rb +12 -6
- data/lib/calligraphy/xml/node.rb +24 -10
- data/lib/calligraphy/xml/utils.rb +22 -11
- data/lib/calligraphy/xml/web_dav_elements.rb +92 -0
- data/lib/generators/calligraphy/install_generator.rb +4 -0
- data/lib/generators/templates/calligraphy.rb +2 -0
- metadata +109 -22
- data/lib/calligraphy/get.rb +0 -12
- data/lib/calligraphy/lock.rb +0 -52
- data/lib/calligraphy/move.rb +0 -31
- data/lib/calligraphy/propfind.rb +0 -18
- data/lib/calligraphy/proppatch.rb +0 -20
- data/lib/calligraphy/put.rb +0 -12
- data/lib/calligraphy/web_dav_request.rb +0 -31
- data/spec/spec_helper.rb +0 -46
@@ -1,93 +1,253 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Calligraphy
|
4
|
+
# Resource base class.
|
5
|
+
#
|
6
|
+
# All custom resource classes should be inherited from Resource and should
|
7
|
+
# implement the relevant methods needed for the desired level of WebDAV
|
8
|
+
# support.
|
2
9
|
class Resource
|
3
10
|
attr_accessor :client_nonce, :contents, :updated_at
|
4
|
-
attr_reader :full_request_path, :mount_point, :request_body, :request_path,
|
11
|
+
attr_reader :full_request_path, :mount_point, :request_body, :request_path,
|
12
|
+
:root_dir
|
5
13
|
|
14
|
+
#:nodoc:
|
6
15
|
def initialize(resource: nil, req: nil, mount: nil, root_dir: nil)
|
7
16
|
@full_request_path = req&.original_url
|
8
17
|
@mount_point = mount || req&.path&.tap { |s| s.slice! resource }
|
9
18
|
@request_body = req&.body&.read || ''
|
10
19
|
@request_path = mount.nil? ? resource : resource.split(mount)[-1]
|
20
|
+
@root_dir = root_dir
|
11
21
|
end
|
12
22
|
|
23
|
+
# Responsible for returning a boolean value indicating if an ancestor
|
24
|
+
# exists for the resource.
|
25
|
+
#
|
26
|
+
# Used in COPY and MKCOL requests.
|
13
27
|
def ancestor_exist?
|
14
|
-
raise
|
28
|
+
raise NotImplementedError
|
15
29
|
end
|
16
30
|
|
17
|
-
|
18
|
-
|
31
|
+
# Responsible for returning a boolean value indicating if the resource
|
32
|
+
# is a collection.
|
33
|
+
#
|
34
|
+
# Used in DELETE, MKCOL, MOVE, and PUT requests.
|
35
|
+
def collection?
|
36
|
+
raise NotImplementedError
|
19
37
|
end
|
20
38
|
|
21
|
-
|
22
|
-
|
39
|
+
# Responsible for returning a hash with keys indicating if the resource
|
40
|
+
# can be copied, if an ancestor exists, or if the copy destinatin is
|
41
|
+
# locked.
|
42
|
+
#
|
43
|
+
# Return hash should contain `can_copy`, `ancestor_exist`, and `locked`
|
44
|
+
# keys with boolean values.
|
45
|
+
#
|
46
|
+
# Used in COPY and MOVE (which inherits from COPY) requests.
|
47
|
+
def copy_options(_options)
|
48
|
+
raise NotImplementedError
|
23
49
|
end
|
24
50
|
|
25
|
-
|
26
|
-
|
51
|
+
# Responsible for creating a duplicate of the resource in
|
52
|
+
# `options[:destination]` (see section 9.8 of RFC4918).
|
53
|
+
#
|
54
|
+
# Used in COPY and MOVE (which inherits from COPY) requests.
|
55
|
+
def copy(_options)
|
56
|
+
raise NotImplementedError
|
27
57
|
end
|
28
58
|
|
59
|
+
# Responsible for creating a new collection based on the resource (see
|
60
|
+
# section 9.3 of RFC4918).
|
61
|
+
#
|
62
|
+
# Used in MKCOL requests.
|
29
63
|
def create_collection
|
30
|
-
raise
|
64
|
+
raise NotImplementedError
|
31
65
|
end
|
32
66
|
|
67
|
+
# A DAV-compliant resource can advertise several classes of compliance.
|
68
|
+
# `dav_compliance` is responsible for returning the classes of WebDAV
|
69
|
+
# compliance that the resource supports (see section 18 of RFC4918).
|
70
|
+
#
|
71
|
+
# Used in OPTIONS requests.
|
72
|
+
def dav_compliance
|
73
|
+
'1, 2, 3'
|
74
|
+
end
|
75
|
+
|
76
|
+
# Responsible for deleting a resource collection (see section 9.6 of
|
77
|
+
# RFC4918).
|
78
|
+
#
|
79
|
+
# Used in DELETE and MOVE requests.
|
33
80
|
def delete_collection
|
34
|
-
raise
|
81
|
+
raise NotImplementedError
|
35
82
|
end
|
36
83
|
|
84
|
+
# Responsible for returning unique identifier used to create an etag.
|
85
|
+
#
|
86
|
+
# Used in precondition validation, as well as GET, HEAD, and PROPFIND
|
87
|
+
# requests.
|
37
88
|
def etag
|
38
|
-
raise
|
89
|
+
raise NotImplementedError
|
39
90
|
end
|
40
91
|
|
92
|
+
# Responsible for indicating if the resource already exists.
|
93
|
+
#
|
94
|
+
# Used in DELETE, LOCK, MKCOL, and MOVE requests.
|
41
95
|
def exists?
|
42
|
-
raise
|
96
|
+
raise NotImplementedError
|
43
97
|
end
|
44
98
|
|
45
|
-
|
46
|
-
|
99
|
+
# Responsible for creating a lock on the resource (see section 9.10 of
|
100
|
+
# RFC4918).
|
101
|
+
#
|
102
|
+
# Used in LOCK requests.
|
103
|
+
def lock(_nodes, _depth = 'infinity')
|
104
|
+
raise NotImplementedError
|
47
105
|
end
|
48
106
|
|
107
|
+
# Responsible for indicating if a resource lock is exclusive.
|
108
|
+
#
|
109
|
+
# Used in LOCK requests.
|
49
110
|
def lock_is_exclusive?
|
50
|
-
raise
|
51
|
-
end
|
52
|
-
|
53
|
-
def lock_tokens
|
54
|
-
raise NotImplemented
|
111
|
+
raise NotImplementedError
|
55
112
|
end
|
56
113
|
|
114
|
+
# Responsible for indicating if a resource is current locked.
|
115
|
+
#
|
116
|
+
# Used in LOCK requests.
|
57
117
|
def locked?
|
58
|
-
raise
|
118
|
+
raise NotImplementedError
|
59
119
|
end
|
60
120
|
|
61
|
-
|
62
|
-
|
121
|
+
# Responsible for indicating if a resource is locked to the current user.
|
122
|
+
#
|
123
|
+
# Used in DELETE, LOCK, MOVE, PROPPATCH, and PUT requests.
|
124
|
+
def locked_to_user?(_headers = nil)
|
125
|
+
raise NotImplementedError
|
63
126
|
end
|
64
127
|
|
65
|
-
|
66
|
-
|
128
|
+
# Responsible for handling the retrieval of properties defined on the
|
129
|
+
# resource (see section 9.1 of RFC4918).
|
130
|
+
#
|
131
|
+
# Used in PROPFIND requests.
|
132
|
+
def propfind(_nodes)
|
133
|
+
raise NotImplementedError
|
67
134
|
end
|
68
135
|
|
69
|
-
|
70
|
-
|
136
|
+
# Responsible for handling the addition and/or removal of properties
|
137
|
+
# defined on the resource through a PROPPATCH request (see section 9.2 of
|
138
|
+
# RFC4918).
|
139
|
+
#
|
140
|
+
# Used in PROPPATCH requests.
|
141
|
+
def proppatch(_nodes)
|
142
|
+
raise NotImplementedError
|
71
143
|
end
|
72
144
|
|
145
|
+
# Responsible for setting and returning the contents of a resource
|
146
|
+
# if it is readable (see section 9.4 of RFC4918).
|
147
|
+
#
|
148
|
+
# Used in GET requests.
|
73
149
|
def read
|
74
|
-
raise
|
150
|
+
raise NotImplementedError
|
75
151
|
end
|
76
152
|
|
153
|
+
# Responsible for indicating if a resource is readable.
|
154
|
+
#
|
155
|
+
# Used in GET and HEAD requests.
|
77
156
|
def readable?
|
78
157
|
exists? && !collection?
|
79
158
|
end
|
80
159
|
|
160
|
+
# Responsible for refreshing locks (see section 9.10.2 of RFC4918).
|
161
|
+
#
|
162
|
+
# Used in LOCK requests.
|
81
163
|
def refresh_lock
|
82
|
-
raise
|
164
|
+
raise NotImplementedError
|
165
|
+
end
|
166
|
+
|
167
|
+
# Responsible for unlocking a resource lock (see section 9.11 of RFC4918).
|
168
|
+
#
|
169
|
+
# Used in UNLOCK requests.
|
170
|
+
def unlock(_token)
|
171
|
+
raise NotImplementedError
|
172
|
+
end
|
173
|
+
|
174
|
+
# Responsible for writing contents to a resource (see section 9.7 of
|
175
|
+
# RFC4918).
|
176
|
+
#
|
177
|
+
# Used in PUT requests.
|
178
|
+
def write(_contents = @request_body.to_s)
|
179
|
+
raise NotImplementedError
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
# DAV property which can be retrieved by a PROPFIND request. `creationdate`
|
185
|
+
# records the time and date the resource was created (see section 15.1 of
|
186
|
+
# RFC4918).
|
187
|
+
def creationdate
|
188
|
+
raise NotImplementedError
|
189
|
+
end
|
190
|
+
|
191
|
+
# DAV property which can be retrieved by a PROPFIND request. `displayname`
|
192
|
+
# returns a name for the resource that is suitable for presentation to the
|
193
|
+
# user (see section 15.2 of RFC4918).
|
194
|
+
def displayname
|
195
|
+
raise NotImplementedError
|
196
|
+
end
|
197
|
+
|
198
|
+
# DAV property which can be retrieved by a PROPFIND request.
|
199
|
+
# `getcontentlanguage` returns the Content-Language header value (see
|
200
|
+
# section 15.3 of RFC4918).
|
201
|
+
def getcontentlanguage
|
202
|
+
raise NotImplementedError
|
203
|
+
end
|
204
|
+
|
205
|
+
# DAV property which can be retrieved by a PROPFIND request.
|
206
|
+
# `getcontentlength` returns the Content-Length header value (see section
|
207
|
+
# 15.4 of RFC4918).
|
208
|
+
def getcontentlength
|
209
|
+
raise NotImplementedError
|
210
|
+
end
|
211
|
+
|
212
|
+
# DAV property which can be retrieved by a PROPFIND request.
|
213
|
+
# `getcontenttype` returns the Content-Type header value (see section
|
214
|
+
# 15.5 of RFC4918).
|
215
|
+
def getcontenttype
|
216
|
+
raise NotImplementedError
|
217
|
+
end
|
218
|
+
|
219
|
+
# DAV property which can be retrieved by a PROPFIND request.
|
220
|
+
# `getetag` returns the ETag header value (see section 15.6 of RFC4918).
|
221
|
+
def getetag
|
222
|
+
raise NotImplementedError
|
223
|
+
end
|
224
|
+
|
225
|
+
# DAV property which can be retrieved by a PROPFIND request.
|
226
|
+
# `getlastmodified` returns the Last-Modified header value (see section
|
227
|
+
# 15.7 of RFC4918).
|
228
|
+
def getlastmodified
|
229
|
+
raise NotImplementedError
|
230
|
+
end
|
231
|
+
|
232
|
+
# DAV property which can be retrieved by a PROPFIND request.
|
233
|
+
# `lockdiscovery` describes the active locks on a resource (see section
|
234
|
+
# 15.8 of RFC4918).
|
235
|
+
def lockdiscovery
|
236
|
+
raise NotImplementedError
|
83
237
|
end
|
84
238
|
|
85
|
-
|
86
|
-
|
239
|
+
# DAV property which can be retrieved by a PROPFIND request.
|
240
|
+
# `resourcetype` specifies the nature of the resource (see section 15.9 of
|
241
|
+
# RFC4918).
|
242
|
+
def resourcetype
|
243
|
+
raise NotImplementedError
|
87
244
|
end
|
88
245
|
|
89
|
-
|
90
|
-
|
246
|
+
# DAV property which can be retrieved by a PROPFIND request.
|
247
|
+
# `supportedlock` provides a listing of the lock capabilities supported by
|
248
|
+
# the resource (see section 15.10 of RFC4918).
|
249
|
+
def supportedlock
|
250
|
+
raise NotImplementedError
|
91
251
|
end
|
92
252
|
end
|
93
253
|
end
|
data/lib/calligraphy/utils.rb
CHANGED
@@ -1,38 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Calligraphy
|
4
|
+
# Miscellaneous general convenience methods.
|
2
5
|
module Utils
|
3
|
-
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE']
|
4
|
-
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE']
|
6
|
+
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].freeze
|
7
|
+
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].freeze
|
5
8
|
|
6
|
-
|
9
|
+
# Determines if a value is truthy.
|
10
|
+
def true?(val)
|
7
11
|
TRUE_VALUES.include? val
|
8
12
|
end
|
9
13
|
|
10
|
-
|
14
|
+
# Determines if a value is falsy.
|
15
|
+
def false?(val)
|
11
16
|
FALSE_VALUES.include? val
|
12
17
|
end
|
13
18
|
|
19
|
+
# Joins paths.
|
14
20
|
def join_paths(*paths)
|
15
21
|
paths.join '/'
|
16
22
|
end
|
17
23
|
|
24
|
+
# Given a path and separator, splits the path string using the separator
|
25
|
+
# and pops off the last element of the split array.
|
18
26
|
def split_and_pop(path:, separator: '/')
|
19
27
|
path.split(separator)[0..-2]
|
20
28
|
end
|
21
29
|
|
30
|
+
# Determines if object exists and if existing object is of a given type.
|
22
31
|
def obj_exists_and_is_not_type?(obj:, type:)
|
23
32
|
obj.nil? ? false : obj != type
|
24
33
|
end
|
25
34
|
|
35
|
+
# Given an array of hashes, returns an array of hash values.
|
26
36
|
def map_array_of_hashes(arr_hashes)
|
27
37
|
[].tap do |output_array|
|
28
38
|
arr_hashes.each do |hash|
|
29
|
-
output_array.push hash.
|
39
|
+
output_array.push hash.values
|
30
40
|
end
|
31
41
|
end
|
32
42
|
end
|
33
43
|
|
44
|
+
# Extracts a lock token from an If headers.
|
34
45
|
def extract_lock_token(if_header)
|
35
|
-
if_header.scan(Calligraphy::LOCK_TOKEN_REGEX)
|
46
|
+
token = if_header.scan(Calligraphy::LOCK_TOKEN_REGEX)
|
47
|
+
token.flatten.first if token.is_a? Array
|
48
|
+
end
|
49
|
+
|
50
|
+
# Hash used in describing a supportedlock.
|
51
|
+
def lockentry_hash(scope, type)
|
52
|
+
{ lockentry: { lockscope: scope, locktype: type } }
|
36
53
|
end
|
37
54
|
end
|
38
55
|
end
|
data/lib/calligraphy/version.rb
CHANGED
@@ -1,19 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Calligraphy
|
4
|
+
# Responsible for creating a duplicate of the source resource identified
|
5
|
+
# by the request to the destination resource identified by the URI in
|
6
|
+
# the Destination header.
|
2
7
|
class Copy < WebDavRequest
|
3
|
-
|
8
|
+
# Executes the WebDAV request for a particular resource.
|
9
|
+
def execute
|
4
10
|
options = copy_move_options
|
5
|
-
|
11
|
+
copy_options = @resource.copy_options options
|
6
12
|
|
7
|
-
|
8
|
-
return :precondition_failed
|
9
|
-
else
|
13
|
+
unless copy_options[:can_copy]
|
14
|
+
return :precondition_failed if copy_options[:ancestor_exist]
|
10
15
|
return :conflict
|
11
|
-
end
|
16
|
+
end
|
12
17
|
|
13
|
-
return :locked if
|
18
|
+
return :locked if copy_options[:locked]
|
14
19
|
|
15
20
|
overwritten = @resource.copy options
|
16
|
-
|
21
|
+
overwritten ? :no_content : :created
|
17
22
|
end
|
18
23
|
|
19
24
|
private
|
@@ -1,6 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Calligraphy
|
4
|
+
# Responsible for deleting the resource identified by the request.
|
2
5
|
class Delete < WebDavRequest
|
3
|
-
|
6
|
+
# Executes the WebDAV request for a particular resource.
|
7
|
+
def execute
|
4
8
|
return :locked if @resource.locked_to_user? @headers
|
5
9
|
|
6
10
|
if @resource.collection?
|
@@ -11,7 +15,7 @@ module Calligraphy
|
|
11
15
|
return :not_found unless @resource.exists?
|
12
16
|
end
|
13
17
|
|
14
|
-
|
18
|
+
:no_content
|
15
19
|
end
|
16
20
|
end
|
17
21
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Calligraphy
|
4
|
+
# Responsible for retrieving whatever information is identified by the
|
5
|
+
# request.
|
6
|
+
class Get < WebDavRequest
|
7
|
+
# Executes the WebDAV request for a particular resource.
|
8
|
+
def execute(head: false)
|
9
|
+
if @resource.readable?
|
10
|
+
return :ok if head
|
11
|
+
|
12
|
+
[:ok, @resource.read]
|
13
|
+
else
|
14
|
+
:not_found
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Calligraphy
|
4
|
+
# Responsible for taking out a lock of any access type and refreshing
|
5
|
+
# existing locks.
|
6
|
+
class Lock < WebDavRequest
|
7
|
+
include Calligraphy::XML::Utils
|
8
|
+
|
9
|
+
attr_reader :resource_exists
|
10
|
+
|
11
|
+
#:nodoc:
|
12
|
+
def initialize(headers:, request:, response:, resource:)
|
13
|
+
super
|
14
|
+
|
15
|
+
# Determine is resource already exists before lock operation.
|
16
|
+
@resource_exists = @resource.exists?
|
17
|
+
end
|
18
|
+
|
19
|
+
# Executes the WebDAV request for a particular resource.
|
20
|
+
def execute
|
21
|
+
if refresh_lock?
|
22
|
+
lock_properties = @resource.refresh_lock
|
23
|
+
elsif resource_locked?
|
24
|
+
return :locked
|
25
|
+
else
|
26
|
+
# The `lockinfo` tag is used to specify the type of lock the client
|
27
|
+
# wishes to have created.
|
28
|
+
xml = xml_for body: body, node: 'lockinfo'
|
29
|
+
return :bad_request if xml == :bad_request
|
30
|
+
|
31
|
+
lock_properties = @resource.lock xml, @headers['Depth']
|
32
|
+
end
|
33
|
+
|
34
|
+
build_response lock_properties
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def refresh_lock?
|
40
|
+
@resource.request_body.blank? && !@resource.locked_to_user?(@headers)
|
41
|
+
end
|
42
|
+
|
43
|
+
def resource_locked?
|
44
|
+
(@resource.locked? && @resource.lock_is_exclusive?) ||
|
45
|
+
(@resource.locked_to_user?(@headers) && !xml_contains_shared_lock?)
|
46
|
+
end
|
47
|
+
|
48
|
+
def xml_contains_shared_lock?
|
49
|
+
lock_type = nil
|
50
|
+
xml = xml_for body: body, node: 'lockinfo'
|
51
|
+
|
52
|
+
xml.each do |node|
|
53
|
+
next unless node.is_a? Nokogiri::XML::Element
|
54
|
+
|
55
|
+
lock_type = node.children[0].name if node.name == 'lockscope'
|
56
|
+
end
|
57
|
+
|
58
|
+
lock_type == 'shared'
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_response(lock_properties)
|
62
|
+
builder = xml_builder
|
63
|
+
xml_res = builder.lock_response lock_properties
|
64
|
+
|
65
|
+
lock_token = extract_lock_token lock_properties
|
66
|
+
prepare_response_headers lock_token
|
67
|
+
|
68
|
+
response_status xml_res
|
69
|
+
end
|
70
|
+
|
71
|
+
def extract_lock_token(properties)
|
72
|
+
properties[-1]
|
73
|
+
.select { |x| x.name == 'locktoken' }[0]
|
74
|
+
.children[0]
|
75
|
+
.text
|
76
|
+
end
|
77
|
+
|
78
|
+
def prepare_response_headers(lock_token)
|
79
|
+
response.headers['Lock-Token'] = "<#{lock_token}>"
|
80
|
+
|
81
|
+
set_xml_content_type
|
82
|
+
end
|
83
|
+
|
84
|
+
def response_status(xml_res)
|
85
|
+
return :ok, xml_res if @resource_exists
|
86
|
+
[:created, xml_res]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|