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
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Calligraphy
4
+ module Rails
5
+ # Provides methods to handle checking and validating WebDAV request
6
+ # preconditions.
7
+ module WebDavPreconditions
8
+ private
9
+
10
+ def check_preconditions
11
+ return true unless request.headers['If'].present?
12
+
13
+ evaluate_if_header
14
+ end
15
+
16
+ def evaluate_if_header
17
+ conditions_met = false
18
+ condition_lists = if_conditions
19
+
20
+ condition_lists.each do |list|
21
+ conditions = parse_preconditions list
22
+
23
+ conditions_met = evaluate_preconditions conditions
24
+ break if conditions_met
25
+ end
26
+
27
+ conditions_met
28
+ end
29
+
30
+ def if_conditions
31
+ if request.headers['If'][0] == '<'
32
+ request.headers['If'].split Calligraphy::TAGGED_LIST_REGEX
33
+ else
34
+ request.headers['If'].split Calligraphy::UNTAGGAGED_LIST_REGEX
35
+ end
36
+ end
37
+
38
+ def parse_preconditions(list)
39
+ conditions = conditions_hash
40
+ conditions[:dav_no_lock] = match_dav_no_lock list
41
+ conditions[:resource] = scan_for_resource list
42
+ conditions[:lock_token] = scan_for_lock_token list
43
+ conditions[:etag] = scan_for_etag list
44
+ conditions
45
+ end
46
+
47
+ def conditions_hash
48
+ {
49
+ dav_no_lock: nil,
50
+ etag: nil,
51
+ lock_token: nil,
52
+ resource: nil
53
+ }
54
+ end
55
+
56
+ def match_dav_no_lock(list)
57
+ return nil unless list =~ Calligraphy::DAV_NO_LOCK_REGEX
58
+
59
+ list =~ Calligraphy::DAV_NOT_NO_LOCK_REGEX ? nil : true
60
+ end
61
+
62
+ def scan_for_resource(list)
63
+ return nil unless list =~ Calligraphy::RESOURCE_REGEX
64
+
65
+ list.scan(Calligraphy::RESOURCE_REGEX).flatten[0]
66
+ end
67
+
68
+ def scan_for_lock_token(list)
69
+ return nil unless list =~ Calligraphy::LOCK_TOKEN_REGEX
70
+
71
+ list.scan(Calligraphy::LOCK_TOKEN_REGEX).flatten[0]
72
+ end
73
+
74
+ def scan_for_etag(list)
75
+ return nil unless list =~ Calligraphy::ETAG_IF_REGEX
76
+
77
+ list.scan(Calligraphy::ETAG_IF_REGEX).flatten[0]
78
+ end
79
+
80
+ def evaluate_preconditions(conditions)
81
+ conditions_met = true
82
+
83
+ if conditions[:etag]
84
+ conditions_met = false unless evaluate_etag_condition conditions
85
+ end
86
+
87
+ conditions_met = false if conditions[:dav_no_lock]
88
+ conditions_met
89
+ end
90
+
91
+ def target_resource(conditions)
92
+ if conditions[:resource]
93
+ @resource_class.new(
94
+ resource: conditions[:resource],
95
+ mount: @resource.mount_point
96
+ )
97
+ else
98
+ @resource
99
+ end
100
+ end
101
+
102
+ def evaluate_etag_condition(conditions)
103
+ validators = [@resource.etag, '']
104
+ validate_etag validators, conditions[:etag]
105
+ end
106
+
107
+ def validate_etag(etag_validators, validate_against)
108
+ cache_key = ActiveSupport::Cache.expand_cache_key etag_validators
109
+
110
+ validate_against == "W/\"#{Digest::MD5.hexdigest(cache_key)}\""
111
+ end
112
+ end
113
+ end
114
+ end
@@ -1,213 +1,105 @@
1
- module Calligraphy::Rails
2
- class WebDavRequestsController < ActionController::Base
3
- before_action :verify_resource_scope
4
- before_action :authenticate_with_digest_authentiation
5
- before_action :set_resource
6
-
7
- # Entry-point for all WebDAV requests. Handles checking and validating
8
- # preconditions, directing of requests to the proper WebDAV action
9
- # method, and composing responses to send back to the client.
10
- def invoke_method
11
- method = request.request_method.downcase
12
-
13
- if check_preconditions
14
- if method == 'head'
15
- status = get head: true
16
- elsif Calligraphy.allowed_http_methods.include? method
17
- set_resource_client_nonce(method) if Calligraphy.enable_digest_authentication
18
-
19
- status, body = send method
20
- else
21
- status = :method_not_allowed
1
+ # frozen_string_literal: true
2
+
3
+ module Calligraphy
4
+ module Rails
5
+ # Controller for all WebDAV requests.
6
+ class WebDavRequestsController < ActionController::Base
7
+ include Calligraphy::Rails::WebDavMethods
8
+ include Calligraphy::Rails::WebDavPreconditions
9
+
10
+ before_action :verify_resource_scope
11
+ before_action :authenticate_with_digest_authentiation
12
+ before_action :set_resource
13
+
14
+ # Entry-point for all WebDAV requests. Handles checking and validating
15
+ # preconditions, directing of requests to the proper WebDAV action
16
+ # method, and composing responses to send back to the client.
17
+ def invoke_method
18
+ unless check_preconditions
19
+ return send_response(status: :precondition_failed)
22
20
  end
