calligraphy 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +64 -0
- data/lib/calligraphy.rb +58 -0
- data/lib/calligraphy/copy.rb +37 -0
- data/lib/calligraphy/delete.rb +17 -0
- data/lib/calligraphy/file_resource.rb +487 -0
- data/lib/calligraphy/get.rb +12 -0
- data/lib/calligraphy/lock.rb +52 -0
- data/lib/calligraphy/mkcol.rb +20 -0
- data/lib/calligraphy/move.rb +31 -0
- data/lib/calligraphy/propfind.rb +18 -0
- data/lib/calligraphy/proppatch.rb +20 -0
- data/lib/calligraphy/put.rb +12 -0
- data/lib/calligraphy/rails/mapper.rb +120 -0
- data/lib/calligraphy/rails/web_dav_requests_controller.rb +208 -0
- data/lib/calligraphy/resource.rb +93 -0
- data/lib/calligraphy/unlock.rb +15 -0
- data/lib/calligraphy/utils.rb +38 -0
- data/lib/calligraphy/version.rb +3 -0
- data/lib/calligraphy/web_dav_request.rb +31 -0
- data/lib/calligraphy/xml/builder.rb +147 -0
- data/lib/calligraphy/xml/namespace.rb +10 -0
- data/lib/calligraphy/xml/node.rb +23 -0
- data/lib/calligraphy/xml/utils.rb +18 -0
- data/spec/spec_helper.rb +46 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8ca4ef1090301769811a4e4e9582826338ebcc7d
|
4
|
+
data.tar.gz: 6453585dca9f697419f6f5e8b4c73e2f664d5b10
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5055f8ed11cda5dcc4bdaf6d1e821b989fd57f2e2869e9e316a8a9ed49e40a6da5962614dac6a8f6448d155a5de743f9ca3360aaa8700af736c7e989a8a66f91
|
7
|
+
data.tar.gz: 7de86141b36bd5bef0bd915a8eed30c524568c1e409bf0ecdc1bf5d7f41a22fa887cbbc69964ef404f8f5f0de094474cb4d5eea10e68278d20d1e573e8dc90f5
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Brandon Robins
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# Calligraphy
|
2
|
+
|
3
|
+
Calligraphy is a Web Distributed Authoring and Versioning (WebDAV) solution for Rails that:
|
4
|
+
|
5
|
+
* Provides a framework for handling WebDAV requests (e.g. `PROPFIND`, `PROPPATCH`)
|
6
|
+
* Allows you to extend WedDAV functionality to any type of resource
|
7
|
+
* Passes all of the [Litmus](https://github.com/eanlain/litmus) tests (using `Calligraphy::FileResource` and digest authentication)
|
8
|
+
|
9
|
+
## Getting Started
|
10
|
+
|
11
|
+
Add the following line to your Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'calligraphy', :git => 'https://github.com/eanlain/calligraphy'
|
15
|
+
```
|
16
|
+
|
17
|
+
Then run `bundle install`
|
18
|
+
|
19
|
+
Next, set up a `calligraphy_resource` route in `config/routes.rb` with a `resource_class`.
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
calligraphy_resource :webdav, resource_class: Calligraphy::FileResource
|
23
|
+
```
|
24
|
+
|
25
|
+
The above will create a route, `/webdav` that will be able to handle the following HTTP request methods:
|
26
|
+
|
27
|
+
* `OPTIONS`
|
28
|
+
* `GET`
|
29
|
+
* `PUT`
|
30
|
+
* `DELETE`
|
31
|
+
* `COPY`
|
32
|
+
* `MOVE`
|
33
|
+
* `MKCOL`
|
34
|
+
* `PROPFIND`
|
35
|
+
* `PROPPATCH`
|
36
|
+
* `LOCK`
|
37
|
+
* `UNLOCK`
|
38
|
+
|
39
|
+
The routes will also use the `Calligraphy::FileResource`, enabling Rails to carry out WebDAV actions on files.
|
40
|
+
|
41
|
+
## Extensibility
|
42
|
+
|
43
|
+
The `Calligraphy::Resource` class exposes all the methods used in the various `Calligraphy::WebDavRequest` classes.
|
44
|
+
To create a custom resource, simply inherit from `Calligraphy::Resource` and redefine the public methods you'd like to customize.
|
45
|
+
|
46
|
+
For example, to create a `CalendarResource`:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
module Calligraphy
|
50
|
+
class CalendarResource < Resource
|
51
|
+
|
52
|
+
def propfind(nodes)
|
53
|
+
# custom implementation of propfind for CalendarResource
|
54
|
+
end
|
55
|
+
|
56
|
+
...
|
57
|
+
end
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
## License
|
62
|
+
|
63
|
+
Calligraphy is Copyright © 2017 Brandon Robins.
|
64
|
+
It is free software, and may be redistributed under the terms specified in the [LICENSE](/LICENSE) file.
|
data/lib/calligraphy.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'calligraphy/rails/mapper'
|
2
|
+
require 'calligraphy/rails/web_dav_requests_controller'
|
3
|
+
|
4
|
+
require 'calligraphy/xml/builder'
|
5
|
+
require 'calligraphy/xml/namespace'
|
6
|
+
require 'calligraphy/xml/node'
|
7
|
+
require 'calligraphy/xml/utils'
|
8
|
+
|
9
|
+
require 'calligraphy/utils'
|
10
|
+
require 'calligraphy/resource'
|
11
|
+
require 'calligraphy/file_resource'
|
12
|
+
|
13
|
+
require 'calligraphy/web_dav_request'
|
14
|
+
require 'calligraphy/copy'
|
15
|
+
require 'calligraphy/delete'
|
16
|
+
require 'calligraphy/get'
|
17
|
+
require 'calligraphy/lock'
|
18
|
+
require 'calligraphy/mkcol'
|
19
|
+
require 'calligraphy/move'
|
20
|
+
require 'calligraphy/propfind'
|
21
|
+
require 'calligraphy/proppatch'
|
22
|
+
require 'calligraphy/put'
|
23
|
+
require 'calligraphy/unlock'
|
24
|
+
|
25
|
+
module Calligraphy
|
26
|
+
DAV_NS = 'DAV:'
|
27
|
+
DAV_NO_LOCK_REGEX = /DAV:no-lock/i
|
28
|
+
DAV_NOT_NO_LOCK_REGEX = /Not\s+<DAV:no-lock>/i
|
29
|
+
ETAG_IF_REGEX = /\[(.+?)\]/
|
30
|
+
LOCK_TOKEN_ANGLE_REGEX = /[<>]/
|
31
|
+
LOCK_TOKEN_REGEX = /<(urn:uuid:.+?)>/
|
32
|
+
RESOURCE_REGEX = /^<+(.+?)>\s/
|
33
|
+
TAGGED_LIST_REGEX = /\)\s</
|
34
|
+
UNTAGGAGED_LIST_REGEX = /\)\s\(/
|
35
|
+
|
36
|
+
mattr_accessor :allowed_methods
|
37
|
+
@@allowed_methods = %w(
|
38
|
+
options head get put delete copy move mkcol propfind proppatch lock unlock
|
39
|
+
)
|
40
|
+
|
41
|
+
mattr_accessor :digest_password_procedure
|
42
|
+
@@digest_password_procedure = Proc.new { |x| 'changeme!' }
|
43
|
+
|
44
|
+
mattr_accessor :enable_digest_authentication
|
45
|
+
@@enable_digest_authentication = false
|
46
|
+
|
47
|
+
mattr_accessor :lock_timeout_period
|
48
|
+
@@lock_timeout_period = 24 * 60 * 60
|
49
|
+
|
50
|
+
mattr_accessor :web_dav_actions
|
51
|
+
@@web_dav_actions = %i(
|
52
|
+
options get put delete copy move mkcol propfind proppatch lock unlock
|
53
|
+
)
|
54
|
+
|
55
|
+
def self.configure
|
56
|
+
yield self
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Calligraphy
|
2
|
+
class Copy < WebDavRequest
|
3
|
+
def request
|
4
|
+
options = copy_move_options
|
5
|
+
can_copy = @resource.can_copy? options
|
6
|
+
|
7
|
+
if can_copy[:ancestor_exist]
|
8
|
+
return :precondition_failed
|
9
|
+
else
|
10
|
+
return :conflict
|
11
|
+
end unless can_copy[:can_copy]
|
12
|
+
|
13
|
+
return :locked if can_copy[:locked]
|
14
|
+
|
15
|
+
overwritten = @resource.copy options
|
16
|
+
return overwritten ? :no_content : :created
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def copy_move_options
|
22
|
+
{
|
23
|
+
depth: @headers['Depth'],
|
24
|
+
destination: remove_trailing_slash(destination_header),
|
25
|
+
overwrite: @headers['Overwrite'] || true
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def destination_header
|
30
|
+
@headers['Destination'].split(@headers['Host'])[-1]
|
31
|
+
end
|
32
|
+
|
33
|
+
def remove_trailing_slash(input)
|
34
|
+
input[-1] == '/' ? input[0..-2] : input
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Calligraphy
|
2
|
+
class Delete < WebDavRequest
|
3
|
+
def request
|
4
|
+
return :locked if @resource.locked_to_user? @headers
|
5
|
+
|
6
|
+
if @resource.collection?
|
7
|
+
@resource.delete_collection
|
8
|
+
|
9
|
+
return :no_content
|
10
|
+
else
|
11
|
+
return :not_found unless @resource.exists?
|
12
|
+
end
|
13
|
+
|
14
|
+
return :no_content
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,487 @@
|
|
1
|
+
require 'pstore'
|
2
|
+
|
3
|
+
module Calligraphy
|
4
|
+
class FileResource < Resource
|
5
|
+
include Calligraphy::Utils
|
6
|
+
|
7
|
+
def initialize(resource: nil, req: nil, mount: nil, root_dir: Dir.pwd)
|
8
|
+
super
|
9
|
+
|
10
|
+
@root_dir = root_dir || Dir.pwd
|
11
|
+
@src_path = join_paths @root_dir, @request_path
|
12
|
+
|
13
|
+
if exists?
|
14
|
+
@name = File.basename @src_path
|
15
|
+
init_pstore
|
16
|
+
set_file_stats
|
17
|
+
end
|
18
|
+
|
19
|
+
set_ancestors
|
20
|
+
end
|
21
|
+
|
22
|
+
def ancestor_exist?
|
23
|
+
File.exist? @ancestor_path
|
24
|
+
end
|
25
|
+
|
26
|
+
def can_copy?(options)
|
27
|
+
copy_options = { can_copy: false, ancestor_exist: false, locked: false }
|
28
|
+
|
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)
|
32
|
+
|
33
|
+
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
|
57
|
+
|
58
|
+
copy_options
|
59
|
+
end
|
60
|
+
|
61
|
+
def collection?
|
62
|
+
File.directory? @src_path
|
63
|
+
end
|
64
|
+
|
65
|
+
def copy(options)
|
66
|
+
destination = options[:destination].tap { |s| s.slice! @mount_point }
|
67
|
+
preserve_existing = is_false? options[:overwrite]
|
68
|
+
|
69
|
+
to_path = join_paths @root_dir, destination
|
70
|
+
to_path_exists = File.exist? to_path
|
71
|
+
|
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"
|
81
|
+
|
82
|
+
FileUtils.cp @store_path, dest_store_path, preserve: preserve_existing
|
83
|
+
end
|
84
|
+
|
85
|
+
to_path_exists
|
86
|
+
end
|
87
|
+
|
88
|
+
def create_collection
|
89
|
+
Dir.mkdir @src_path
|
90
|
+
end
|
91
|
+
|
92
|
+
def delete_collection
|
93
|
+
FileUtils.rm_r @src_path
|
94
|
+
FileUtils.rm_r @store_path if store_exist?
|
95
|
+
end
|
96
|
+
|
97
|
+
def etag
|
98
|
+
[@updated_at.to_i, @stats[:inode], @stats[:size]].join('-').to_s
|
99
|
+
end
|
100
|
+
|
101
|
+
def exists?
|
102
|
+
File.exist? @src_path
|
103
|
+
end
|
104
|
+
|
105
|
+
def lock(nodes, depth='infinity')
|
106
|
+
properties = {}
|
107
|
+
|
108
|
+
nodes.each do |node|
|
109
|
+
next unless node.is_a? Nokogiri::XML::Element
|
110
|
+
properties[node.name.to_sym] = node
|
111
|
+
end
|
112
|
+
|
113
|
+
unless exists?
|
114
|
+
write ''
|
115
|
+
@name = File.basename @src_path
|
116
|
+
init_pstore
|
117
|
+
end
|
118
|
+
|
119
|
+
create_lock properties, depth
|
120
|
+
end
|
121
|
+
|
122
|
+
def lock_is_exclusive?
|
123
|
+
lockscope == 'exclusive'
|
124
|
+
end
|
125
|
+
|
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
|
+
|
131
|
+
def locked?
|
132
|
+
get_lock_info
|
133
|
+
obj_exists_and_is_not_type? obj: @lock_info, type: []
|
134
|
+
end
|
135
|
+
|
136
|
+
def locked_to_user?(headers=nil)
|
137
|
+
if locked?
|
138
|
+
!can_unlock? headers
|
139
|
+
else
|
140
|
+
locking_ancestor? @ancestor_path, @ancestors.dup, headers
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def propfind(nodes)
|
145
|
+
properties = { found: [], not_found: [] }
|
146
|
+
|
147
|
+
nodes.each do |node|
|
148
|
+
node.children.each do |prop|
|
149
|
+
next unless prop.is_a? Nokogiri::XML::Element
|
150
|
+
|
151
|
+
value = get_property prop
|
152
|
+
|
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
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
properties
|
166
|
+
end
|
167
|
+
|
168
|
+
def proppatch(nodes)
|
169
|
+
actions = { set: [], remove: [] }
|
170
|
+
|
171
|
+
@store.transaction do
|
172
|
+
@store[:properties] = {} unless @store[:properties].is_a? Hash
|
173
|
+
|
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
|
211
|
+
end
|
212
|
+
|
213
|
+
get_custom_property nil
|
214
|
+
actions
|
215
|
+
end
|
216
|
+
|
217
|
+
def read
|
218
|
+
@contents ||= File.read @src_path if readable?
|
219
|
+
end
|
220
|
+
|
221
|
+
def refresh_lock
|
222
|
+
if locked?
|
223
|
+
@store.transaction do
|
224
|
+
@store[:lockdiscovery][-1][:timeout] = timeout_node
|
225
|
+
end
|
226
|
+
|
227
|
+
get_lock_info
|
228
|
+
else
|
229
|
+
refresh_ancestor_locks @ancestor_path, @ancestors.dup
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def unlock(token)
|
234
|
+
if lock_tokens.include? token
|
235
|
+
remove_lock token
|
236
|
+
:no_content
|
237
|
+
else
|
238
|
+
:forbidden
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def write(contents=@request_body.to_s)
|
243
|
+
@contents = contents
|
244
|
+
|
245
|
+
File.open(@src_path, 'w') do |file|
|
246
|
+
file.write @contents
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
private
|
251
|
+
|
252
|
+
def init_pstore
|
253
|
+
pstore_path = collection? ? "#{@src_path}/#{@name}" : @src_path
|
254
|
+
@store = PStore.new "#{pstore_path}.pstore"
|
255
|
+
@store_path = @store.instance_variable_get :@filename
|
256
|
+
end
|
257
|
+
|
258
|
+
def set_file_stats
|
259
|
+
file_stats = File.stat @src_path
|
260
|
+
|
261
|
+
@stats = {
|
262
|
+
created_at: file_stats.ctime,
|
263
|
+
inode: file_stats.ino,
|
264
|
+
size: file_stats.size,
|
265
|
+
}
|
266
|
+
@updated_at = file_stats.mtime
|
267
|
+
end
|
268
|
+
|
269
|
+
def set_ancestors
|
270
|
+
@ancestors = split_and_pop path: @request_path
|
271
|
+
@ancestor_path = join_paths @root_dir, @ancestors.join('/')
|
272
|
+
end
|
273
|
+
|
274
|
+
def parent_path(path)
|
275
|
+
join_paths @root_dir, split_and_pop(path: path)
|
276
|
+
end
|
277
|
+
|
278
|
+
def destination_locked?(path)
|
279
|
+
store = PStore.new "#{path}.pstore"
|
280
|
+
lock = store.transaction(true) { store[:lockdiscovery] }
|
281
|
+
|
282
|
+
obj_exists_and_is_not_type? obj: lock, type: {}
|
283
|
+
end
|
284
|
+
|
285
|
+
def common_path_ancestors(path, ancestors)
|
286
|
+
[].tap do |common|
|
287
|
+
ancestors.each do |ancestor|
|
288
|
+
split_path = path.split ancestor
|
289
|
+
common.push ancestor if split_path.length > 1
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def ancestors_from_path_to_ancestor(path, stop_at_ancestor)
|
295
|
+
path = split_and_pop path: path
|
296
|
+
ancestors = []
|
297
|
+
|
298
|
+
until path.last == stop_at_ancestor
|
299
|
+
ancestors.push path.pop
|
300
|
+
end
|
301
|
+
|
302
|
+
ancestors.push stop_at_ancestor
|
303
|
+
ancestors.reverse
|
304
|
+
end
|
305
|
+
|
306
|
+
def store_exist?
|
307
|
+
File.exist? @store_path
|
308
|
+
end
|
309
|
+
|
310
|
+
def create_lock(properties, depth)
|
311
|
+
@store.transaction do
|
312
|
+
@store[:lockcreator] = client_nonce
|
313
|
+
@store[:lockdiscovery] = [] unless @store[:lockdiscovery].is_a? Array
|
314
|
+
@store[:lockdepth] = depth
|
315
|
+
|
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
|
323
|
+
|
324
|
+
@store[:lockdiscovery].push activelock
|
325
|
+
end
|
326
|
+
|
327
|
+
get_lock_info
|
328
|
+
end
|
329
|
+
|
330
|
+
def create_lock_token
|
331
|
+
token = Calligraphy::XML::Node.new
|
332
|
+
token.name = 'locktoken'
|
333
|
+
|
334
|
+
href = Calligraphy::XML::Node.new
|
335
|
+
href.name = 'href'
|
336
|
+
href.text = ['urn', 'uuid', SecureRandom.uuid].join ':'
|
337
|
+
|
338
|
+
token.children = [href]
|
339
|
+
token
|
340
|
+
end
|
341
|
+
|
342
|
+
def timeout_node
|
343
|
+
Calligraphy::XML::Node.new.tap do |node|
|
344
|
+
node.name = 'timeout'
|
345
|
+
node.text = ['Second', Calligraphy.lock_timeout_period].join '-'
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
def get_lock_info
|
350
|
+
return nil if @store.nil?
|
351
|
+
|
352
|
+
@lock_info = @store.transaction(true) { @store[:lockdiscovery] }
|
353
|
+
@lock_info.nil? ? nil : map_array_of_hashes(@lock_info)
|
354
|
+
end
|
355
|
+
|
356
|
+
def lockscope
|
357
|
+
@lock_info[-1][:lockscope].children[0].name
|
358
|
+
end
|
359
|
+
|
360
|
+
def can_unlock?(headers=nil)
|
361
|
+
token = unless headers.nil?
|
362
|
+
extract_lock_token(headers['If']) if headers['If']
|
363
|
+
end
|
364
|
+
|
365
|
+
lock_tokens.include? token
|
366
|
+
end
|
367
|
+
|
368
|
+
def locking_ancestor?(ancestor_path, ancestors, headers=nil)
|
369
|
+
ancestor_store_path = "#{ancestor_path}/#{ancestors[-1]}.pstore"
|
370
|
+
check_lock_creator = Calligraphy.enable_digest_authentication
|
371
|
+
blocking_lock = false
|
372
|
+
unlockable = true
|
373
|
+
|
374
|
+
ancestors.pop
|
375
|
+
|
376
|
+
if File.exist? ancestor_store_path
|
377
|
+
ancestor_store = PStore.new ancestor_store_path
|
378
|
+
ancestor_lock_depth = ancestor_store.transaction(true) do
|
379
|
+
ancestor_store[:lockdepth]
|
380
|
+
end
|
381
|
+
|
382
|
+
ancestor_lock = ancestor_store.transaction(true) do
|
383
|
+
ancestor_store[:lockdiscovery]
|
384
|
+
end
|
385
|
+
|
386
|
+
ancestor_lock_creator = ancestor_store.transaction(true) do
|
387
|
+
ancestor_store[:lockcreator]
|
388
|
+
end if check_lock_creator
|
389
|
+
|
390
|
+
blocking_lock = obj_exists_and_is_not_type? obj: ancestor_lock, type: []
|
391
|
+
|
392
|
+
if blocking_lock
|
393
|
+
token = unless headers.nil?
|
394
|
+
extract_lock_token(headers['If']) if headers['If']
|
395
|
+
end
|
396
|
+
|
397
|
+
ancestor_lock_tokens = ancestor_lock
|
398
|
+
.each { |x| x }
|
399
|
+
.map { |k, v| k[:locktoken].children[0].text }
|
400
|
+
|
401
|
+
unlockable = ancestor_lock_tokens.include?(token) ||
|
402
|
+
(check_lock_creator && (ancestor_lock_creator == client_nonce))
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
if blocking_lock || ancestors.empty?
|
407
|
+
@locking_ancestor = {
|
408
|
+
depth: ancestor_lock_depth,
|
409
|
+
info: ancestor_lock
|
410
|
+
}
|
411
|
+
|
412
|
+
return unlockable ? false : true
|
413
|
+
end
|
414
|
+
|
415
|
+
next_ancestor = split_and_pop(path: ancestor_path).join '/'
|
416
|
+
locking_ancestor? next_ancestor, ancestors, headers
|
417
|
+
end
|
418
|
+
|
419
|
+
def get_property(prop)
|
420
|
+
case prop.name
|
421
|
+
when 'creationdate'
|
422
|
+
prop.content = @stats[:created_at]
|
423
|
+
when 'displayname'
|
424
|
+
prop.content = @name
|
425
|
+
when 'getcontentlength'
|
426
|
+
prop.content = @stats[:size]
|
427
|
+
when 'getlastmodified'
|
428
|
+
prop.content = @updated_at
|
429
|
+
when 'resourcetype'
|
430
|
+
prop.content = 'collection'
|
431
|
+
when 'lockdiscovery'
|
432
|
+
return get_lock_info
|
433
|
+
else
|
434
|
+
return get_custom_property prop.name
|
435
|
+
end
|
436
|
+
|
437
|
+
prop
|
438
|
+
end
|
439
|
+
|
440
|
+
def get_custom_property(prop)
|
441
|
+
@store_properties ||= @store.transaction(true) { @store[:properties] }
|
442
|
+
@store_properties[prop.to_sym] unless @store_properties.nil? || prop.nil?
|
443
|
+
end
|
444
|
+
|
445
|
+
def matching_namespace?(node_arr, node)
|
446
|
+
node_arr.select { |x| x.namespace.href == node.namespace.href }.length > 0
|
447
|
+
end
|
448
|
+
|
449
|
+
def same_namespace?(node1, node2)
|
450
|
+
node1.namespace.href == node2.namespace.href
|
451
|
+
end
|
452
|
+
|
453
|
+
def refresh_ancestor_locks(ancestor_path, ancestors)
|
454
|
+
ancestor_store_path = "#{ancestor_path}/#{ancestors[-1]}.pstore"
|
455
|
+
ancestors.pop
|
456
|
+
|
457
|
+
if File.exist? ancestor_store_path
|
458
|
+
ancestor_store = PStore.new ancestor_store_path
|
459
|
+
ancestor_lock = ancestor_store.transaction(true) do
|
460
|
+
ancestor_store[:lockdiscovery][-1][:timeout] = timeout_node
|
461
|
+
ancestor_store[:lockdiscovery]
|
462
|
+
end
|
463
|
+
|
464
|
+
return map_array_of_hashes ancestor_lock
|
465
|
+
end
|
466
|
+
|
467
|
+
next_ancestor = split_and_pop(path: ancestor_path).join '/'
|
468
|
+
refresh_ancestor_locks next_ancestor, ancestors
|
469
|
+
end
|
470
|
+
|
471
|
+
def remove_lock(token)
|
472
|
+
@store.transaction do
|
473
|
+
@store.delete :lockcreator
|
474
|
+
|
475
|
+
if @store[:lockdiscovery].length == 1
|
476
|
+
@store.delete :lockdiscovery
|
477
|
+
else
|
478
|
+
@store[:lockdiscovery] = @store[:lockdiscovery].reject do |activelock|
|
479
|
+
activelock[:locktoken].children[0].text == token
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
@lock_info = nil
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|