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.
Files changed (38) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +1 -1
  3. data/lib/calligraphy.rb +23 -17
  4. data/lib/calligraphy/rails/mapper.rb +150 -142
  5. data/lib/calligraphy/rails/web_dav_methods.rb +67 -0
  6. data/lib/calligraphy/rails/web_dav_preconditions.rb +114 -0
  7. data/lib/calligraphy/rails/web_dav_requests_controller.rb +72 -180
  8. data/lib/calligraphy/resource/file_resource.rb +377 -194
  9. data/lib/calligraphy/resource/resource.rb +192 -32
  10. data/lib/calligraphy/utils.rb +23 -6
  11. data/lib/calligraphy/version.rb +3 -1
  12. data/lib/calligraphy/{copy.rb → web_dav_request/copy.rb} +13 -8
  13. data/lib/calligraphy/{delete.rb → web_dav_request/delete.rb} +6 -2
  14. data/lib/calligraphy/web_dav_request/get.rb +18 -0
  15. data/lib/calligraphy/web_dav_request/lock.rb +89 -0
  16. data/lib/calligraphy/{mkcol.rb → web_dav_request/mkcol.rb} +7 -2
  17. data/lib/calligraphy/web_dav_request/move.rb +56 -0
  18. data/lib/calligraphy/web_dav_request/propfind.rb +24 -0
  19. data/lib/calligraphy/web_dav_request/proppatch.rb +29 -0
  20. data/lib/calligraphy/web_dav_request/put.rb +16 -0
  21. data/lib/calligraphy/{unlock.rb → web_dav_request/unlock.rb} +6 -1
  22. data/lib/calligraphy/web_dav_request/web_dav_request.rb +43 -0
  23. data/lib/calligraphy/xml/builder.rb +83 -117
  24. data/lib/calligraphy/xml/namespace.rb +12 -6
  25. data/lib/calligraphy/xml/node.rb +24 -10
  26. data/lib/calligraphy/xml/utils.rb +22 -11
  27. data/lib/calligraphy/xml/web_dav_elements.rb +92 -0
  28. data/lib/generators/calligraphy/install_generator.rb +4 -0
  29. data/lib/generators/templates/calligraphy.rb +2 -0
  30. metadata +109 -22
  31. data/lib/calligraphy/get.rb +0 -12
  32. data/lib/calligraphy/lock.rb +0 -52
  33. data/lib/calligraphy/move.rb +0 -31
  34. data/lib/calligraphy/propfind.rb +0 -18
  35. data/lib/calligraphy/proppatch.rb +0 -20
  36. data/lib/calligraphy/put.rb +0 -12
  37. data/lib/calligraphy/web_dav_request.rb +0 -31
  38. 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, :root_dir
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 NotImplemented
28
+ raise NotImplementedError
15
29
  end
16
30
 
17
- def can_copy?(options)
18
- raise NotImplemented
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
- def collection?
22
- raise NotImplemented
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
- def copy(options)
26
- raise NotImplemented
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 NotImplemented
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 NotImplemented
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 NotImplemented
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 NotImplemented
96
+ raise NotImplementedError
43
97
  end
44
98
 
45
- def lock(nodes, depth=INFINITY)
46
- raise NotImplemented
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 NotImplemented
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 NotImplemented
118
+ raise NotImplementedError
59
119
  end
60
120
 
61
- def locked_to_user?(headers=nil)
62
- raise NotImplemented
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
- def propfind(nodes)
66
- raise NotImplemented
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
- def proppatch(nodes)
70
- raise NotImplemented
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 NotImplemented
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 NotImplemented
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
- def unlock(token)
86
- raise NotImplemented
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
- def write(contents=@request_body.to_s)
90
- raise NotImplemented
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
@@ -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
- def is_true?(val)
9
+ # Determines if a value is truthy.
10
+ def true?(val)
7
11
  TRUE_VALUES.include? val
8
12
  end
9
13
 
10
- def is_false?(val)
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.map { |k, v| v }
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)&.flatten[0]
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Calligraphy
2
- VERSION = '0.2.1'
4
+ VERSION = '0.3.1'
3
5
  end
@@ -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
- def request
8
+ # Executes the WebDAV request for a particular resource.
9
+ def execute
4
10
  options = copy_move_options
5
- can_copy = @resource.can_copy? options
11
+ copy_options = @resource.copy_options options
6
12
 
7
- if can_copy[:ancestor_exist]
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 unless can_copy[:can_copy]
16
+ end
12
17
 
13
- return :locked if can_copy[:locked]
18
+ return :locked if copy_options[:locked]
14
19
 
15
20
  overwritten = @resource.copy options
16
- return overwritten ? :no_content : :created
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
- def request
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
- return :no_content
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