23
21
 
22
+ method = request.request_method.downcase
23
+ status, body = make_request method
24
+
24
25
  send_response status: status, body: body
25
- else
26
- send_response status: :precondition_failed
27
26
  end
28
- end
29
27
 
30
- private
28
+ private
31
29
 
32
- def verify_resource_scope
33
- head :forbidden if %w(. ..).any? { |seg| params[:resource].include? seg }
34
- end
30
+ def verify_resource_scope
31
+ # Prevent any request with `.` or `..` as part of the resource.
32
+ head :forbidden if %w[. ..].any? do |seg|
33
+ params[:resource].include? seg
34
+ end
35
+ end
35
36
 
36
- def authenticate_with_digest_authentiation
37
- return unless Calligraphy.enable_digest_authentication
37
+ def authenticate_with_digest_authentiation
38
+ return unless digest_enabled?
38
39
 
39
- realm = Calligraphy.http_authentication_realm
40
+ realm = Calligraphy.http_authentication_realm
40
41
 
41
- authenticate_or_request_with_http_digest(realm) do |username|
42
- Calligraphy.digest_password_procedure.call(username)
42
+ authenticate_or_request_with_http_digest(realm) do |username|
43
+ Calligraphy.digest_password_procedure.call(username)
44
+ end
43
45
  end
44
- end
45
46
 
46
- def set_resource
47
- resource_id = if params[:format]
48
- [params[:resource], params[:format]].join '.'
49
- else
50
- params[:resource]
47
+ def digest_enabled?
48
+ Calligraphy.enable_digest_authentication
51
49
  end
52
50
 
53
- @resource_class = params[:resource_class]
54
- @resource_root_path = params[:resource_root_path]
55
- @resource = @resource_class.new resource: resource_id, req: request, root_dir: @resource_root_path
56
- end
57
-
58
- def check_preconditions
59
- return true unless request.headers['If'].present?
60
-
61
- evaluate_if_header
62
- end
63
-
64
- def evaluate_if_header
65
- conditions_met = false
66
- condition_lists = get_if_conditions
67
-
68
- condition_lists.each do |list|
69
- conditions = parse_preconditions list
51
+ def set_resource
52
+ @resource_class = params[:resource_class] || Calligraphy::Resource
53
+ @resource_root_path = params[:resource_root_path]
70
54
 
71
- conditions_met = evaluate_preconditions conditions
72
- break if conditions_met
55
+ @resource = @resource_class.new(
56
+ resource: resource_id,
57
+ req: request,
58
+ root_dir: @resource_root_path
59
+ )
73
60
  end
74
61
 
75
- conditions_met
76
- end
77
-
78
- def get_if_conditions
79
- lists = if request.headers['If'][0] == '<'
80
- request.headers['If'].split Calligraphy::TAGGED_LIST_REGEX
81
- else
82
- request.headers['If'].split Calligraphy::UNTAGGAGED_LIST_REGEX
62
+ def resource_id
63
+ if params[:format]
64
+ [params[:resource], params[:format]].join '.'
65
+ else
66
+ params[:resource]
67
+ end
83
68
  end
84
69
 
85
- lists
86
- end
87
-
88
- def parse_preconditions(list)
89
- conditions = { dav_no_lock: nil, etag: nil, lock_token: nil, resource: nil }
90
-
91
- conditions[:dav_no_lock] = if list =~ Calligraphy::DAV_NO_LOCK_REGEX
92
- list =~ Calligraphy::DAV_NOT_NO_LOCK_REGEX ? nil : true
93
- end
70
+ def make_request(method)
71
+ if method == 'head'
72
+ status = get head: true
73
+ elsif Calligraphy.allowed_http_methods.include? method
74
+ resource_client_nonce(method) if digest_enabled?
94
75
 
95
- if list =~ Calligraphy::RESOURCE_REGEX
96
- conditions[:resource] = list.scan(Calligraphy::RESOURCE_REGEX).flatten[0]
97
- end
76
+ status, body = send method
77
+ else
78
+ status = :method_not_allowed
79
+ end
98
80
 
99
- if list =~ Calligraphy::LOCK_TOKEN_REGEX
100
- conditions[:lock_token] = list.scan(Calligraphy::LOCK_TOKEN_REGEX).flatten[0]
81
+ [status, body]
101
82
  end
102
83
 
103
- if list =~ Calligraphy::ETAG_IF_REGEX
104
- conditions[:etag] = list.scan(Calligraphy::ETAG_IF_REGEX).flatten[0]
84
+ def resource_client_nonce(_method)
85
+ @resource.client_nonce = client_nonce
105
86
  end
106
87
 
107
- conditions
108
- end
88
+ def client_nonce
89
+ auth_header = request.headers['HTTP_AUTHORIZATION']
90
+ digest = ::ActionController::HttpAuthentication::Digest
109
91
 
110
- def evaluate_preconditions(conditions)
111
- conditions_met = true
112
- target = if conditions[:resource]
113
- @resource_class.new(
114
- resource: conditions[:resource],
115
- mount: @resource.mount_point
116
- )
117
- else
118
- @resource
92
+ auth = digest.decode_credentials auth_header
93
+ auth[:cnonce]
119
94
  end
120
95
 
