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