calligraphy 0.2.1 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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