121
- if conditions[:lock_token]
122
- if target.locked?
123
- conditions_met = false unless target.lock_tokens&.include? conditions[:lock_token]
96
+ def send_response(status:, body: nil)
97
+ if body.nil?
98
+ head status
124
99
  else
125
- conditions_met = false if target.locked_to_user? request.headers
100
+ render body: body, status: status
126
101
  end
127
102
  end
128
-
129
- if conditions[:etag]
130
- validators = [@resource.etag, ""]
131
- conditions_met = false unless validate_etag validators, conditions[:etag]
132
- end
133
-
134
- conditions_met = false if conditions[:dav_no_lock]
135
- conditions_met
136
- end
137
-
138
- def validate_etag(etag_validators, validate_against)
139
- cache_key = ActiveSupport::Cache.expand_cache_key etag_validators
140
- "W/\"#{Digest::MD5.hexdigest(cache_key)}\"" == validate_against
141
- end
142
-
143
- def web_dav_request
144
- { headers: request.headers, request: request, resource: @resource, response: response }
145
- end
146
-
147
- def options
148
- response.headers['DAV'] = '1, 2, 3'
149
- :ok
150
- end
151
-
152
- def get(head: false)
153
- fresh_when(@resource, etag: @resource.etag) if @resource.readable?
154
-
155
- Calligraphy::Get.new(web_dav_request).request(head: head)
156
- end
157
-
158
- def put
159
- Calligraphy::Put.new(web_dav_request).request
160
- end
161
-
162
- def delete
163
- Calligraphy::Delete.new(web_dav_request).request
164
- end
165
-
166
- def copy
167
- Calligraphy::Copy.new(web_dav_request).request
168
- end
169
-
170
- def move
171
- Calligraphy::Move.new(web_dav_request).request
172
- end
173
-
174
- def mkcol
175
- Calligraphy::Mkcol.new(web_dav_request).request
176
- end
177
-
178
- def propfind
179
- Calligraphy::Propfind.new(web_dav_request).request
180
- end
181
-
182
- def proppatch
183
- Calligraphy::Proppatch.new(web_dav_request).request
184
- end
185
-
186
- def lock
187
- Calligraphy::Lock.new(web_dav_request).request
188
- end
189
-
190
- def unlock
191
- Calligraphy::Unlock.new(web_dav_request).request
192
- end
193
-
194
- def send_response(status:, body: nil)
195
- if body.nil?
196
- head status
197
- else
198
- render body: body, status: status
199
- end
200
- end
201
-
202
- def set_resource_client_nonce(method)
203
- @resource.client_nonce = get_client_nonce
204
- end
205
-
206
- def get_client_nonce
207
- auth_header = request.headers["HTTP_AUTHORIZATION"]
208
-
209
- auth = ::ActionController::HttpAuthentication::Digest.decode_credentials auth_header
210
- auth[:cnonce]
211
103
  end
212
104
  end
213
105
  end
@@ -1,13 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pstore'
2
4
 
3
5
  module Calligraphy
6
+ # Resource responsible for writing and deleting directories and files to disk.
4
7
  class FileResource < Resource
8
+ DAV_PROPERTY_METHODS = %w[
9
+ creationdate displayname getcontentlanguage getcontentlength
10
+ getcontenttype getetag getlastmodified lockdiscovery resourcetype
11
+ supportedlock
12
+ ].freeze
13
+
5
14
  include Calligraphy::Utils
6
15
 
16
+ #:nodoc:
7
17
  def initialize(resource: nil, req: nil, mount: nil, root_dir: Dir.pwd)
8
18
  super
9
19
 
10
- @root_dir = root_dir || Dir.pwd
11
20
  @src_path = join_paths @root_dir, @request_path
12
21
 
13
22
  if exists?
@@ -19,90 +28,97 @@ module Calligraphy
19
28
  set_ancestors
20
29
  end
21
30
 
31
+ # Responsible for returning a boolean value indicating if an ancestor
32
+ # exists for the resource.
33
+ #
34
+ # Used in COPY and MKCOL requests.
22
35
  def ancestor_exist?
23
36
  File.exist? @ancestor_path
24
37
  end
25
38
 
26
- def can_copy?(options)
27
- copy_options = { can_copy: false, ancestor_exist: false, locked: false }
39
+ # Responsible for returning a boolean value indicating if the resource
40
+ # is a collection.
41
+ #
42
+ # Used in DELETE, MKCOL, MOVE, and PUT requests.
43
+ def collection?
44
+ File.directory? @src_path
45
+ end
28
46
 
29
- overwrite = is_true? options[:overwrite]
30
- destination = options[:destination].tap { |s| s.slice! @mount_point }
31
- copy_options[:ancestor_exist] = File.exist? parent_path(destination)
47
+ # Responsible for returning a hash with keys indicating if the resource
48
+ # can be copied, if an ancestor exists, or if the copy destinatin is
49
+ # locked.
50
+ #
51
+ # Return hash should contain `can_copy`, `ancestor_exist`, and `locked`
52
+ # keys with boolean values.
53
+ #
54
+ # Used in COPY and MOVE (which inherits from COPY) requests.
55
+ def copy_options(options)
56
+ copy_options = { can_copy: false, ancestor_exist: false, locked: false }
32
57
 
58
+ destination = copy_destination options
33
59
  to_path = join_paths @root_dir, destination
