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.
@@ -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