cloudfs 1.0.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/.yardopts +13 -0
- data/lib/cloudfs.rb +18 -0
- data/lib/cloudfs/account.rb +95 -0
- data/lib/cloudfs/client/connection.rb +154 -0
- data/lib/cloudfs/client/constants.rb +80 -0
- data/lib/cloudfs/client/error.rb +452 -0
- data/lib/cloudfs/client/utils.rb +93 -0
- data/lib/cloudfs/container.rb +51 -0
- data/lib/cloudfs/file.rb +209 -0
- data/lib/cloudfs/filesystem.rb +111 -0
- data/lib/cloudfs/filesystem_common.rb +228 -0
- data/lib/cloudfs/folder.rb +94 -0
- data/lib/cloudfs/item.rb +640 -0
- data/lib/cloudfs/media.rb +32 -0
- data/lib/cloudfs/rest_adapter.rb +1233 -0
- data/lib/cloudfs/session.rb +256 -0
- data/lib/cloudfs/share.rb +286 -0
- data/lib/cloudfs/user.rb +107 -0
- data/lib/cloudfs/version.rb +3 -0
- data/spec/account_spec.rb +93 -0
- data/spec/container_spec.rb +37 -0
- data/spec/file_spec.rb +134 -0
- data/spec/filesystem_spec.rb +16 -0
- data/spec/folder_spec.rb +106 -0
- data/spec/item_spec.rb +194 -0
- data/spec/session_spec.rb +102 -0
- data/spec/share_spec.rb +159 -0
- data/spec/user_spec.rb +70 -0
- metadata +124 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative 'file.rb'
|
2
|
+
module CloudFS
|
3
|
+
|
4
|
+
# @review Not creating file objects based on mime type,
|
5
|
+
# since save operation cannot update the class of file object,
|
6
|
+
# if mime is changed
|
7
|
+
# Photo class initializes the type of the file, not used currently
|
8
|
+
class Photo < File;
|
9
|
+
end
|
10
|
+
|
11
|
+
# @review Not creating file objects based on mime type,
|
12
|
+
# since save operation cannot update the class of file object,
|
13
|
+
# if mime is changed
|
14
|
+
# Video class initializes the type of the file, not used currently
|
15
|
+
class Video < File;
|
16
|
+
end
|
17
|
+
|
18
|
+
# @review Not creating file objects based on mime type,
|
19
|
+
# since save operation cannot update the class of file object,
|
20
|
+
# if mime is changed
|
21
|
+
# Audio class initializes the type of the file, not used currently
|
22
|
+
class Audio < File;
|
23
|
+
end
|
24
|
+
|
25
|
+
# @review Not creating file objects based on mime type,
|
26
|
+
# since save operation cannot update the class of file object,
|
27
|
+
# if mime is changed
|
28
|
+
# Document class initializes the type of the file, not used currently
|
29
|
+
class Document < File;
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,1233 @@
|
|
1
|
+
require_relative 'version'
|
2
|
+
require_relative 'client/connection'
|
3
|
+
require_relative 'client/constants'
|
4
|
+
require_relative 'client/utils'
|
5
|
+
require_relative 'client/error'
|
6
|
+
|
7
|
+
module CloudFS
|
8
|
+
# Provides low level mapping APIs to Bitcasa CloudFS Service
|
9
|
+
#
|
10
|
+
# @author Mrinal Dhillon
|
11
|
+
# Maintains an instance of RESTful {RestAdapter::Connection},
|
12
|
+
# since RestAdapter::Connection instance is MT-safe
|
13
|
+
# and can be called from several threads without synchronization
|
14
|
+
# after setting up an instance, same behaviour is expected from
|
15
|
+
# RestAdapter class.
|
16
|
+
# Should use single instance for all calls per remote server across
|
17
|
+
# multiple threads for performance.
|
18
|
+
#
|
19
|
+
# @note
|
20
|
+
# path, destination as input parameter expects absolute path (url) of
|
21
|
+
# object in end-user's account.
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# Authenticate
|
25
|
+
# rest_adapter = CloudFS::RestAdapter.new(clientid, secret, host)
|
26
|
+
# rest_adapter.authenticate(username, password)
|
27
|
+
# rest_adapter.ping
|
28
|
+
|
29
|
+
# @example Upload file
|
30
|
+
# ::File.open(local_file_path, "r") do |file|
|
31
|
+
# rest_adapter.upload(path, file,
|
32
|
+
# name: 'somename', exists: 'FAIL')
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# @example Download file
|
36
|
+
# Download into buffer
|
37
|
+
# buffer = rest_adapter.download(path, startbyte: 0, bytecount: 1000)
|
38
|
+
#
|
39
|
+
# Streaming download i.e. chunks are synchronously returned as soon as
|
40
|
+
# available
|
41
|
+
# preferable for large files download:
|
42
|
+
#
|
43
|
+
# ::File.open(local_filepath, 'wb') do |file|
|
44
|
+
# rest_adapter.download(path) { |buffer| file.write(buffer) }
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# @optimize Support async requests,
|
48
|
+
# blocker methods like wait for async operations,
|
49
|
+
# chunked/streaming upload i.e. chunked upload(not sure if server supports),
|
50
|
+
# StringIO, String upload, debug
|
51
|
+
class RestAdapter
|
52
|
+
|
53
|
+
# Creates RestAdapter instance that manages rest api calls to CloudFS service
|
54
|
+
#
|
55
|
+
# @param clientid [String] application clientid
|
56
|
+
# @param secret [String] application secret
|
57
|
+
# @param host [String] server address
|
58
|
+
# @param [Hash] params RESTful connection configurations
|
59
|
+
# @option params [Fixnum] :connect_timeout (60) for server handshake
|
60
|
+
# @option params [Fixnum] :send_timeout (0) for send request,
|
61
|
+
# default is set to never, in order to support large uploads
|
62
|
+
# @option params [Fixnum] :receive_timeout (120) for read timeout per block
|
63
|
+
# @option params [Fixnum] :max_retry (3) for http 500 level errors
|
64
|
+
# @option params [#<<] :http_debug (nil) to enable http debugging,
|
65
|
+
# example STDERR, STDOUT, {::File} object opened with permissions to write
|
66
|
+
#
|
67
|
+
# @raise [Errors::ArgumentError]
|
68
|
+
#
|
69
|
+
# @optimize Configurable chunk size for chunked stream downloads,
|
70
|
+
# default is 16KB.
|
71
|
+
# Configurable keep alive timeout for persistent connections in
|
72
|
+
# connection pool, default is 15 seconds.
|
73
|
+
# Async api support
|
74
|
+
#
|
75
|
+
# @review optimum default values for send and receive timeouts
|
76
|
+
def initialize(clientid, secret, host, ** params)
|
77
|
+
fail Errors::ArgumentError,
|
78
|
+
'Invalid argument provided' if (Utils.is_blank?(clientid) ||
|
79
|
+
Utils.is_blank?(secret) || Utils.is_blank?(host))
|
80
|
+
|
81
|
+
@clientid = "#{clientid}"
|
82
|
+
@secret = "#{secret}"
|
83
|
+
@host = /https:\/\// =~ host ? "#{host}" :
|
84
|
+
"#{Constants::URI_PREFIX_HTTPS}#{host}"
|
85
|
+
@access_token = nil
|
86
|
+
|
87
|
+
connect_timeout, send_timeout, receive_timeout, max_retries, http_debug =
|
88
|
+
params.values_at(
|
89
|
+
:connect_timeout,
|
90
|
+
:send_timeout,
|
91
|
+
:receive_timeout,
|
92
|
+
:max_retries,
|
93
|
+
:http_debug)
|
94
|
+
|
95
|
+
connect_timeout ||= 60
|
96
|
+
send_timeout ||= 0
|
97
|
+
receive_timeout ||= 120
|
98
|
+
max_retries ||= 3
|
99
|
+
|
100
|
+
@http_connection = Connection.new(
|
101
|
+
connect_timeout: connect_timeout,
|
102
|
+
send_timeout: send_timeout,
|
103
|
+
receive_timeout: receive_timeout,
|
104
|
+
max_retries: max_retries, debug_dev: http_debug,
|
105
|
+
agent_name: "#{Constants::HTTP_AGENT_NAME} (#{CloudFS::VERSION})")
|
106
|
+
end
|
107
|
+
|
108
|
+
# @return [Boolean] whether rest_adapter can make authenticated
|
109
|
+
# requests to cloudfs service
|
110
|
+
# @raise [Errors::ServiceError]
|
111
|
+
def linked?
|
112
|
+
ping
|
113
|
+
true
|
114
|
+
rescue Errors::SessionNotLinked
|
115
|
+
false
|
116
|
+
end
|
117
|
+
|
118
|
+
# Unlinks this rest_adapter object from cloudfs user's account
|
119
|
+
#
|
120
|
+
# @note this will disconnect all keep alive connections and internal
|
121
|
+
# sessions
|
122
|
+
def unlink
|
123
|
+
if @access_token
|
124
|
+
@access_token = ''
|
125
|
+
@http_connection.unlink
|
126
|
+
end
|
127
|
+
true
|
128
|
+
end
|
129
|
+
|
130
|
+
# Obtains an OAuth2 access token that authenticates an end-user for this
|
131
|
+
# rest_adapter
|
132
|
+
#
|
133
|
+
# @param username [String] username of the end-user
|
134
|
+
# @param password [String] password of the end-user
|
135
|
+
#
|
136
|
+
# @return [true]
|
137
|
+
#
|
138
|
+
# @raise [Errors::ServiceError, Errors::ArgumentError]
|
139
|
+
def authenticate(username, password)
|
140
|
+
fail Errors::ArgumentError,
|
141
|
+
'Invalid argument, must pass username' if Utils.is_blank?(username)
|
142
|
+
fail Errors::ArgumentError,
|
143
|
+
'Invalid argument, must pass password' if Utils.is_blank?(password)
|
144
|
+
|
145
|
+
date = Time.now.utc.strftime(Constants::DATE_FORMAT)
|
146
|
+
form = {
|
147
|
+
Constants::PARAM_GRANT_TYPE => Constants::PARAM_PASSWORD,
|
148
|
+
Constants::PARAM_PASSWORD => password,
|
149
|
+
Constants::PARAM_USER => username
|
150
|
+
}
|
151
|
+
|
152
|
+
headers = {
|
153
|
+
Constants::HEADER_CONTENT_TYPE =>
|
154
|
+
Constants::CONTENT_TYPE_APP_URLENCODED,
|
155
|
+
Constants::HEADER_DATE => date
|
156
|
+
}
|
157
|
+
|
158
|
+
uri = {endpoint: Constants::ENDPOINT_OAUTH}
|
159
|
+
signature = Utils.generate_auth_signature(Constants::ENDPOINT_OAUTH,
|
160
|
+
form, headers, @secret)
|
161
|
+
headers[Constants::HEADER_AUTHORIZATION] =
|
162
|
+
"#{Constants::HEADER_AUTH_PREFIX_BCS} #{@clientid}:#{signature}"
|
163
|
+
|
164
|
+
access_info = request('POST', uri: uri, header: headers, body: form)
|
165
|
+
@access_token = access_info.fetch(:access_token)
|
166
|
+
true
|
167
|
+
end
|
168
|
+
|
169
|
+
# Ping cloudfs server to verifies the end-user’s access token
|
170
|
+
#
|
171
|
+
# @return
|
172
|
+
#
|
173
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError]
|
174
|
+
def ping
|
175
|
+
request('GET', uri: {endpoint: Constants::ENDPOINT_PING},
|
176
|
+
header: Constants::HEADER_CONTENT_TYPE_APP_URLENCODED)
|
177
|
+
true
|
178
|
+
end
|
179
|
+
|
180
|
+
# Creates a new end-user account for a Paid CloudFS (developer’s) account
|
181
|
+
#
|
182
|
+
# @param username [String] username of the end-user.
|
183
|
+
# @param password [String] password of the end-user.
|
184
|
+
# @param email [String] email of the end-user
|
185
|
+
# @param first_name [String] first name of the end-user
|
186
|
+
# @param last_name [String] last name of the end-user
|
187
|
+
#
|
188
|
+
# @return [Hash] end-user's attributes
|
189
|
+
#
|
190
|
+
# @raise [Errors::ServiceError, Errors::ArgumentError]
|
191
|
+
def create_account(username, password, email: nil,
|
192
|
+
first_name: nil, last_name: nil)
|
193
|
+
fail Errors::ArgumentError,
|
194
|
+
'Invalid argument, must pass username' if Utils.is_blank?(username)
|
195
|
+
fail Errors::ArgumentError,
|
196
|
+
'Invalid argument, must pass password' if Utils.is_blank?(password)
|
197
|
+
|
198
|
+
date = Time.now.utc.strftime(Constants::DATE_FORMAT)
|
199
|
+
form = {
|
200
|
+
Constants::PARAM_PASSWORD => password,
|
201
|
+
Constants::PARAM_USER => username
|
202
|
+
}
|
203
|
+
|
204
|
+
form[Constants::PARAM_EMAIL] = email unless Utils.is_blank?(email)
|
205
|
+
form[Constants::PARAM_FIRST_NAME] =
|
206
|
+
first_name unless Utils.is_blank?(first_name)
|
207
|
+
form[Constants::PARAM_LAST_NAME] =
|
208
|
+
last_name unless Utils.is_blank?(last_name)
|
209
|
+
|
210
|
+
headers = {
|
211
|
+
Constants::HEADER_CONTENT_TYPE =>
|
212
|
+
Constants::CONTENT_TYPE_APP_URLENCODED,
|
213
|
+
Constants::HEADER_DATE => date
|
214
|
+
}
|
215
|
+
uri = {endpoint: Constants::ENDPOINT_CUSTOMERS}
|
216
|
+
signature = Utils.generate_auth_signature(Constants::ENDPOINT_CUSTOMERS,
|
217
|
+
form, headers, @secret)
|
218
|
+
headers[Constants::HEADER_AUTHORIZATION] =
|
219
|
+
"#{Constants::HEADER_AUTH_PREFIX_BCS} #{@clientid}:#{signature}"
|
220
|
+
|
221
|
+
request('POST', uri: uri, header: headers, body: form)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Get cloudfs end-user profile information
|
225
|
+
#
|
226
|
+
# @return [Hash] account metadata for the authenticated user
|
227
|
+
#
|
228
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError]
|
229
|
+
def get_profile
|
230
|
+
uri = {endpoint: Constants::ENDPOINT_USER_PROFILE}
|
231
|
+
|
232
|
+
request('GET', uri: uri,
|
233
|
+
header: Constants::HEADER_CONTENT_TYPE_APP_URLENCODED)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Create folder at specified destination path in end-user's account
|
237
|
+
#
|
238
|
+
# @param name [Sting] name of folder to create
|
239
|
+
# @param path [String] default: root, absolute path to destination folder
|
240
|
+
# @param exists [String] ('FAIL', 'OVERWRITE', 'RENAME', 'REUSE')
|
241
|
+
#
|
242
|
+
# @return [Hash] metadata of created folder
|
243
|
+
#
|
244
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
245
|
+
# Errors::ArgumentError]
|
246
|
+
#
|
247
|
+
# @review why this api returns an array of items
|
248
|
+
def create_folder(name, path: nil, exists: 'FAIL')
|
249
|
+
fail Errors::ArgumentError,
|
250
|
+
'Invalid argument, must pass name' if Utils.is_blank?(name)
|
251
|
+
exists = Constants::EXISTS.fetch(exists.to_sym) {
|
252
|
+
fail Errors::ArgumentError, 'Invalid value for exists' }
|
253
|
+
|
254
|
+
uri = set_uri_params(Constants::ENDPOINT_FOLDERS, name: path)
|
255
|
+
query = {operation: Constants::QUERY_OPS_CREATE}
|
256
|
+
form = {name: name, exists: exists}
|
257
|
+
|
258
|
+
response = request('POST', uri: uri, query: query, body: form)
|
259
|
+
items = response.fetch(:items)
|
260
|
+
items.first
|
261
|
+
end
|
262
|
+
|
263
|
+
# @param path [String] defaults: root, folder path to list
|
264
|
+
# @param depth [Fixnum] default: nil, levels to recurse, 0 - infinite depth
|
265
|
+
# @param filter [String]
|
266
|
+
# @param strict_traverse [Boolean] traversal based on success of filters
|
267
|
+
# and possibly the depth parameters
|
268
|
+
#
|
269
|
+
# @return [Array<Hash>] metadata of files and folders under listed folder
|
270
|
+
#
|
271
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
272
|
+
# Errors::ArgumentError]
|
273
|
+
#
|
274
|
+
# @todo accept filter array, return { meta: Hash, items: Array<Hash> }
|
275
|
+
def list_folder(path: nil, depth: nil, filter: nil, strict_traverse: false)
|
276
|
+
fail Errors::ArgumentError,
|
277
|
+
'Invalid argument must pass strict_traverse of type boolean' unless !!strict_traverse == strict_traverse
|
278
|
+
|
279
|
+
uri = set_uri_params(Constants::ENDPOINT_FOLDERS, name: path)
|
280
|
+
query = {}
|
281
|
+
query = {depth: depth} if depth
|
282
|
+
unless Utils.is_blank?(filter)
|
283
|
+
query[:filter] = filter
|
284
|
+
query[:'strict-traverse'] = "#{strict_traverse}"
|
285
|
+
end
|
286
|
+
|
287
|
+
response = request('GET', uri: uri, query: query)
|
288
|
+
response.fetch(:items)
|
289
|
+
end
|
290
|
+
|
291
|
+
# Delete folder
|
292
|
+
#
|
293
|
+
# @param path [String] folder path
|
294
|
+
# @param commit [Boolean]
|
295
|
+
# set true to remove folder permanently, else will be moved to trash
|
296
|
+
# @param force [Boolean] set true to delete non-empty folder
|
297
|
+
#
|
298
|
+
# @return [Hash] hash with key for success and deleted folder's last version
|
299
|
+
|
300
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
301
|
+
# Errors::ArgumentError]
|
302
|
+
def delete_folder(path, commit: false, force: false)
|
303
|
+
delete(Constants::ENDPOINT_FOLDERS, path, commit: commit, force: force)
|
304
|
+
end
|
305
|
+
|
306
|
+
# Delete file
|
307
|
+
#
|
308
|
+
# @param path [String] file path
|
309
|
+
# @param commit [Boolean]
|
310
|
+
# set true to remove file permanently, else will be moved to trash
|
311
|
+
#
|
312
|
+
# @return [Hash] hash with key for success and deleted file's last version
|
313
|
+
#
|
314
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
315
|
+
# Errors::ArgumentError]
|
316
|
+
def delete_file(path, commit: false)
|
317
|
+
delete(Constants::ENDPOINT_FILES, path, commit: commit)
|
318
|
+
end
|
319
|
+
|
320
|
+
# Delete private common method for file and folder
|
321
|
+
#
|
322
|
+
# @param endpoint [String] CloudFS endpoint for file/folder
|
323
|
+
# @param path [String] file/folder path
|
324
|
+
# @param commit [Boolean]
|
325
|
+
# set true to remove file/folder permanently, else will be moved to trash
|
326
|
+
# @param force [Boolean] set true to delete non-empty folder
|
327
|
+
#
|
328
|
+
# @return [Hash] hash with key for success and deleted file/folder's
|
329
|
+
# last version
|
330
|
+
|
331
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
332
|
+
# Errors::ArgumentError]
|
333
|
+
def delete(endpoint, path, commit: false, force: false)
|
334
|
+
fail Errors::ArgumentError,
|
335
|
+
'Invalid argument, must pass endpoint' if Utils.is_blank?(endpoint)
|
336
|
+
fail Errors::ArgumentError,
|
337
|
+
'Invalid argument, must pass path' if Utils.is_blank?(path)
|
338
|
+
fail Errors::ArgumentError,
|
339
|
+
'Invalid argument must pass commit of type boolean' unless !!commit == commit
|
340
|
+
fail Errors::ArgumentError,
|
341
|
+
'Invalid argument must pass force of type boolean' unless !!force == force
|
342
|
+
|
343
|
+
uri = set_uri_params(endpoint, name: path)
|
344
|
+
query = {commit: "#{commit}"}
|
345
|
+
query[:force] = "#{force}" if force
|
346
|
+
|
347
|
+
request('DELETE', uri: uri, query: query)
|
348
|
+
end
|
349
|
+
|
350
|
+
# Copy folder to specified destination folder
|
351
|
+
#
|
352
|
+
# @param path [String] source folder path
|
353
|
+
# @param destination [String] destination folder path
|
354
|
+
# @param name [String] new name of copied folder
|
355
|
+
# @param exists [String] ('FAIL', 'OVERWRITE', 'RENAME')
|
356
|
+
# action to take in case of a conflict with an existing folder
|
357
|
+
# An unused integer is appended to folder name if exists: RENAME
|
358
|
+
#
|
359
|
+
# @return [Hash] metadata of new folder
|
360
|
+
#
|
361
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
362
|
+
# Errors::ArgumentError]
|
363
|
+
def copy_folder(path, destination, name, exists: 'FAIL')
|
364
|
+
copy(Constants::ENDPOINT_FOLDERS, path, destination, name, exists: exists)
|
365
|
+
end
|
366
|
+
|
367
|
+
# Copy file to specified destination folder
|
368
|
+
#
|
369
|
+
# @param path [String] source file path
|
370
|
+
# @param destination [String] destination folder path
|
371
|
+
# @param name [String] new name of copied file
|
372
|
+
# @param exists [String] ('FAIL', 'OVERWRITE', 'RENAME')
|
373
|
+
# action to take in case of a conflict with an existing file
|
374
|
+
# An unused integer is appended to file name if exists: RENAME
|
375
|
+
#
|
376
|
+
# @return [Hash] metadata of new file
|
377
|
+
#
|
378
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
379
|
+
# Errors::ArgumentError]
|
380
|
+
def copy_file(path, destination, name, exists: 'RENAME')
|
381
|
+
copy(Constants::ENDPOINT_FILES, path, destination, name, exists: exists)
|
382
|
+
end
|
383
|
+
|
384
|
+
# Copy private common function for folder/file
|
385
|
+
#
|
386
|
+
# @param endpoint [String] folder/file server endpoint
|
387
|
+
# @param path [String] source folder/file path
|
388
|
+
# @param destination [String] destination folder path
|
389
|
+
# @param name [String] name of copied folder/file,
|
390
|
+
# default is source folder/file's name
|
391
|
+
# @param exists [String] ('FAIL', 'OVERWRITE', 'RENAME')
|
392
|
+
# action to take in case of a conflict with an existing folder/file
|
393
|
+
# An unused integer is appended to folder/file name if exists: RENAME
|
394
|
+
#
|
395
|
+
# @return [Hash] metadata of new folder/file
|
396
|
+
#
|
397
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
398
|
+
# Errors::ArgumentError]
|
399
|
+
def copy(endpoint, path, destination, name, exists: 'FAIL')
|
400
|
+
fail Errors::ArgumentError,
|
401
|
+
'Invalid argument, must pass valid path' if Utils.is_blank?(path)
|
402
|
+
fail Errors::ArgumentError,
|
403
|
+
'Invalid argument, must pass valid name' if Utils.is_blank?(name)
|
404
|
+
fail Errors::ArgumentError,
|
405
|
+
'Invalid argument, must pass valid destination' if Utils.is_blank?(destination)
|
406
|
+
exists = Constants::EXISTS.fetch(exists.to_sym) {
|
407
|
+
raise Errors::ArgumentError, 'Invalid value for exists' }
|
408
|
+
|
409
|
+
destination = prepend_path_with_forward_slash(destination)
|
410
|
+
uri = set_uri_params(endpoint, name: path)
|
411
|
+
query = {operation: Constants::QUERY_OPS_COPY}
|
412
|
+
form = {to: destination, name: name, exists: exists}
|
413
|
+
|
414
|
+
response = request('POST', uri: uri, query: query, body: form)
|
415
|
+
response.fetch(:meta, response)
|
416
|
+
end
|
417
|
+
|
418
|
+
# Move folder to specified destination folder
|
419
|
+
#
|
420
|
+
# @param path [String] source folder path
|
421
|
+
# @param destination [String] destination folder path
|
422
|
+
# @param name [String] new name of moved folder
|
423
|
+
# @param exists [String] ('FAIL', 'OVERWRITE', 'RENAME')
|
424
|
+
# action to take in case of a conflict with an existing folder
|
425
|
+
# An unused integer is appended to folder name if exists: RENAME
|
426
|
+
#
|
427
|
+
# @return [Hash] metadata of moved folder
|
428
|
+
|
429
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
430
|
+
# Errors::ArgumentError]
|
431
|
+
def move_folder(path, destination, name, exists: 'FAIL')
|
432
|
+
move(Constants::ENDPOINT_FOLDERS, path, destination, name, exists: exists)
|
433
|
+
end
|
434
|
+
|
435
|
+
# Move file to specified destination folder
|
436
|
+
#
|
437
|
+
# @param path [String] source file path
|
438
|
+
# @param destination [String] destination folder path
|
439
|
+
# @param name [String] name of moved file
|
440
|
+
# @param exists [String] ('FAIL', 'OVERWRITE', 'RENAME')
|
441
|
+
# action to take in case of a conflict with an existing file
|
442
|
+
# An unused integer is appended to file name if exists: RENAME
|
443
|
+
#
|
444
|
+
# @return [Hash] metadata of moved file
|
445
|
+
#
|
446
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
447
|
+
# Errors::ArgumentError]
|
448
|
+
def move_file(path, destination, name, exists: 'RENAME')
|
449
|
+
move(Constants::ENDPOINT_FILES, path, destination, name, exists: exists)
|
450
|
+
end
|
451
|
+
|
452
|
+
# Move folder/file private common method
|
453
|
+
#
|
454
|
+
# @param endpoint [String] file/folder server endpoint
|
455
|
+
# @param path [String] source folder/file path
|
456
|
+
# @param destination [String] destination folder path
|
457
|
+
# @param name [String] name of moved folder/file
|
458
|
+
# @param exists [String] ('FAIL', 'OVERWRITE', 'RENAME')
|
459
|
+
# action to take in case of a conflict with an existing folder/file
|
460
|
+
# An unused integer is appended to folder/file name if exists: RENAME
|
461
|
+
#
|
462
|
+
# @return [Hash] metadata of moved folder/file
|
463
|
+
#
|
464
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
465
|
+
# Errors::ArgumentError]
|
466
|
+
#
|
467
|
+
# @review according to cloudfs rest api docs of move folder,
|
468
|
+
# path default is root i.e. root is moved!
|
469
|
+
def move(endpoint, path, destination, name, exists: 'FAIL')
|
470
|
+
fail Errors::ArgumentError,
|
471
|
+
'Invalid argument, must pass valid path' if Utils.is_blank?(path)
|
472
|
+
fail Errors::ArgumentError,
|
473
|
+
'Invalid argument, must pass valid name' if Utils.is_blank?(name)
|
474
|
+
fail Errors::ArgumentError,
|
475
|
+
'Invalid argument, must pass valid destination' if Utils.is_blank?(destination)
|
476
|
+
exists = Constants::EXISTS.fetch(exists.to_sym) {
|
477
|
+
fail Errors::ArgumentError, 'Invalid value for exists' }
|
478
|
+
|
479
|
+
destination = prepend_path_with_forward_slash(destination)
|
480
|
+
uri = set_uri_params(endpoint, name: path)
|
481
|
+
query = {operation: Constants::QUERY_OPS_MOVE}
|
482
|
+
form = {to: destination, exists: exists, name: name}
|
483
|
+
|
484
|
+
response = request('POST', uri: uri, query: query, body: form)
|
485
|
+
response.fetch(:meta, response)
|
486
|
+
end
|
487
|
+
|
488
|
+
# Get folder meta
|
489
|
+
#
|
490
|
+
# @param path [String] folder path
|
491
|
+
#
|
492
|
+
# @return [Hash] metadata of folder
|
493
|
+
#
|
494
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
495
|
+
# Errors::ArgumentError]
|
496
|
+
def get_folder_meta(path)
|
497
|
+
get_meta(Constants::ENDPOINT_FOLDERS, path)
|
498
|
+
end
|
499
|
+
|
500
|
+
# Get file meta
|
501
|
+
#
|
502
|
+
# @param path [String] file path
|
503
|
+
#
|
504
|
+
# @return [Hash] metadata of file
|
505
|
+
#
|
506
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
507
|
+
# Errors::ArgumentError]
|
508
|
+
def get_file_meta(path)
|
509
|
+
get_meta(Constants::ENDPOINT_FILES, path)
|
510
|
+
end
|
511
|
+
|
512
|
+
# Get item meta
|
513
|
+
#
|
514
|
+
# @param path [String] file path
|
515
|
+
#
|
516
|
+
# @return [Hash] metadata of item
|
517
|
+
#
|
518
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
519
|
+
# Errors::ArgumentError]
|
520
|
+
def get_item_meta(path)
|
521
|
+
get_meta(Constants::ENDPOINT_ITEM, path)
|
522
|
+
end
|
523
|
+
|
524
|
+
# Get folder/file meta private common method
|
525
|
+
#
|
526
|
+
# @param endpoint [String] file/folder server endpoint
|
527
|
+
# @param path [String] file/folder path
|
528
|
+
#
|
529
|
+
# @return [Hash] metadata of file/folder
|
530
|
+
#
|
531
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
532
|
+
# Errors::ArgumentError]
|
533
|
+
def get_meta(endpoint, path)
|
534
|
+
fail Errors::ArgumentError,
|
535
|
+
'Invalid argument, must pass valid path' if Utils.is_blank?(path)
|
536
|
+
|
537
|
+
uri = set_uri_params(endpoint, name: path, operation: 'meta')
|
538
|
+
|
539
|
+
response = request('GET', uri: uri)
|
540
|
+
response.fetch(:meta, response)
|
541
|
+
end
|
542
|
+
|
543
|
+
# Alter folder metadata
|
544
|
+
#
|
545
|
+
# @param path [String] folder path
|
546
|
+
# @param version [Fixnum] version number of folder
|
547
|
+
# @param version_conflict [String] ('FAIL', 'IGNORE') action to take
|
548
|
+
# if the version on the rest_adapter does not match the version
|
549
|
+
# on the server
|
550
|
+
#
|
551
|
+
# @param [Hash] properties
|
552
|
+
# @option properties [String] :name (nil) new name
|
553
|
+
# @option properties [Fixnum] :date_created (nil) timestamp
|
554
|
+
# @option properties [Fixnum] :date_meta_last_modified (nil) timestamp
|
555
|
+
# @option properties [Fixnum] :date_content_last_modified (nil) timestamp
|
556
|
+
# @option properties [Hash] :application_data({}) will be merged
|
557
|
+
# with existing application data
|
558
|
+
#
|
559
|
+
# @return [Hash] updated metadata of folder
|
560
|
+
#
|
561
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
562
|
+
# Errors::ArgumentError]
|
563
|
+
def alter_folder_meta(path, version, version_conflict: 'FAIL', ** properties)
|
564
|
+
alter_meta(Constants::ENDPOINT_FOLDERS, path, version,
|
565
|
+
version_conflict: version_conflict, ** properties)
|
566
|
+
end
|
567
|
+
|
568
|
+
# Alter file metadata
|
569
|
+
#
|
570
|
+
# @param path [String] file path
|
571
|
+
# @param version [Fixnum] version number of file
|
572
|
+
# @param version_conflict [String] ('FAIL', 'IGNORE') action to take
|
573
|
+
# if the version on rest_adapter does not match the version on server
|
574
|
+
#
|
575
|
+
# @param [Hash] properties
|
576
|
+
# @option properties [String] :name (nil) new name
|
577
|
+
# @option properties [Fixnum] :date_created (nil) timestamp
|
578
|
+
# @option properties [Fixnum] :date_meta_last_modified (nil) timestamp
|
579
|
+
# @option properties [Fixnum] :date_content_last_modified (nil) timestamp
|
580
|
+
# @option properties [Hash] :application_data ({}) will be merged
|
581
|
+
# with existing application data
|
582
|
+
#
|
583
|
+
# @return [Hash] updated metadata of file
|
584
|
+
#
|
585
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError, Errors::ArgumentError]
|
586
|
+
def alter_file_meta(path, version, version_conflict: 'FAIL', ** properties)
|
587
|
+
alter_meta(Constants::ENDPOINT_FILES, path, version,
|
588
|
+
version_conflict: version_conflict, ** properties)
|
589
|
+
end
|
590
|
+
|
591
|
+
# Alter file/folder meta common private method
|
592
|
+
#
|
593
|
+
# @param endpoint [String] file/folder server endpoint
|
594
|
+
# @param path [String] file/folder path
|
595
|
+
# @param version [String, Fixnum] version number of file/folder
|
596
|
+
# @param version_conflict [String] ('FAIL', 'IGNORE') action to take
|
597
|
+
# if the version on the rest_adapter does not match the version
|
598
|
+
# on the server
|
599
|
+
#
|
600
|
+
# @param [Hash] properties
|
601
|
+
# @option properties [String] :name (nil) new name
|
602
|
+
# @option properties [Fixnum] :date_created (nil) timestamp
|
603
|
+
# @option properties [Fixnum] :date_meta_last_modified (nil) timestamp
|
604
|
+
# @option properties [Fixnum] :date_content_last_modified (nil) timestamp
|
605
|
+
# @option properties [Hash] :application_data ({}) will be merged
|
606
|
+
# with existing application data
|
607
|
+
#
|
608
|
+
# @return [Hash] updated metadata of file/folder
|
609
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
610
|
+
# Errors::ArgumentError]
|
611
|
+
#
|
612
|
+
# @review does not suppress multi_json exception for application data
|
613
|
+
def alter_meta(endpoint, path, version, version_conflict: 'FAIL', ** properties)
|
614
|
+
fail Errors::ArgumentError,
|
615
|
+
'Invalid argument, must pass path' if Utils.is_blank?(path)
|
616
|
+
|
617
|
+
version_conflict =
|
618
|
+
Constants::VERSION_EXISTS.fetch(version_conflict.to_sym) {
|
619
|
+
fail Errors::ArgumentError, 'Invalid value for version-conflict' }
|
620
|
+
uri = set_uri_params(endpoint, name: path, operation: 'meta')
|
621
|
+
|
622
|
+
req_properties = {}
|
623
|
+
req_properties = properties.dup unless properties.empty?
|
624
|
+
application_data = req_properties[:application_data]
|
625
|
+
req_properties[:application_data] =
|
626
|
+
Utils.hash_to_json(application_data) unless Utils.is_blank?(application_data)
|
627
|
+
req_properties[:'version'] = "#{version}"
|
628
|
+
req_properties[:'version-conflict'] = version_conflict
|
629
|
+
|
630
|
+
response = request('POST', uri: uri, body: req_properties)
|
631
|
+
response.fetch(:meta, response)
|
632
|
+
end
|
633
|
+
|
634
|
+
# Upload file
|
635
|
+
#
|
636
|
+
# @param path [String] path to upload file to
|
637
|
+
# @param source [#read&#pos&#pos=, String] any object that
|
638
|
+
# responds to first set of methods or is an in-memory string
|
639
|
+
# @param name [String] name of uploaded file, must be set
|
640
|
+
# if source does not respond to #path
|
641
|
+
# @param exists [String] ('FAIL', 'OVERWRITE', 'RENAME', 'REUSE')
|
642
|
+
# action to take if the filename of the file being uploaded conflicts
|
643
|
+
# with an existing file
|
644
|
+
#
|
645
|
+
# @return [Hash] metadata of uploaded file
|
646
|
+
#
|
647
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
648
|
+
# Errors::ArgumentError]
|
649
|
+
#
|
650
|
+
# @example
|
651
|
+
# Upload file
|
652
|
+
# ::File.open(local_file_path, "r") do |file|
|
653
|
+
# rest_adapter.upload(path, file, name: "testfile.txt")
|
654
|
+
# end
|
655
|
+
#
|
656
|
+
# @example
|
657
|
+
# Upload string
|
658
|
+
# rest_adapter.upload(path, "This is upload string", name: 'testfile.txt')
|
659
|
+
#
|
660
|
+
# Upload stream
|
661
|
+
# io = StringIO.new
|
662
|
+
# io.write("this is test stringio")
|
663
|
+
# rest_adapter.upload(path, io, name: 'testfile.txt')
|
664
|
+
# io.close
|
665
|
+
#
|
666
|
+
# @note name must be set if source does not respond to #path
|
667
|
+
#
|
668
|
+
# @todo reuse fallback and reuse attributes
|
669
|
+
def upload(path, source, name: nil, exists: 'FAIL')
|
670
|
+
exists = Constants::EXISTS.fetch(exists.to_sym) {
|
671
|
+
fail Errors::ArgumentError, 'Invalid value for exists' }
|
672
|
+
|
673
|
+
if source.respond_to?(:path)
|
674
|
+
name ||= ::File.basename(source.path)
|
675
|
+
elsif Utils.is_blank?(name)
|
676
|
+
fail Errors::ArgumentError, 'Invalid argument, custom name is required if source does not respond to path'
|
677
|
+
end
|
678
|
+
|
679
|
+
if source.respond_to?(:pos) && source.respond_to?(:pos=)
|
680
|
+
original_pos = source.pos
|
681
|
+
# Setting source offset to start of stream
|
682
|
+
source.pos=0
|
683
|
+
end
|
684
|
+
|
685
|
+
uri = set_uri_params(Constants::ENDPOINT_FILES, name: path)
|
686
|
+
form = {file: source, exists: exists}
|
687
|
+
form[:name] = name
|
688
|
+
|
689
|
+
headers = {
|
690
|
+
Constants::HEADER_CONTENT_TYPE => Constants::CONTENT_TYPE_MULTI
|
691
|
+
}
|
692
|
+
|
693
|
+
begin
|
694
|
+
request('POST', uri: uri, header: headers, body: form)
|
695
|
+
ensure
|
696
|
+
# Reset source offset to original position
|
697
|
+
source.pos=original_pos if source.respond_to?(:pos=)
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
# Download file
|
702
|
+
#
|
703
|
+
# @param path [String] path of file in end-user's account
|
704
|
+
# @param startbyte [Fixnum] starting byte (offset) in file
|
705
|
+
# @param bytecount [Fixnum] number of bytes to download
|
706
|
+
#
|
707
|
+
# @yield [String] chunk of data as soon as available,
|
708
|
+
# chunksize size may vary each time
|
709
|
+
# @return [String] file data is returned if no block given
|
710
|
+
#
|
711
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
712
|
+
# Errors::ArgumentError]
|
713
|
+
#
|
714
|
+
# @example
|
715
|
+
# Download into buffer
|
716
|
+
# buffer = rest_adapter.download(path, startbyte: 0, bytecount: 1000)
|
717
|
+
#
|
718
|
+
# Streaming download i.e. chunks are synchronously returned as soon as available
|
719
|
+
# preferable for large files download:
|
720
|
+
#
|
721
|
+
# ::File.open(local_filepath, 'wb') do |file|
|
722
|
+
# rest_adapter.download(path) { |buffer| file.write(buffer) }
|
723
|
+
# end
|
724
|
+
def download(path, startbyte: 0, bytecount: 0, &block)
|
725
|
+
fail Errors::ArgumentError,
|
726
|
+
'Invalid argument, must pass path' if Utils.is_blank?(path)
|
727
|
+
fail Errors::ArgumentError,
|
728
|
+
'Size must be positive' if (bytecount < 0 || startbyte < 0)
|
729
|
+
|
730
|
+
uri = set_uri_params(Constants::ENDPOINT_FILES, name: path)
|
731
|
+
header = Constants::HEADER_CONTENT_TYPE_APP_URLENCODED.dup
|
732
|
+
|
733
|
+
unless startbyte == 0 && bytecount == 0
|
734
|
+
if bytecount == 0
|
735
|
+
header[:Range] = "bytes=#{startbyte}-"
|
736
|
+
else
|
737
|
+
header[:Range] = "bytes=#{startbyte}-#{startbyte + bytecount - 1}"
|
738
|
+
end
|
739
|
+
end
|
740
|
+
|
741
|
+
request('GET', uri: uri, header: header, &block)
|
742
|
+
end
|
743
|
+
|
744
|
+
# Get the download URL of the file.
|
745
|
+
#
|
746
|
+
# @raise [RestAdapter::Errors::SessionNotLinked,
|
747
|
+
# RestAdapter::Errors::ServiceError,
|
748
|
+
# RestAdapter::Errors::ArgumentError,
|
749
|
+
# RestAdapter::Errors::InvalidItemError,
|
750
|
+
# RestAdapter::Errors::OperationNotAllowedError]
|
751
|
+
#
|
752
|
+
# @return [String] request response containing the download URL.
|
753
|
+
def download_url(path)
|
754
|
+
uri = set_uri_params(Constants::ENDPOINT_FILES, name: path)
|
755
|
+
header = {
|
756
|
+
Constants::HEADER_REDIRECT => false
|
757
|
+
}.merge(Constants::HEADER_CONTENT_TYPE_APP_URLENCODED.dup)
|
758
|
+
|
759
|
+
request('GET', uri: uri, header: header)
|
760
|
+
end
|
761
|
+
|
762
|
+
# List specified version of file
|
763
|
+
#
|
764
|
+
# @param path [String] file path
|
765
|
+
# @param version [Fixnum] desired version of the file referenced by path
|
766
|
+
#
|
767
|
+
# @return [Hash] metatdata passed version of file
|
768
|
+
#
|
769
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
770
|
+
# Errors::ArgumentError]
|
771
|
+
#
|
772
|
+
# @review If current version of file is passed, CloudFS Server
|
773
|
+
# returns unspecified error 9999, works for pervious file versions.
|
774
|
+
def list_single_file_version(path, version)
|
775
|
+
fail Errors::ArgumentError,
|
776
|
+
'Invalid argument, must pass valid path' if Utils.is_blank?(path)
|
777
|
+
fail Errors::ArgumentError,
|
778
|
+
'Invalid argument, must pass valid path' unless version.is_a?(Fixnum)
|
779
|
+
|
780
|
+
uri = set_uri_params(Constants::ENDPOINT_FILES, name: path,
|
781
|
+
operation: "versions/#{version}")
|
782
|
+
|
783
|
+
request('GET', uri: uri,
|
784
|
+
header: Constants::HEADER_CONTENT_TYPE_APP_URLENCODED)
|
785
|
+
end
|
786
|
+
|
787
|
+
# Given a specified version, set that version’s metadata to
|
788
|
+
# current metadata for the file, creating a new version in the process
|
789
|
+
#
|
790
|
+
# @param path [String] file path
|
791
|
+
# @param version [Fixnum] version of file specified by path
|
792
|
+
#
|
793
|
+
# @return [Hash] update metadata with new version number
|
794
|
+
#
|
795
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
796
|
+
# Errors::ArgumentError]
|
797
|
+
def promote_file_version(path, version)
|
798
|
+
fail Errors::ArgumentError,
|
799
|
+
'Invalid argument, must pass valid path' if Utils.is_blank?(path)
|
800
|
+
fail Errors::ArgumentError,
|
801
|
+
'Invalid argument, must pass valid version' unless version.is_a?(Fixnum)
|
802
|
+
|
803
|
+
uri = set_uri_params(Constants::ENDPOINT_FILES, name: path,
|
804
|
+
operation: "versions/#{version}")
|
805
|
+
query = {operation: Constants::QUERY_OPS_PROMOTE}
|
806
|
+
|
807
|
+
request('POST', uri: uri, query: query)
|
808
|
+
end
|
809
|
+
|
810
|
+
# List versions of file
|
811
|
+
#
|
812
|
+
# @param path [String] file path
|
813
|
+
# @param start_version [Fixnum] version number to begin listing file versions
|
814
|
+
# @param stop_version [Fixnum] version number from which to stop
|
815
|
+
# listing file versions
|
816
|
+
# @param limit [Fixnum] how many versions to list in the result set.
|
817
|
+
# It can be negative.
|
818
|
+
#
|
819
|
+
# @return [Array<Hash>] hashes representing metadata for selected versions
|
820
|
+
# of the file as recorded in the History
|
821
|
+
#
|
822
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
823
|
+
# Errors::ArgumentError]
|
824
|
+
#
|
825
|
+
# @review Returns empty items array if file has no old version
|
826
|
+
def list_file_versions(path, start_version: 0, stop_version: nil, limit: 10)
|
827
|
+
fail Errors::ArgumentError,
|
828
|
+
'Invalid argument, must pass valid path' if Utils.is_blank?(path)
|
829
|
+
|
830
|
+
uri = set_uri_params(Constants::ENDPOINT_FILES, name: path,
|
831
|
+
operation: 'versions')
|
832
|
+
|
833
|
+
query = {
|
834
|
+
:'start-version' => start_version, :'limit' => limit
|
835
|
+
}
|
836
|
+
query[:'stop-version'] = stop_version if stop_version
|
837
|
+
|
838
|
+
request('GET', uri: uri, query: query,
|
839
|
+
header: Constants::HEADER_CONTENT_TYPE_APP_URLENCODED)
|
840
|
+
end
|
841
|
+
|
842
|
+
# Creates a share of locations specified by the passed list of paths
|
843
|
+
#
|
844
|
+
# @param paths [Array<String>] array of file/folder paths in end-user's accoun
|
845
|
+
# @param password [String] password of the share
|
846
|
+
#
|
847
|
+
# @return [Hash] metadata of share
|
848
|
+
#
|
849
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
850
|
+
# Errors::ArgumentError]
|
851
|
+
#
|
852
|
+
# @review according to cloudfs rest doc: If the share points to a single item,
|
853
|
+
# only the share data is returned (not the item’s metadata).
|
854
|
+
# Observed only share data returned even when share points to multiple paths?
|
855
|
+
def create_share(paths, password: nil)
|
856
|
+
fail Errors::ArgumentError,
|
857
|
+
'Invalid argument, must pass valid list of paths' if Utils.is_blank?(paths)
|
858
|
+
|
859
|
+
body = [*paths].map { |path|
|
860
|
+
path = prepend_path_with_forward_slash(path)
|
861
|
+
"path=#{Utils.urlencode(path)}" }.join('&')
|
862
|
+
|
863
|
+
unless password.nil?
|
864
|
+
body += "&password=#{password}"
|
865
|
+
end
|
866
|
+
|
867
|
+
uri = {endpoint: Constants::ENDPOINT_SHARES}
|
868
|
+
|
869
|
+
request('POST', uri: uri, body: body)
|
870
|
+
end
|
871
|
+
|
872
|
+
# Deletes the user created share
|
873
|
+
#
|
874
|
+
# @param share_key [String] id of the share to be deleted
|
875
|
+
#
|
876
|
+
# @return [Hash] hash containing success string
|
877
|
+
#
|
878
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
879
|
+
# Errors::ArgumentError]
|
880
|
+
def delete_share(share_key)
|
881
|
+
fail Errors::ArgumentError,
|
882
|
+
'Invalid argument, must pass valid share key' if Utils.is_blank?(share_key)
|
883
|
+
|
884
|
+
uri = set_uri_params(Constants::ENDPOINT_SHARES, name: "#{share_key}/")
|
885
|
+
|
886
|
+
request('DELETE', uri: uri,
|
887
|
+
header: Constants::HEADER_CONTENT_TYPE_APP_URLENCODED)
|
888
|
+
end
|
889
|
+
|
890
|
+
# List files and folders in a share
|
891
|
+
#
|
892
|
+
# @param share_key [String] id of the share
|
893
|
+
# @param path [String] path to any folder in share, default is root of share
|
894
|
+
#
|
895
|
+
# @return [Hash] metadata of browsed path in share defaults share,
|
896
|
+
# share's metadata and array of hashes representing list of items
|
897
|
+
# under browsed item if folder - { :meta => Hash, share: Hash, :items => Array<Hash> }
|
898
|
+
#
|
899
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError, Errors::ArgumentError]
|
900
|
+
def browse_share(share_key, path: nil)
|
901
|
+
fail Errors::ArgumentError,
|
902
|
+
'Invalid argument, must pass valid path' if Utils.is_blank?(share_key)
|
903
|
+
|
904
|
+
uri = set_uri_params(Constants::ENDPOINT_SHARES,
|
905
|
+
name: "#{share_key}#{path}", operation: 'meta')
|
906
|
+
|
907
|
+
request('GET', uri: uri,
|
908
|
+
header: Constants::HEADER_CONTENT_TYPE_APP_URLENCODED)
|
909
|
+
end
|
910
|
+
|
911
|
+
# Lists the metadata of the shares the authenticated user has created
|
912
|
+
#
|
913
|
+
# @return [Array<Hash>] metatdata of user's shares
|
914
|
+
#
|
915
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError]
|
916
|
+
def list_shares
|
917
|
+
uri = {endpoint: Constants::ENDPOINT_SHARES}
|
918
|
+
|
919
|
+
request('GET', uri: uri,
|
920
|
+
header: Constants::HEADER_CONTENT_TYPE_APP_URLENCODED)
|
921
|
+
end
|
922
|
+
|
923
|
+
# Add contents of share to user's filesystem
|
924
|
+
#
|
925
|
+
# @param share_key [String] id of the share
|
926
|
+
# @param path [String] default root, path in user's account to
|
927
|
+
# receive share at
|
928
|
+
# @param exists [String] ('RENAME', 'FAIL', 'OVERWRITE']
|
929
|
+
#
|
930
|
+
# @return [Array<Hash>] metadata of files and folders in share
|
931
|
+
#
|
932
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
933
|
+
# Errors::ArgumentError]
|
934
|
+
def receive_share(share_key, path: nil, exists: 'RENAME')
|
935
|
+
fail Errors::ArgumentError,
|
936
|
+
'Invalid argument, must pass valid share key' if Utils.is_blank?(share_key)
|
937
|
+
exists = Constants::EXISTS.fetch(exists.to_sym) {
|
938
|
+
fail Errors::ArgumentError, 'Invalid value for exists' }
|
939
|
+
|
940
|
+
uri = set_uri_params(Constants::ENDPOINT_SHARES, name: "#{share_key}/")
|
941
|
+
form = {exists: exists}
|
942
|
+
form[:path] = path unless Utils.is_blank?(path)
|
943
|
+
|
944
|
+
request('POST', uri: uri, body: form)
|
945
|
+
end
|
946
|
+
|
947
|
+
# Unlock share
|
948
|
+
#
|
949
|
+
# @param share_key [String] id of the share
|
950
|
+
# @param password [String] password of share
|
951
|
+
#
|
952
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
953
|
+
# Errors::ArgumentError]
|
954
|
+
def unlock_share(share_key, password)
|
955
|
+
fail Errors::ArgumentError,
|
956
|
+
'Invalid argument, must pass valid path' if Utils.is_blank?(share_key)
|
957
|
+
fail Errors::ArgumentError,
|
958
|
+
'Invalid argument, must pass valid path' if Utils.is_blank?(password)
|
959
|
+
|
960
|
+
uri = set_uri_params(Constants::ENDPOINT_SHARES, name: share_key,
|
961
|
+
operation: 'unlock')
|
962
|
+
form = {password: password}
|
963
|
+
|
964
|
+
request('POST', uri: uri, body: form)
|
965
|
+
end
|
966
|
+
|
967
|
+
# Alter share info
|
968
|
+
# changes, adds, or removes the share’s password or updates the name
|
969
|
+
#
|
970
|
+
# @param share_key [String] id of the share whose attributes are to be changed
|
971
|
+
# @param current_password [String] current password for this share,
|
972
|
+
# if has been set, it is necessary even if share has been unlocked
|
973
|
+
# @param password [String] new password of share
|
974
|
+
# @param name [String] new name of share
|
975
|
+
#
|
976
|
+
# @return [Hash] updated metadata of share
|
977
|
+
#
|
978
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
979
|
+
# Errors::ArgumentError]
|
980
|
+
#
|
981
|
+
# @review remove password has not been tested
|
982
|
+
def alter_share_info(share_key, current_password: nil,
|
983
|
+
password: nil, name: nil)
|
984
|
+
fail Errors::ArgumentError,
|
985
|
+
'Invalid argument, must pass valid path' if Utils.is_blank?(share_key)
|
986
|
+
|
987
|
+
uri = set_uri_params(Constants::ENDPOINT_SHARES, name: share_key,
|
988
|
+
operation: 'info')
|
989
|
+
form = {}
|
990
|
+
form[:current_password] = current_password if current_password
|
991
|
+
form[:password] = password if password
|
992
|
+
form[:name] = name unless Utils.is_blank?(name)
|
993
|
+
|
994
|
+
request('POST', uri: uri, body: form)
|
995
|
+
end
|
996
|
+
|
997
|
+
# List the history of file, folder, and share actions
|
998
|
+
#
|
999
|
+
# @param start [Fixnum] version number to start listing historical
|
1000
|
+
# actions from,
|
1001
|
+
# default -10. It can be negative in order to get most recent actions.
|
1002
|
+
# @param stop [Fixnum] version number to stop listing historical actions
|
1003
|
+
# from (non-inclusive)
|
1004
|
+
#
|
1005
|
+
# @return [Array<Hash>] history items
|
1006
|
+
#
|
1007
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError]
|
1008
|
+
def list_history(start: -10, stop: nil)
|
1009
|
+
uri = {endpoint: Constants::ENDPOINT_HISTORY}
|
1010
|
+
query = {start: start}
|
1011
|
+
query[:stop] = stop if stop
|
1012
|
+
|
1013
|
+
request('GET', uri: uri, query: query,
|
1014
|
+
header: Constants::HEADER_CONTENT_TYPE_APP_URLENCODED)
|
1015
|
+
end
|
1016
|
+
|
1017
|
+
# List files and folders in trash at specified path
|
1018
|
+
#
|
1019
|
+
# @param path [String] path to location in user's trash, defaults to
|
1020
|
+
# root of trash
|
1021
|
+
#
|
1022
|
+
# @return [Hash] metadata of browsed trash item and array of hashes
|
1023
|
+
# representing list of items under browsed item if folder -
|
1024
|
+
# { :meta => Hash, :items => <Hash> }
|
1025
|
+
#
|
1026
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError]
|
1027
|
+
def browse_trash(path: nil)
|
1028
|
+
uri = set_uri_params(Constants::ENDPOINT_TRASH, name: path)
|
1029
|
+
request('GET', uri: uri,
|
1030
|
+
header: Constants::HEADER_CONTENT_TYPE_APP_URLENCODED)
|
1031
|
+
end
|
1032
|
+
|
1033
|
+
# Delete trash item
|
1034
|
+
#
|
1035
|
+
# @param path [String] default: trash root, path to location in user's trash,
|
1036
|
+
# default all trash items are deleted
|
1037
|
+
#
|
1038
|
+
# @return [Hash] containing success: true
|
1039
|
+
#
|
1040
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError]
|
1041
|
+
#
|
1042
|
+
# @review CloudFS Server returns Unspecified Error 9999 if no path provided,
|
1043
|
+
# expected behaviour is to delete all items in trash
|
1044
|
+
def delete_trash_item(path: nil)
|
1045
|
+
uri = set_uri_params(Constants::ENDPOINT_TRASH, name: path)
|
1046
|
+
request('DELETE', uri: uri,
|
1047
|
+
header: Constants::HEADER_CONTENT_TYPE_APP_URLENCODED)
|
1048
|
+
end
|
1049
|
+
|
1050
|
+
# Recover trash item
|
1051
|
+
#
|
1052
|
+
# @param path [String] path to location in user's trash
|
1053
|
+
# @param restore [String] ('FAIL', 'RESCUE', 'RECREATE') action to take
|
1054
|
+
# if recovery operation encounters issues
|
1055
|
+
# @param destination [String] rescue (default root) or recreate(named path)
|
1056
|
+
# path depending on exists option to place item into if the original
|
1057
|
+
# path does not exist
|
1058
|
+
#
|
1059
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError,
|
1060
|
+
# Errors::ArgumentError]
|
1061
|
+
def recover_trash_item(path, restore: 'FAIL', destination: nil)
|
1062
|
+
fail Errors::ArgumentError,
|
1063
|
+
'Invalid argument, must pass valid path' if Utils.is_blank?(path)
|
1064
|
+
restore = Constants::RESTORE_METHOD.fetch(restore.to_sym) {
|
1065
|
+
fail Errors::ArgumentError, 'Invalid value for restore' }
|
1066
|
+
|
1067
|
+
uri = set_uri_params(Constants::ENDPOINT_TRASH, name: path)
|
1068
|
+
|
1069
|
+
form = {:'restore' => restore}
|
1070
|
+
if restore == Constants::RESTORE_METHOD[:RESCUE]
|
1071
|
+
unless Utils.is_blank?(destination)
|
1072
|
+
destination = prepend_path_with_forward_slash(destination)
|
1073
|
+
form[:'rescue-path'] = destination
|
1074
|
+
end
|
1075
|
+
elsif restore == Constants::RESTORE_METHOD[:RECREATE]
|
1076
|
+
unless Utils.is_blank?(destination)
|
1077
|
+
destination = prepend_path_with_forward_slash(destination)
|
1078
|
+
form[:'recreate-path'] = destination
|
1079
|
+
end
|
1080
|
+
end
|
1081
|
+
|
1082
|
+
request('POST', uri: uri, body: form)
|
1083
|
+
end
|
1084
|
+
|
1085
|
+
# Common private method to send http request to cloudfs service
|
1086
|
+
#
|
1087
|
+
# @param method [String, Symbol] ('GET', 'POST', 'DELETE') http verb
|
1088
|
+
#
|
1089
|
+
# @param uri [Hash] containing endpoint and name that is endpoint suffix
|
1090
|
+
# uri: { :endpoint => "/v2/folders", :name => "{ path }/meta" }
|
1091
|
+
# @param header [Hash] containing key:value pairs for request header
|
1092
|
+
# @param query [Hash] containing key:value pairs of query
|
1093
|
+
# @param body [Hash, String] containing key:value pairs for post forms-
|
1094
|
+
# body: { :grant_type => "password", :password => "xyz" },
|
1095
|
+
# body: { :file => (File,StringIO), :name => "name" }
|
1096
|
+
# body: "path=pathid&path=pathdid&path=pathid"
|
1097
|
+
#
|
1098
|
+
# @return [Hash, String] containing result from cloudfs service or file data
|
1099
|
+
#
|
1100
|
+
# @raise [Errors::SessionNotLinked, Errors::ServiceError]
|
1101
|
+
def request(method, uri: {}, header: {}, query: {}, body: {}, &block)
|
1102
|
+
header = {
|
1103
|
+
Constants::HEADER_AUTHORIZATION => "Bearer #{@access_token}"
|
1104
|
+
}.merge(header)
|
1105
|
+
|
1106
|
+
unless uri[:endpoint] == Constants::ENDPOINT_OAUTH ||
|
1107
|
+
uri[:endpoint] == Constants::ENDPOINT_CUSTOMERS
|
1108
|
+
fail Errors::SessionNotLinked if Utils.is_blank?(@access_token)
|
1109
|
+
end
|
1110
|
+
|
1111
|
+
url = create_url(@host, endpoint: uri[:endpoint], name: uri[:name])
|
1112
|
+
body = set_multipart_upload_body(body)
|
1113
|
+
response = @http_connection.request(
|
1114
|
+
method,
|
1115
|
+
url,
|
1116
|
+
query: query,
|
1117
|
+
header: header,
|
1118
|
+
body: body, &block)
|
1119
|
+
|
1120
|
+
parse_response(response)
|
1121
|
+
rescue Errors::ServerError
|
1122
|
+
Errors::raise_service_error($!)
|
1123
|
+
end
|
1124
|
+
|
1125
|
+
# Set multipart body for file upload
|
1126
|
+
#
|
1127
|
+
# @param body [Hash]
|
1128
|
+
#
|
1129
|
+
# @return [Array<Hash>] multipart upload forms
|
1130
|
+
def set_multipart_upload_body(body={})
|
1131
|
+
return body unless body.is_a?(Hash) && body.key?(:file)
|
1132
|
+
|
1133
|
+
file = body[:file]
|
1134
|
+
exists = body[:exists]
|
1135
|
+
|
1136
|
+
if Utils.is_blank?(body[:name])
|
1137
|
+
path = file.respond_to?(:path) ? file.path : ''
|
1138
|
+
filename = ::File.basename(path)
|
1139
|
+
else
|
1140
|
+
filename = body[:name]
|
1141
|
+
end
|
1142
|
+
|
1143
|
+
multipart_body = []
|
1144
|
+
multipart_body << {'Content-Disposition' => 'form-data; name="exists"',
|
1145
|
+
:content => exists} if exists
|
1146
|
+
multipart_body << {'Content-Disposition' =>
|
1147
|
+
"form-data; name=\"file\"; filename=\"#{filename}\"",
|
1148
|
+
'Content-Type' => 'application/octet-stream',
|
1149
|
+
:content => file}
|
1150
|
+
multipart_body
|
1151
|
+
end
|
1152
|
+
|
1153
|
+
# Create url
|
1154
|
+
# appends endpoint and name prefix to host
|
1155
|
+
#
|
1156
|
+
# @param host [String] server address
|
1157
|
+
# @param endpoint [String] server endpoint
|
1158
|
+
# @param name [String] name prefix
|
1159
|
+
#
|
1160
|
+
# @return [String] url
|
1161
|
+
def create_url(host, endpoint: nil, name: nil)
|
1162
|
+
"#{host}#{endpoint}#{name}"
|
1163
|
+
end
|
1164
|
+
|
1165
|
+
# Create response
|
1166
|
+
# parses cloudfs service response into hash
|
1167
|
+
#
|
1168
|
+
# @param response [Hash]
|
1169
|
+
# @see CloudFS::RestAdapter::Connection#request
|
1170
|
+
#
|
1171
|
+
# @return [Hash] response from cloudfs service
|
1172
|
+
def parse_response(response)
|
1173
|
+
if response[:content_type] &&
|
1174
|
+
response[:content_type].include?('application/json')
|
1175
|
+
|
1176
|
+
resp = Utils.json_to_hash(response.fetch(:content))
|
1177
|
+
resp.fetch(:result, resp)
|
1178
|
+
else
|
1179
|
+
response.fetch(:content)
|
1180
|
+
end
|
1181
|
+
end
|
1182
|
+
|
1183
|
+
# Prepend path with '/'
|
1184
|
+
#
|
1185
|
+
# @param [String, nil] path
|
1186
|
+
#
|
1187
|
+
# @return [String] path
|
1188
|
+
def prepend_path_with_forward_slash(path)
|
1189
|
+
if Utils.is_blank?(path)
|
1190
|
+
path = '/'
|
1191
|
+
elsif path[0] != '/'
|
1192
|
+
path = "#{path}".insert(0, '/')
|
1193
|
+
end
|
1194
|
+
path
|
1195
|
+
end
|
1196
|
+
|
1197
|
+
# Set uri params
|
1198
|
+
#
|
1199
|
+
# @param endpoint [String] server endpoint
|
1200
|
+
# @param name [String] path prefix
|
1201
|
+
# @param operation [String] path prefix
|
1202
|
+
#
|
1203
|
+
# @return [Hash] uri { :endpoint => "/v2/xyz", :name => "/abc/meta" }
|
1204
|
+
#
|
1205
|
+
# @optimize clean this method
|
1206
|
+
def set_uri_params(endpoint, name: nil, operation: nil)
|
1207
|
+
uri = {endpoint: endpoint}
|
1208
|
+
delim = nil
|
1209
|
+
# removing new line and spaces from end and begining of name
|
1210
|
+
unless Utils.is_blank?(name)
|
1211
|
+
name = name.strip
|
1212
|
+
delim = '/' unless name[-1] == '/'
|
1213
|
+
end
|
1214
|
+
# append to name with delim if operation is given
|
1215
|
+
name = "#{name}#{delim}#{operation}" unless Utils.is_blank?(operation)
|
1216
|
+
unless Utils.is_blank?(name)
|
1217
|
+
if endpoint.to_s[-1] == '/' && name[0] == '/'
|
1218
|
+
# remove leading / from name if endpoint has traling /
|
1219
|
+
name = name[1..-1]
|
1220
|
+
elsif endpoint.to_s[-1] != '/' && name.to_s[0] != '/'
|
1221
|
+
# insert leading / to name
|
1222
|
+
name = "#{name}".insert(0, '/')
|
1223
|
+
end
|
1224
|
+
uri[:name] = name
|
1225
|
+
end
|
1226
|
+
uri
|
1227
|
+
end
|
1228
|
+
|
1229
|
+
private :delete, :copy, :move, :get_meta, :alter_meta, :request,
|
1230
|
+
:set_multipart_upload_body, :parse_response, :create_url,
|
1231
|
+
:prepend_path_with_forward_slash, :set_uri_params
|
1232
|
+
end
|
1233
|
+
end
|