34
- to_path_exist = File.exist? to_path
35
-
36
- copy_options[:locked] = if to_path_exist
37
- if destination_locked? to_path
38
- true
39
- else
40
- to_path_parent = split_and_pop(path: to_path).join '/'
41
- common_ancestor = common_path_ancestors(to_path, @ancestors).first
42
- to_path_ancestors = ancestors_from_path_to_ancestor to_path, common_ancestor
43
-
44
- locking_ancestor? to_path_parent, to_path_ancestors
45
- end
46
- else
47
- false
48
- end
49
-
50
- if copy_options[:ancestor_exist]
51
- if !overwrite && to_path_exist
52
- copy_options[:can_copy] = false
53
- else
54
- copy_options[:can_copy] = true
55
- end
56
- end
60
+ to_path_exists = File.exist? to_path
57
61
 
62
+ copy_options[:ancestor_exist] = File.exist? parent_path destination
63
+ copy_options[:locked] = can_copy_locked_option to_path, to_path_exists
64
+ copy_options = can_copy_option copy_options, options, to_path_exists
58
65
  copy_options
59
66
  end
60
67
 
61
- def collection?
62
- File.directory? @src_path
63
- end
64
-
68
+ # Responsible for creating a duplicate of the resource in
69
+ # `options[:destination]` (see section 9.8 of RFC4918).
70
+ #
71
+ # Used in COPY and MOVE (which inherits from COPY) requests.
65
72
  def copy(options)
66
- destination = options[:destination].tap { |s| s.slice! @mount_point }
67
- preserve_existing = is_false? options[:overwrite]
68
-
73
+ destination = copy_destination options
69
74
  to_path = join_paths @root_dir, destination
70
75
  to_path_exists = File.exist? to_path
71
76
 
72
- if collection?
73
- FileUtils.cp_r @src_path, to_path, preserve: preserve_existing
74
- else
75
- FileUtils.cp @src_path, to_path, preserve: preserve_existing
76
- end
77
-
78
- if store_exist? && preserve_existing
79
- dest_store_path = collection? ? "#{to_path}/#{@name}" : to_path
80
- dest_store_path += ".pstore"
77
+ preserve_existing = false? options[:overwrite]
81
78
 
82
- FileUtils.cp @store_path, dest_store_path, preserve: preserve_existing
83
- end
79
+ copy_resource_to_path to_path, preserve_existing
80
+ copy_pstore_to_path to_path, preserve_existing
84
81
 
85
82
  to_path_exists
86
83
  end
87
84
 
85
+ # Responsible for creating a new collection based on the resource (see
86
+ # section 9.3 of RFC4918).
87
+ #
88
+ # Used in MKCOL requests.
88
89
  def create_collection
89
90
  Dir.mkdir @src_path
90
91
  end
91
92
 
93
+ # Responsible for deleting a resource collection (see section 9.6 of
94
+ # RFC4918).
95
+ #
96
+ # Used in DELETE and MOVE requests.
92
97
  def delete_collection
93
98
  FileUtils.rm_r @src_path
94
99
  FileUtils.rm_r @store_path if store_exist?
95
100
  end
96
101
 
102
+ # Responsible for returning unique identifier used to create an etag.
103
+ #
104
+ # Used in precondition validation, as well as GET, HEAD, and PROPFIND
105
+ # requests.
97
106
  def etag
98
107
  [@updated_at.to_i, @stats[:inode], @stats[:size]].join('-').to_s
99
108
  end
100
109
 
110
+ # Responsible for indicating if the resource already exists.
111
+ #
112
+ # Used in DELETE, LOCK, MKCOL, and MOVE requests.
101
113
  def exists?
102
114
  File.exist? @src_path
103
115
  end
104
116
 
105
- def lock(nodes, depth=INFINITY)
117
+ # Responsible for creating a lock on the resource (see section 9.10 of
118
+ # RFC4918).
119
+ #
120
+ # Used in LOCK requests.
121
+ def lock(nodes, depth = 'infinity')
106
122
  properties = {}
107
123
 
108
124
  nodes.each do |node|
@@ -110,30 +126,32 @@ module Calligraphy
110
126
  properties[node.name.to_sym] = node
111
127
  end
112
128
 
113
- unless exists?
114
- write ''
115
- @name = File.basename @src_path
116
- init_pstore
117
- end
129
+ create_blank_file unless exists?
118
130
 
119
131
  create_lock properties, depth
132
+ fetch_lock_info
120
133
  end
121
134
 
135
+ # Responsible for indicating if a resource lock is exclusive.
136
+ #
137
+ # Used in LOCK requests.
122
138
  def lock_is_exclusive?
123
139
  lockscope == 'exclusive'
124
140
  end
125
141
 
126
- def lock_tokens
127
- get_lock_info
128
- @lock_info&.each { |x| x }&.map { |k, v| k[:locktoken].children[0].text }
129
- end
130
-
142
+ # Responsible for indicating if a resource is current locked.
143
+ #
144
+ # Used in LOCK requests.
131
145
  def locked?
132
- get_lock_info
146
+ fetch_lock_info
147
+
133
148
  obj_exists_and_is_not_type? obj: @lock_info, type: []
134
149
  end
135
150
 
136
- def locked_to_user?(headers=nil)
151
+ # Responsible for indicating if a resource is locked to the current user.
152
+ #
153
+ # Used in DELETE, LOCK, MOVE, PROPPATCH, and PUT requests.
154
+ def locked_to_user?(headers = nil)
137
155
  if locked?
