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,93 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'base64'
|
3
|
+
require 'openssl'
|
4
|
+
require 'multi_json'
|
5
|
+
|
6
|
+
module CloudFS
|
7
|
+
class RestAdapter
|
8
|
+
# @private
|
9
|
+
# Utility functions module, used to handle common tasks
|
10
|
+
module Utils
|
11
|
+
extend self
|
12
|
+
# Urlencode value
|
13
|
+
#
|
14
|
+
# @param value [#to_s] to urlencode
|
15
|
+
# @return [String] url encoded string
|
16
|
+
def urlencode(value)
|
17
|
+
CGI.escape("#{value}")
|
18
|
+
end
|
19
|
+
|
20
|
+
# Converts hash to url encoded string
|
21
|
+
#
|
22
|
+
# @param hash [Hash] hash to be converted
|
23
|
+
# @param delim [#to_s]
|
24
|
+
# @param join_with [#to_s]
|
25
|
+
#
|
26
|
+
# @return [String] url encode string
|
27
|
+
# "#{ key }#{ delim }#{ value }#{ join_with }#{ key }#{ delim }#{ value }"
|
28
|
+
# @optimize does not handle nested hash
|
29
|
+
def hash_to_urlencoded_str(hash = {}, delim, join_with)
|
30
|
+
hash.map { |k, v|
|
31
|
+
"#{urlencode(k)}#{delim}#{urlencode(v)}" }.join("#{join_with}")
|
32
|
+
end
|
33
|
+
|
34
|
+
# Sorts hash by key.downcase
|
35
|
+
# @param hash [Hash] unsorted hash
|
36
|
+
# @return [Hash] sorted hash
|
37
|
+
def sort_hash(hash={})
|
38
|
+
sorted_hash = {}
|
39
|
+
hash.sort_by { |k, _| k.to_s.downcase }.each { |k, v| sorted_hash["#{k}"] = v }
|
40
|
+
sorted_hash
|
41
|
+
end
|
42
|
+
|
43
|
+
# Generate OAuth2 signature based on cloudfs
|
44
|
+
# signature calculation algorithm
|
45
|
+
#
|
46
|
+
# @param endpoint [String] server endpoint
|
47
|
+
# @param params [Hash] form data
|
48
|
+
# @param headers [Hash] http request headers
|
49
|
+
# @param secret [Hash] cloudfs account secret
|
50
|
+
#
|
51
|
+
# @return [String] OAuth2 signature
|
52
|
+
def generate_auth_signature(endpoint, params, headers, secret)
|
53
|
+
params_sorted = sort_hash(params)
|
54
|
+
params_encoded = hash_to_urlencoded_str(params_sorted, '=', '&')
|
55
|
+
headers_encoded = hash_to_urlencoded_str(headers, ':', '&')
|
56
|
+
string_to_sign = "POST&#{endpoint}&#{params_encoded}&#{headers_encoded}"
|
57
|
+
hmac_str = OpenSSL::HMAC.digest('sha1', secret, string_to_sign)
|
58
|
+
Base64.strict_encode64(hmac_str)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Coverts Json sting to hash
|
62
|
+
# calls MultiJson#load
|
63
|
+
# @param json_str [String] json format string
|
64
|
+
# @return [Hash] converted hash
|
65
|
+
def json_to_hash(json_str)
|
66
|
+
MultiJson.load(json_str, :symbolize_keys => true)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Converts hash to json string
|
70
|
+
# calls MultiJson#dump
|
71
|
+
# @param hash [Hash] hash to convert
|
72
|
+
# @return [String] json formated string
|
73
|
+
def hash_to_json(hash={})
|
74
|
+
MultiJson.dump(hash)
|
75
|
+
end
|
76
|
+
|
77
|
+
# @param hash [Hash]
|
78
|
+
# @option filed [Array<String>, Array<Symbol>]
|
79
|
+
# @return [Array<Object>] values at found fields
|
80
|
+
def hash_to_arguments(hash, *field)
|
81
|
+
if field.any? { |f| hash.key?(f) }
|
82
|
+
hash.values_at(*field)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [Boolean] whether variable is nil, empty
|
87
|
+
def is_blank?(var)
|
88
|
+
var.respond_to?(:empty?) ? var.empty? : !var
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative 'item'
|
2
|
+
require_relative 'rest_adapter'
|
3
|
+
require_relative 'filesystem_common'
|
4
|
+
|
5
|
+
module CloudFS
|
6
|
+
# Base class for {Folder}
|
7
|
+
#
|
8
|
+
# @author Mrinal Dhillon
|
9
|
+
# @example
|
10
|
+
# folder = session.filesystem.root.create_folder(name_of_folder)
|
11
|
+
# folder.list # => []
|
12
|
+
# session.filesystem.root.list # => Array<File, Folder>
|
13
|
+
class Container < Item
|
14
|
+
# @see Item#initialize
|
15
|
+
def initialize(rest_adapter, parent: nil, parent_state: nil, in_trash: false,
|
16
|
+
in_share: false, ** properties)
|
17
|
+
fail RestAdapter::Errors::ArgumentError,
|
18
|
+
"Invalid item of type #{properties[:type]}" unless properties[:type] ==
|
19
|
+
'folder' || properties[:type] == 'root'
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
# List contents of this container
|
24
|
+
#
|
25
|
+
# @return [Array<Folder, File>] list of items
|
26
|
+
# @raise [RestAdapter::Errors::SessionNotLinked, RestAdapter::Errors::ServiceError,
|
27
|
+
# RestAdapter::Errors::InvalidItemError]
|
28
|
+
def list
|
29
|
+
fail RestAdapter::Errors::InvalidItemError,
|
30
|
+
'Operation not allowed as item does not exist anymore' unless exists?
|
31
|
+
|
32
|
+
if @in_share
|
33
|
+
response = @rest_adapter.browse_share(@state[:share_key], path: @url).fetch(:items)
|
34
|
+
elsif @in_trash
|
35
|
+
response = @rest_adapter.browse_trash(path: @url).fetch(:items)
|
36
|
+
else
|
37
|
+
response = @rest_adapter.list_folder(path: @url, depth: 1)
|
38
|
+
end
|
39
|
+
FileSystemCommon.create_items_from_hash_array(
|
40
|
+
response,
|
41
|
+
@rest_adapter,
|
42
|
+
parent: @url,
|
43
|
+
in_share: @in_share,
|
44
|
+
in_trash: @in_trash,
|
45
|
+
parent_state: @state)
|
46
|
+
end
|
47
|
+
|
48
|
+
# overriding inherited properties that are not not valid for folder
|
49
|
+
private :blocklist_key, :blocklist_id, :versions, :old_version?
|
50
|
+
end
|
51
|
+
end
|
data/lib/cloudfs/file.rb
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
require_relative 'item'
|
2
|
+
require_relative 'rest_adapter'
|
3
|
+
require_relative 'filesystem_common'
|
4
|
+
|
5
|
+
module CloudFS
|
6
|
+
# File class is aimed to provide native File object like interface
|
7
|
+
# to cloudfs files
|
8
|
+
#
|
9
|
+
# @author Mrinal Dhillon
|
10
|
+
# @example
|
11
|
+
# file = session.filesystem.root.upload(local_file_path)
|
12
|
+
# file.seek(4, IO::SEEK_SET) #=> 4
|
13
|
+
# file.tell #=> 4
|
14
|
+
# file.read #=> " is some buffer till end of file"
|
15
|
+
# file.rewind
|
16
|
+
# file.read {|chunk| puts chunk} #=> "this is some buffer till end of file"
|
17
|
+
# file.download(local_folder_path, filename: new_name_of_downloaded_file)
|
18
|
+
class File < Item
|
19
|
+
|
20
|
+
# @return [String] the size.
|
21
|
+
attr_reader :size
|
22
|
+
|
23
|
+
# @return [String] the extension.
|
24
|
+
attr_reader :extension
|
25
|
+
|
26
|
+
# @return [String] the mime type of file
|
27
|
+
attr_reader :mime
|
28
|
+
|
29
|
+
# Sets the mime type of the item and updates to CloudFS
|
30
|
+
def mime=(value)
|
31
|
+
fail RestAdapter::Errors::ArgumentError,
|
32
|
+
'Invalid input, expected new mime' if RestAdapter::Utils.is_blank?(value)
|
33
|
+
|
34
|
+
@mime = value
|
35
|
+
@changed_properties[:mime] = value
|
36
|
+
change_attributes(@changed_properties)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @see #extension
|
40
|
+
def extension=(value)
|
41
|
+
FileSystemCommon.validate_item_state(self)
|
42
|
+
@extension = value
|
43
|
+
@changed_properties[:extension] = value
|
44
|
+
end
|
45
|
+
|
46
|
+
# @see Item#initialize
|
47
|
+
def initialize(rest_adapter, parent: nil, parent_state: nil, in_trash: false,
|
48
|
+
in_share: false, old_version: false, ** properties)
|
49
|
+
fail RestAdapter::Errors::ArgumentError,
|
50
|
+
"Invalid item of type #{properties[:type]}" unless properties[:type] == 'file'
|
51
|
+
|
52
|
+
@offset = 0
|
53
|
+
super
|
54
|
+
end
|
55
|
+
|
56
|
+
# Download this file to local directory
|
57
|
+
#
|
58
|
+
# @param local_destination_path [String] path of local folder
|
59
|
+
# @param filename [String] name of downloaded file, default is name of this file
|
60
|
+
# @yield [Integer] download progress.
|
61
|
+
#
|
62
|
+
# @return [true]
|
63
|
+
#
|
64
|
+
# @raise [RestAdapter::Errors::SessionNotLinked, RestAdapter::Errors::ServiceError,
|
65
|
+
# RestAdapter::Errors::ArgumentError, RestAdapter::Errors::InvalidItemError,
|
66
|
+
# RestAdapter::Errors::OperationNotAllowedError]
|
67
|
+
# @review overwrites a file if it exists at local path
|
68
|
+
# @note Internally uses chunked stream download,
|
69
|
+
# max size of in-memory chunk is 16KB.
|
70
|
+
def download(local_destination_path, filename: nil, &block)
|
71
|
+
fail RestAdapter::Errors::ArgumentError,
|
72
|
+
'local path is not a valid directory' unless ::File.directory?(local_destination_path)
|
73
|
+
FileSystemCommon.validate_item_state(self)
|
74
|
+
|
75
|
+
filename ||= @name
|
76
|
+
if local_destination_path[-1] == '/'
|
77
|
+
local_filepath = "#{local_destination_path}#{filename}"
|
78
|
+
else
|
79
|
+
local_filepath = "#{local_destination_path}/#{filename}"
|
80
|
+
end
|
81
|
+
::File.open(local_filepath, 'wb') do |file|
|
82
|
+
downloaded = 0
|
83
|
+
@rest_adapter.download(@url) do |buffer|
|
84
|
+
downloaded += buffer.size
|
85
|
+
file.write(buffer)
|
86
|
+
yield @size, downloaded if block_given?
|
87
|
+
end
|
88
|
+
end
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
92
|
+
# Get the download URL of the file.
|
93
|
+
# @raise [RestAdapter::Errors::SessionNotLinked, RestAdapter::Errors::ServiceError,
|
94
|
+
# RestAdapter::Errors::ArgumentError, RestAdapter::Errors::InvalidItemError,
|
95
|
+
# RestAdapter::Errors::OperationNotAllowedError]
|
96
|
+
# @return [String] download URL of the file.
|
97
|
+
def download_url
|
98
|
+
url = @rest_adapter.download_url(@url)
|
99
|
+
URI.extract(url).first.chomp(';')
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
# Read from file to buffer
|
104
|
+
#
|
105
|
+
# @param bytecount [Fixnum] number of bytes to read from current access position
|
106
|
+
# @return [String] buffer
|
107
|
+
# @raise [RestAdapter::Errors::SessionNotLinked, RestAdapter::Errors::ServiceError]
|
108
|
+
def read_to_buffer(bytecount)
|
109
|
+
buffer = @rest_adapter.download(@url, startbyte: @offset, bytecount: bytecount)
|
110
|
+
@offset += buffer.nil? ? 0 : buffer.size
|
111
|
+
buffer
|
112
|
+
end
|
113
|
+
|
114
|
+
# Read from file to proc
|
115
|
+
#
|
116
|
+
# @param bytecount [Fixnum] number of bytes to read from current access position
|
117
|
+
# @yield [String] chunk of data as soon as available,
|
118
|
+
# chunksize size may vary each time
|
119
|
+
# @raise [RestAdapter::Errors::SessionNotLinked, RestAdapter::Errors::ServiceError]
|
120
|
+
def read_to_proc(bytecount, &block)
|
121
|
+
@rest_adapter.download(@url, startbyte: @offset, bytecount: bytecount) do |chunk|
|
122
|
+
@offset += chunk.nil? ? 0 : chunk.size
|
123
|
+
yield chunk
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Read from file
|
128
|
+
#
|
129
|
+
# @param bytecount [Fixnum] number of bytes to read from
|
130
|
+
# current access position, default reads upto end of file
|
131
|
+
#
|
132
|
+
# @yield [String] chunk data as soon as available,
|
133
|
+
# chunksize size may vary each time
|
134
|
+
# @return [String] buffer, unless block is given
|
135
|
+
# @raise [RestAdapter::Errors::SessionNotLinked, RestAdapter::Errors::ServiceError,
|
136
|
+
# RestAdapter::Errors::ArgumentError, RestAdapter::Errors::InvalidItemError,
|
137
|
+
# RestAdapter::Errors::OperationNotAllowedError]
|
138
|
+
#
|
139
|
+
# @note Pass block to stream chunks as soon as available,
|
140
|
+
# preferable for large reads.
|
141
|
+
def read(bytecount: nil, &block)
|
142
|
+
fail RestAdapter::Errors::ArgumentError,
|
143
|
+
"Negative length given - #{bytecount}" if bytecount && bytecount < 0
|
144
|
+
FileSystemCommon.validate_item_state(self)
|
145
|
+
|
146
|
+
if bytecount == 0 || @offset >= @size
|
147
|
+
return yield '' if block_given?
|
148
|
+
return ''
|
149
|
+
end
|
150
|
+
|
151
|
+
# read till end of file if no bytecount is given
|
152
|
+
# or offset + bytecount > size of file
|
153
|
+
bytecount = @size - @offset if bytecount.nil? || (@offset + bytecount > @size)
|
154
|
+
|
155
|
+
if block_given?
|
156
|
+
read_to_proc(bytecount, &block)
|
157
|
+
else
|
158
|
+
read_to_buffer(bytecount)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Reset position indicator
|
163
|
+
def rewind
|
164
|
+
@offset = 0
|
165
|
+
end
|
166
|
+
|
167
|
+
# Return current access position in this file
|
168
|
+
# @return [Fixnum] current position in file
|
169
|
+
def tell
|
170
|
+
@offset
|
171
|
+
end
|
172
|
+
|
173
|
+
# Seek to a particular byte in this file
|
174
|
+
# @param offset [Fixnum] offset in this file to seek to
|
175
|
+
# @param whence [Fixnum] defaults 0,
|
176
|
+
# If whence is 0 file offset shall be set to offset bytes
|
177
|
+
# If whence is 1, the file offset shall be set to its
|
178
|
+
# current location plus offset
|
179
|
+
# If whence is 2, the file offset shall be set to the size of
|
180
|
+
# the file plus offset
|
181
|
+
# @return [Fixnum] resulting offset
|
182
|
+
# @raise [RestAdapter::Errors::ArgumentError]
|
183
|
+
def seek(offset, whence: 0)
|
184
|
+
|
185
|
+
case whence
|
186
|
+
when 0
|
187
|
+
@offset = offset if whence == 0
|
188
|
+
when 1
|
189
|
+
@offset += offset if whence == 1
|
190
|
+
when 2
|
191
|
+
@offset = @size + offset if whence == 2
|
192
|
+
else
|
193
|
+
fail RestAdapter::Errors::ArgumentError,
|
194
|
+
'Invalid value of whence, should be 0 or IO::SEEK_SET, 1 or IO::SEEK_CUR, 2 or IO::SEEK_END'
|
195
|
+
end
|
196
|
+
|
197
|
+
@offset
|
198
|
+
end
|
199
|
+
|
200
|
+
# @return [String]
|
201
|
+
# @!visibility private
|
202
|
+
def to_s
|
203
|
+
"#{self.class}: url #{@url}, name: #{@name}, mime: #{@mime}, version: #{@version}, size: #{@size} bytes"
|
204
|
+
end
|
205
|
+
|
206
|
+
alias inspect to_s
|
207
|
+
private :read_to_buffer, :read_to_proc
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require_relative 'rest_adapter'
|
2
|
+
require_relative 'folder'
|
3
|
+
require_relative 'filesystem_common'
|
4
|
+
|
5
|
+
module CloudFS
|
6
|
+
# FileSystem class provides interface to maintain cloudfs user's filesystem
|
7
|
+
#
|
8
|
+
# @author Mrinal Dhillon
|
9
|
+
class FileSystem
|
10
|
+
|
11
|
+
# Get root object of filesystem
|
12
|
+
#
|
13
|
+
# @return [Folder] represents root folder of filesystem
|
14
|
+
#
|
15
|
+
# @raise RestAdapter::Errors::SessionNotLinked,
|
16
|
+
# RestAdapter::Errors::ServiceError
|
17
|
+
def root
|
18
|
+
response = @rest_adapter.get_folder_meta('/')
|
19
|
+
FileSystemCommon.create_item_from_hash(@rest_adapter, ** response)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param rest_adapter [RestAdapter] cloudfs RESTful api object
|
23
|
+
#
|
24
|
+
# @raise [RestAdapter::Errors::ArgumentError]
|
25
|
+
def initialize(rest_adapter)
|
26
|
+
fail RestAdapter::Errors::ArgumentError,
|
27
|
+
'invalid RestAdapter, input type must be RestAdapter' unless rest_adapter.is_a?(RestAdapter)
|
28
|
+
@rest_adapter = rest_adapter
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Array<File, Folder>] items in trash
|
32
|
+
#
|
33
|
+
# @raise [RestAdapter::Errors::SessionNotLinked,
|
34
|
+
# RestAdapter::Errors::ServiceError, RestAdapter::Errors::InvalidItemError,
|
35
|
+
# RestAdapter::Errors::OperationNotAllowedError]
|
36
|
+
def list_trash
|
37
|
+
response = @rest_adapter.browse_trash.fetch(:items)
|
38
|
+
FileSystemCommon.create_items_from_hash_array(
|
39
|
+
response,
|
40
|
+
@rest_adapter,
|
41
|
+
in_trash: true)
|
42
|
+
end
|
43
|
+
|
44
|
+
# List shares created by end-user
|
45
|
+
# @return [Array<Share>] shares
|
46
|
+
#
|
47
|
+
# @raise [RestAdapter::Errors::SessionNotLinked,
|
48
|
+
# RestAdapter::Errors::ServiceError]
|
49
|
+
def list_shares
|
50
|
+
response = @rest_adapter.list_shares
|
51
|
+
FileSystemCommon.create_items_from_hash_array(response, @rest_adapter)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Create share of paths in user's filesystem
|
55
|
+
#
|
56
|
+
# @param paths [Array<File, Folder, String>] file, folder or url
|
57
|
+
# @param password [String] password.
|
58
|
+
#
|
59
|
+
# @return [Share] instance
|
60
|
+
#
|
61
|
+
# @raise [RestAdapter::Errors::SessionNotLinked,
|
62
|
+
# RestAdapter::Errors::ServiceError, RestAdapter::Errors::ArgumentError,
|
63
|
+
# RestAdapter::Errors::InvalidItemError,
|
64
|
+
# RestAdapter::Errors::OperationNotAllowedError]
|
65
|
+
def create_share(paths, password: nil)
|
66
|
+
fail RestAdapter::Errors::ArgumentError,
|
67
|
+
'Invalid input, expected items or paths' unless paths
|
68
|
+
|
69
|
+
path_list = []
|
70
|
+
[*paths].each do |path|
|
71
|
+
FileSystemCommon.validate_item_state(path)
|
72
|
+
path_list << FileSystemCommon.get_item_url(path)
|
73
|
+
end
|
74
|
+
|
75
|
+
response = @rest_adapter.create_share(path_list, password: password)
|
76
|
+
FileSystemCommon.create_item_from_hash(@rest_adapter, ** response)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Fetches share associated with share key.
|
80
|
+
#
|
81
|
+
# @param share_key [String] valid share key
|
82
|
+
# @param password [String] password if share is locked
|
83
|
+
#
|
84
|
+
# @return [Share] instance of share
|
85
|
+
# @raise [RestAdapter::Errors::SessionNotLinked,
|
86
|
+
# RestAdapter::Errors::ServiceError, RestAdapter::Errors::ArgumentError]
|
87
|
+
#
|
88
|
+
# @note This method is intended for retrieving share from another user
|
89
|
+
def retrieve_share(share_key, password: nil)
|
90
|
+
fail RestAdapter::Errors::ArgumentError,
|
91
|
+
'Invalid input, expected items or paths' if RestAdapter::Utils.is_blank?(share_key)
|
92
|
+
|
93
|
+
@rest_adapter.unlock_share(share_key, password) if password
|
94
|
+
response = @rest_adapter.browse_share(share_key).fetch(:share)
|
95
|
+
FileSystemCommon.create_item_from_hash(@rest_adapter, ** response)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Get an item located in a given location.
|
99
|
+
def get_item(path)
|
100
|
+
fail RestAdapter::Errors::ArgumentError,
|
101
|
+
'Invalid input, expected item path' if RestAdapter::Utils.is_blank?(path)
|
102
|
+
|
103
|
+
if path.is_a?(String)
|
104
|
+
FileSystemCommon.get_item(@rest_adapter, path)
|
105
|
+
else
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|