138
156
  !can_unlock? headers
139
157
  else
@@ -141,6 +159,10 @@ module Calligraphy
141
159
  end
142
160
  end
143
161
 
162
+ # Responsible for handling the retrieval of properties defined on the
163
+ # resource (see section 9.1 of RFC4918).
164
+ #
165
+ # Used in PROPFIND requests.
144
166
  def propfind(nodes)
145
167
  properties = { found: [], not_found: [] }
146
168
 
@@ -150,96 +172,73 @@ module Calligraphy
150
172
 
151
173
  value = get_property prop
152
174
 
153
- if value.nil?
154
- properties[:not_found].push prop
155
- elsif value.is_a? Hash
156
- value.each_key do |key|
157
- properties[:found].push value[key]
158
- end
159
- else
160
- properties[:found].push value
161
- end
175
+ update_found_properties properties, prop, value
162
176
  end
163
177
  end
164
178
 
165
179
  properties
166
180
  end
167
181
 
182
+ # Responsible for handling the addition and/or removal of properties
183
+ # defined on the resource through a PROPPATCH request (see section 9.2 of
184
+ # RFC4918).
185
+ #
186
+ # Used in PROPPATCH requests.
168
187
  def proppatch(nodes)
169
188
  actions = { set: [], remove: [] }
170
189
 
171
190
  @store.transaction do
172
191
  @store[:properties] = {} unless @store[:properties].is_a? Hash
173
192
 
174
- nodes.each do |node|
175
- if node.name == 'set'
176
- node.children.each do |prop|
177
- prop.children.each do |property|
178
- prop_sym = property.name.to_sym
179
- node = Calligraphy::XML::Node.new property
180
-
181
- if @store[:properties][prop_sym]
182
- if @store[:properties][prop_sym].is_a? Array
183
- unless matching_namespace? @store[:properties][prop_sym], node
184
- @store[:properties][prop_sym].push node
185
- end
186
- else
187
- if !same_namespace? @store[:properties][prop_sym], node
188
- @store[:properties][prop_sym] = [@store[:properties][prop_sym]]
189
- @store[:properties][prop_sym].push node
190
- else
191
- @store[:properties][prop_sym] = node
192
- end
193
- end
194
- else
195
- @store[:properties][prop_sym] = node
196
- end
197
-
198
- actions[:set].push property
199
- end
200
- end
201
- elsif node.name == 'remove'
202
- node.children.each do |prop|
203
- prop.children.each do |property|
204
- @store[:properties].delete property.name.to_sym
205
-
206
- actions[:remove].push property
207
- end
208
- end
209
- end
210
- end
193
+ add_remove_properties nodes, actions
211
194
  end
212
195
 
213
196
  get_custom_property nil
214
197
  actions
215
198
  end
216
199
 
200
+ # Responsible for setting and returning the contents of a resource
201
+ # if it is readable (see section 9.4 of RFC4918).
202
+ #
203
+ # Used in GET requests.
217
204
  def read
218
205
  @contents ||= File.read @src_path if readable?
219
206
  end
220
207
 
208
+ # Responsible for refreshing locks (see section 9.10.2 of RFC4918).
209
+ #
210
+ # Used in LOCK requests.
221
211
  def refresh_lock
222
212
  if locked?
223
213
  @store.transaction do
224
214
  @store[:lockdiscovery][-1][:timeout] = timeout_node
225
215
  end
226
216
 
227
- get_lock_info
217
+ fetch_lock_info
228
218
  else
229
219
  refresh_ancestor_locks @ancestor_path, @ancestors.dup
230
220
  end
231
221
  end
232
222
 
223
+ # Responsible for unlocking a resource lock (see section 9.11 of RFC4918).
224
+ #
225
+ # Used in UNLOCK requests.
233
226
  def unlock(token)
234
227
  if lock_tokens.include? token
235
228
  remove_lock token
229
+ @lock_info = nil
230
+
236
231
  :no_content
237
232
  else
238
233
  :forbidden
239
234
  end
240
235
  end
241
236
 
242
- def write(contents=@request_body.to_s)
237
+ # Responsible for writing contents to a resource (see section 9.7 of
238
+ # RFC4918).
239
+ #
240
+ # Used in PUT requests.
241
+ def write(contents = @request_body.to_s)
243
242
  @contents = contents
244
243
 
245
244
  File.open(@src_path, 'w') do |file|
@@ -261,7 +260,7 @@ module Calligraphy
261
260
  @stats = {
262
261
  created_at: file_stats.ctime,
263
262
  inode: file_stats.ino,
264
- size: file_stats.size,
263
+ size: file_stats.size
265
264
  }
266
265
  @updated_at = file_stats.mtime
267
266
  end
@@ -275,6 +274,34 @@ module Calligraphy
275
274
  join_paths @root_dir, split_and_pop(path: path)
276
275
  end
277
276
 
277
+ def copy_destination(options)
278
+ options[:destination].tap { |s| s.slice! @mount_point }
279
+ end
280
+
281
+ def can_copy_locked_option(to_path, to_path_exists)
282
+ return false unless to_path_exists
283
+ return true if destination_locked? to_path
284
+
285
+ to_path_parent = split_and_pop(path: to_path).join '/'
286
+ common_ancestor = common_path_ancestors(to_path, @ancestors).first
287
+ to_path_ancestors = ancestors_from_path_to_ancestor(to_path,
288
+ common_ancestor)
289
+
290
+ locking_ancestor? to_path_parent, to_path_ancestors
291
+ end
292
+
293
+ def can_copy_option(copy_options, options, to_path_exists)
294
+ return copy_options unless copy_options[:ancestor_exist]
295
+
296
+ copy_options[:can_copy] = if false?(options[:overwrite]) && to_path_exists
297
+ false
298
+ else
299
+ true
300
+ end
301
+
302
+ copy_options
303
+ end
304
+
278
305
  def destination_locked?(path)
279
306
  store = PStore.new "#{path}.pstore"
280
307
  lock = store.transaction(true) { store[:lockdiscovery] }
@@ -295,36 +322,51 @@ module Calligraphy
295
322
  path = split_and_pop path: path
296
323
  ancestors = []
297
324
 
298
- until path.last == stop_at_ancestor
299
- ancestors.push path.pop
300
- end
301
-
325
+ ancestors.push path.pop until path.last == stop_at_ancestor
302
326
  ancestors.push stop_at_ancestor
303
327
  ancestors.reverse
304
328
  end
305
329
 
330
+ def copy_resource_to_path(to_path, preserve_existing)
331
+ if collection?
332
+ FileUtils.cp_r @src_path, to_path, preserve: preserve_existing
333
+ else
334
+ FileUtils.cp @src_path, to_path, preserve: preserve_existing
335
+ end
336
+ end
337
+
338
+ def copy_pstore_to_path(to_path, preserve_existing)
339
+ return unless store_exist? && preserve_existing
340
+
341
+ dest_store_path = collection? ? "#{to_path}/#{@name}" : to_path
342
+ dest_store_path += '.pstore'
343
+
344
+ FileUtils.cp @store_path, dest_store_path, preserve: preserve_existing
345
+ end
346
+
306
347
  def store_exist?
307
348
  File.exist? @store_path
308
349
  end
309
350
 
351
+ def create_blank_file
352
+ write ''
353
+ @name = File.basename @src_path
354
+ init_pstore
355
+ end
356
+
310
357
  def create_lock(properties, depth)
311
358
  @store.transaction do
312
359
  @store[:lockcreator] = client_nonce
313
- @store[:lockdiscovery] = [] unless @store[:lockdiscovery].is_a? Array
314
360
  @store[:lockdepth] = depth
361
+ @store[:lockdiscovery] = [] unless @store[:lockdiscovery].is_a? Array
315
362
 
316
- activelock = {}
317
- activelock[:locktoken] = create_lock_token
318
- activelock[:timeout] = timeout_node
319
-
320
- properties.each_key do |prop|
321
- activelock[prop] = Calligraphy::XML::Node.new properties[prop]
322
- end
363
+ @store[:lockdiscovery].push({}.tap do |activelock|
364
+ activelock[:locktoken] = create_lock_token
365
+ activelock[:timeout] = timeout_node
323
366
 
324
- @store[:lockdiscovery].push activelock
367
+ add_lock_properties activelock, properties
368
+ end)
325
369
  end
326
-
327
- get_lock_info
328
370
  end
329
371
 
330
372
  def create_lock_token
@@ -346,7 +388,13 @@ module Calligraphy
346
388
  end
347
389
  end
348
390
 
349
- def get_lock_info
391
+ def add_lock_properties(activelock, properties)
392
+ properties.each_key do |prop|
393
+ activelock[prop] = Calligraphy::XML::Node.new properties[prop]
394
+ end
395
+ end
396
+
397
+ def fetch_lock_info
350
398
  return nil if @store.nil?
351
399
 
352
400
  @lock_info = @store.transaction(true) { @store[:lockdiscovery] }
@@ -357,94 +405,153 @@ module Calligraphy
357
405
  @lock_info[-1][:lockscope].children[0].name
358
406
  end
359
407
 
360
- def can_unlock?(headers=nil)
408
+ def can_unlock?(headers = nil)
361
409
  token = unless headers.nil?
362
- extract_lock_token(headers['If']) if headers['If']
363
- end
410
+ extract_lock_token(headers['If']) if headers['If']
411
+ end
364
412
 
365
413
  lock_tokens.include? token
366
414
  end
367
415
 
368
- def locking_ancestor?(ancestor_path, ancestors, headers=nil)
416
+ def lock_tokens
417
+ fetch_lock_info
418
+ @lock_info&.each { |x| x }&.map { |k| k[:locktoken].children[0].text }
419
+ end
420
+
421
+ def locking_ancestor?(ancestor_path, ancestors, headers = nil)
422
+ ancestor_info = ancestor_lock_info headers
369
423
  ancestor_store_path = "#{ancestor_path}/#{ancestors[-1]}.pstore"
370
- check_lock_creator = Calligraphy.enable_digest_authentication
371
- blocking_lock = false
372
- unlockable = true
373
424
 
374
425
  ancestors.pop
375
426
 
376
- if File.exist? ancestor_store_path
377
- ancestor_store = PStore.new ancestor_store_path
427
+ check_for_ancestor ancestor_info, ancestor_store_path
378
428
 
379
- ancestor_lock = nil
380
- ancestor_lock_creator = nil
381
- ancestor_lock_depth = nil
429
+ if ancestor_info[:blocking] || ancestors.empty?
430
+ assign_locking_ancestor ancestor_info
382
431
 
383
- ancestor_store.transaction(true) do
384
- ancestor_lock = ancestor_store[:lockdiscovery]
385
- ancestor_lock_depth = ancestor_store[:lockdepth]
432
+ return ancestor_info[:unlockable] ? false : true
433
+ end
386
434
 
387
- if check_lock_creator
388
- ancestor_lock_creator = ancestor_store[:lockcreator]
389
- end
390
- end
435
+ next_ancestor = split_and_pop(path: ancestor_path).join '/'
436
+ locking_ancestor? next_ancestor, ancestors, ancestor_info[:headers]
437
+ end
438
+
439
+ def ancestor_lock_info(headers)
440
+ {
441
+ blocking: false,
442
+ check_creator: Calligraphy.enable_digest_authentication,
443
+ creator: nil,
444
+ depth: nil,
445
+ headers: headers || nil,
446
+ lock: nil,
447
+ unlockable: true
448
+ }
449
+ end
391
450
 
392
- blocking_lock = obj_exists_and_is_not_type? obj: ancestor_lock, type: []
451
+ def check_for_ancestor(ancestor_info, store_path)
452
+ return unless File.exist? store_path
453
+ ancestor_lock_from_store ancestor_info, store_path
393
454
 
394
- if blocking_lock
395
- token = unless headers.nil?
396
- extract_lock_token(headers['If']) if headers['If']
397
- end
455
+ ancestor_info[:blocking] = obj_exists_and_is_not_type?(
456
+ obj: ancestor_info[:lock],
457
+ type: []
458
+ )
398
459
 
399
- ancestor_lock_tokens = ancestor_lock
400
- .each { |x| x }
401
- .map { |k, v| k[:locktoken].children[0].text }
460
+ blocking_lock_unlockable? ancestor_info if ancestor_info[:blocking]
461
+ end
462
+
463
+ def ancestor_lock_from_store(lock_info, store_path)
464
+ ancestor_store = PStore.new store_path
465
+
466
+ ancestor_store.transaction(true) do
467
+ lock_info[:lock] = ancestor_store[:lockdiscovery]
468
+ lock_info[:depth] = ancestor_store[:lockdepth]
402
469
 
403
- unlockable = ancestor_lock_tokens.include?(token) ||
404
- (check_lock_creator && (ancestor_lock_creator == client_nonce))
470
+ if lock_info[:check_creator]
471
+ lock_info[:creator] = ancestor_store[:lockcreator]
405
472
  end
406
473
  end
474
+ end
407
475
 
408
- if blocking_lock || ancestors.empty?
409
- @locking_ancestor = {
410
- depth: ancestor_lock_depth,
411
- info: ancestor_lock
412
- }
476
+ def blocking_lock_unlockable?(lock_info)
477
+ headers = lock_info[:headers]
413
478
 
414
- return unlockable ? false : true
415
- end
479
+ token = unless headers.nil?
480
+ extract_lock_token(headers['If']) if headers['If']
481
+ end
416
482
 
417
- next_ancestor = split_and_pop(path: ancestor_path).join '/'
418
- locking_ancestor? next_ancestor, ancestors, headers
483
+ ancestor_tokens = lock_info[:lock]
484
+
485
+ lock_info[:unlockable] =
486
+ ancestor_tokens.include?(token) ||
487
+ (lock_info[:check_creator] && (lock_info[:creator] == client_nonce))
488
+ end
489
+
490
+ def ancestor_lock_tokens(lock_info)
491
+ lock_info[:lock].each { |x| x }.map { |k| k[:locktoken].children[0].text }
492
+ end
493
+
494
+ def assign_locking_ancestor(ancestor_info)
495
+ @locking_ancestor = {
496
+ depth: ancestor_info[:depth],
497
+ info: ancestor_info[:lock]
498
+ }
419
499
  end
420
500
 
421
501
  def get_property(prop)
422
502
  case prop.name
423
- when 'creationdate'
424
- prop.content = @stats[:created_at]
425
- when 'displayname'
426
- prop.content = @name
427
- when 'getcontentlanguage'
428
- prop.content = nil
429
- when 'getcontentlength'
430
- prop.content = @stats[:size]
431
- when 'getcontenttype'
432
- prop.content = nil
433
- when 'getetag'
434
- prop.content = nil
435
- when 'getlastmodified'
436
- prop.content = @updated_at
437
503
  when 'lockdiscovery'
438
- return get_lock_info
439
- when 'resourcetype'
440
- prop.content = 'collection'
441
- when 'supportedlock'
442
- prop.content = nil
504
+ fetch_lock_info
505
+ when *DAV_PROPERTY_METHODS
506
+ prop.content = send prop.name
507
+ prop
443
508
  else
444
- return get_custom_property prop.name
509
+ get_custom_property prop.name
445
510
  end
511
+ end
512
+
513
+ def creationdate
514
+ @stats[:created_at]
515
+ end
516
+
517
+ def displayname
518
+ get_custom_property(:displayname) || @name
519
+ end
520
+
521
+ def getcontentlanguage
522
+ get_custom_property :contentlanguage
523
+ end
524
+
525
+ def getcontentlength
526
+ @stats[:size]
527
+ end
528
+
529
+ def getcontenttype
530
+ get_custom_property :contenttype
531
+ end
532
+
533
+ def getetag
534
+ cache_key = ActiveSupport::Cache.expand_cache_key [@resource.etag, '']
535
+ "W/\"#{Digest::MD5.hexdigest(cache_key)}\""
536
+ end
537
+
538
+ def getlastmodified
539
+ @updated_at
540
+ end
541
+
542
+ def lockdiscovery
543
+ fetch_lock_info
544
+ end
446
545
 
447
- prop
546
+ def resourcetype
547
+ 'collection' if collection?
548
+ end
549
+
550
+ def supportedlock
551
+ exclusive_write = lockentry_hash('exclusive', 'write')
552
+ shared_write = lockentry_hash('shared', 'write')
553
+
554
+ JSON.generate [exclusive_write, shared_write]
448
555
  end
449
556
 
450
557
  def get_custom_property(prop)
@@ -452,12 +559,85 @@ module Calligraphy
452
559
  @store_properties[prop.to_sym] unless @store_properties.nil? || prop.nil?
453
560
  end
454
561
 
455
- def matching_namespace?(node_arr, node)
456
- node_arr.select { |x| x.namespace.href == node.namespace.href }.length > 0
562
+ def update_found_properties(properties, prop, value)
563
+ if value.nil?
564
+ properties[:not_found].push prop
565
+ elsif value.is_a? Hash
566
+ value.each_key do |key|
567
+ properties[:found].push value[key]
568
+ end
569
+ else
570
+ properties[:found].push value
571
+ end
572
+ end
573
+
574
+ def add_remove_properties(nodes, actions)
575
+ nodes.each do |node|
576
+ if node.name == 'set'
577
+ add_properties node, actions
578
+ elsif node.name == 'remove'
579
+ remove_properties node, actions
580
+ end
581
+ end
582
+ end
583
+
584
+ def add_properties(node, actions)
585
+ node.children.each do |prop|
586
+ prop.children.each do |property|
587
+ node = Calligraphy::XML::Node.new property
588
+ prop_sym = property.name.to_sym
589
+
590
+ store_property_node node, prop_sym
591
+
592
+ actions[:set].push property
593
+ end
594
+ end
595
+ end
596
+
597
+ def store_property_node(node, prop)
598
+ # Property does not exist yet so we can just store the property node.
599
+ return @store[:properties][prop] = node unless @store[:properties][prop]
600
+
601
+ if @store[:properties][prop].is_a? Array
602
+ store_mismatch_namespace_property_node node, prop
603
+ elsif same_namespace? @store[:properties][prop], node
604
+ # If stored property and node have the same namespace, we can just
605
+ # overwrite the previously stored property node.
606
+ @store[:properties][prop] = node
607
+ else
608
+ # If stored property and node DO NOT have the same namespace, create
609
+ # an array for the stored property and push the new property node.
610
+ store_mismatch_namespace_property_nodes node, prop
611
+ end
612
+ end
613
+
614
+ def store_mismatch_namespace_property_node(node, prop)
615
+ node_arr = @store[:properties][prop]
616
+
617
+ namespace_mismatch = node_arr.select do |x|
618
+ x.namespace.href == node.namespace.href
619
+ end.length.positive?
620
+
621
+ @store[:properties][prop].push node unless namespace_mismatch
457
622
  end
458
623
 
459
624
  def same_namespace?(node1, node2)
460
- node1.namespace.href == node2.namespace.href
625
+ node1.namespace&.href == node2.namespace&.href
626
+ end
627
+
628
+ def store_mismatch_namespace_property_nodes(node, prop)
629
+ @store[:properties][prop] = [@store[:properties][prop]]
630
+ @store[:properties][prop].push node
631
+ end
632
+
633
+ def remove_properties(node, actions)
634
+ node.children.each do |prop|
635
+ prop.children.each do |property|
636
+ @store[:properties].delete property.name.to_sym
637
+
638
+ actions[:remove].push property
639
+ end
640
+ end
461
641
  end
462
642
 
463
643
  def refresh_ancestor_locks(ancestor_path, ancestors)
@@ -465,11 +645,7 @@ module Calligraphy
465
645
  ancestors.pop
466
646
 
467
647
  if File.exist? ancestor_store_path
468
- ancestor_store = PStore.new ancestor_store_path
469
- ancestor_lock = ancestor_store.transaction do
470
- ancestor_store[:lockdiscovery][-1][:timeout] = timeout_node
471
- ancestor_store[:lockdiscovery]
472
- end
648
+ ancestor_lock = refresh_ancestor_lock ancestor_store_path
473
649
 
474
650
  return map_array_of_hashes ancestor_lock
475
651
  end
@@ -478,6 +654,15 @@ module Calligraphy
478
654
  refresh_ancestor_locks next_ancestor, ancestors
479
655
  end
480
656
 
657
+ def refresh_ancestor_lock(ancestor_store_path)
658
+ ancestor_store = PStore.new ancestor_store_path
659
+
660
+ ancestor_store.transaction do
661
+ ancestor_store[:lockdiscovery][-1][:timeout] = timeout_node
662
+ ancestor_store[:lockdiscovery]
663
+ end
664
+ end
665
+
481
666
  def remove_lock(token)
482
667
  @store.transaction do
483
668
  @store.delete :lockcreator
@@ -490,8 +675,6 @@ module Calligraphy
490
675
  end
491
676
  end
492
677
  end
493
-
494
- @lock_info = nil
495
678
  end
496
679
  end
497
680
  end