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,256 @@
1
+ require_relative 'rest_adapter'
2
+ require_relative 'account'
3
+ require_relative 'user'
4
+ require_relative 'filesystem'
5
+
6
+ module CloudFS
7
+ # Establishes a session with the api server on behalf of an authenticated
8
+ # end-user
9
+ #
10
+ # It maintains a RESTful low level api {CloudFS::Client} object
11
+ # that provides authenticated access to CloudFS service end user's
12
+ # account and is shared with file system objects linked with this session -
13
+ # {FileSystem}, {Container}, {File}, {Folder}, {Share}, {Account}, {User}
14
+ #
15
+ # @author Mrinal Dhillon
16
+ # @example
17
+ # session = CloudFS::Session.new(end_point, clientid, secret)
18
+ # session.is_linked? #=> false
19
+ # session.autheticate(username, password)
20
+ # session.is_linked? #=> true
21
+ # folder = session.filesystem.root.create_folder(folder_name)
22
+ # folder.name = newname
23
+ # folder.save
24
+ # file = folder.upload(local_filepath)
25
+ # session.unlink
26
+ class Session
27
+
28
+ # @!attribute [r] filesystem
29
+ #
30
+ # @return [FileSystem] {FileSystem} instance linked with this session
31
+ def filesystem
32
+ @filesystem ||= FileSystem.new(@rest_adapter)
33
+ end
34
+
35
+ # @!attribute [r] user
36
+ #
37
+ # @return [User] profile of end-user linked with this session
38
+ #
39
+ # @raise [RestAdapter::Errors::SessionNotLinked,
40
+ # RestAdapter::Errors::ServiceError,
41
+ # RestAdapter::Errors::OperationNotAllowedError]
42
+ def user
43
+ @user ||= get_user
44
+ end
45
+
46
+ # @!attribute [r] account
47
+ #
48
+ # @return [Account] end-user's account linked with this session
49
+ #
50
+ # @raise [RestAdapter::Errors::SessionNotLinked,
51
+ # RestAdapter::Errors::ServiceError,
52
+ # RestAdapter::Errors::OperationNotAllowedError]
53
+ def account
54
+ @account ||= get_account
55
+ end
56
+
57
+ # Set the admin credentials.
58
+ def set_admin_credentials(admin_client_id, admin_client_secret)
59
+ if RestAdapter::Utils.is_blank?(admin_client_id) || RestAdapter::Utils.is_blank?(admin_client_secret)
60
+ fail RestAdapter::Errors::ArgumentError,
61
+ 'Invalid input, expected admin client id and admin client secret'
62
+ end
63
+ @admin_credentials[:clientid] = admin_client_id ? admin_client_id : nil
64
+ @admin_credentials[:secret] = admin_client_secret ? admin_client_secret : nil
65
+ end
66
+
67
+ # @param end_point [String] cloudfs application api server hostname
68
+ # @param clientid [String] account clientid
69
+ # @param secret [String] account secret
70
+ # @param [Hash] http_conf RESTful connection configurations
71
+ # @option http_conf [Fixnum] :connect_timeout (60) for server handshake
72
+ # @option http_conf [Fixnum] :send_timeout (0) for send request,
73
+ # default is set to never, in order to support large uploads
74
+ # @option http_conf [Fixnum] :receive_timeout (120) for read timeout
75
+ # per block
76
+ # @option http_conf [Fixnum] :max_retry (3) for http 500 level errors
77
+ # @option http_conf [#<<] :http_debug (nil) to enable http debugging,
78
+ # example STDERR, STDOUT, {::File} object opened with
79
+ # permissions to write
80
+ #
81
+ # @optimize Configurable chunk size for chunked stream downloads,
82
+ # default is 16KB.
83
+ # Configurable keep alive timeout for persistent connections in
84
+ # connection pool, default is 15 seconds. Async api support
85
+ #
86
+ # @review optimum default values for http timeouts
87
+ def initialize(end_point, clientid, secret, ** http_conf)
88
+ @http_debug = http_conf[:http_debug]
89
+ @rest_adapter = RestAdapter.new(clientid, secret, end_point, ** http_conf)
90
+ @unlinked = false
91
+ @admin_credentials = {}
92
+ @admin_credentials[:host] = end_point ? end_point : 'access.bitcasa.com'
93
+ end
94
+
95
+ # Attempts to log into the end-user's filesystem, links this session
96
+ # to an account
97
+ #
98
+ # @param username [String] end-user's username
99
+ # @param password [String] end-user's password
100
+ #
101
+ # @return [true]
102
+ #
103
+ # @raise [RestAdapter::Errors::ServiceError,
104
+ # RestAdapter::Errors::ArgumentError,
105
+ # RestAdapter::Errors::OperationNotAllowedError]
106
+ def authenticate(username, password)
107
+ validate_session
108
+ fail RestAdapter::Errors::OperationNotAllowedError,
109
+ 'Cannot re-authenticate, initialize new session instance' if is_linked?
110
+ fail RestAdapter::Errors::ArgumentError,
111
+ 'Invalid argument, must pass username' if RestAdapter::Utils.is_blank?(username)
112
+ fail RestAdapter::Errors::ArgumentError,
113
+ 'Invalid argument, must pass password' if RestAdapter::Utils.is_blank?(password)
114
+
115
+ @rest_adapter.authenticate(username, password)
116
+ end
117
+
118
+ # @return [Boolean] whether current session is linked to the API server
119
+ # and can make authenticated requests
120
+ def is_linked?
121
+ @rest_adapter.linked?
122
+ end
123
+
124
+ # Discards current authentication
125
+ #
126
+ # @note CloudFS objects remain valid only till session is linked,
127
+ # once unlinked all RESTful objects generated through this session
128
+ # are expected to raise {RestAdapter::Errors::SessionNotLinked} exception
129
+ # for any RESTful operation.
130
+ #
131
+ # @note Session cannot be re-authenticated once unlinked.
132
+ #
133
+ # @return [true]
134
+ def unlink
135
+ @rest_adapter.unlink
136
+ @unlinked = true
137
+ end
138
+
139
+ # Creates a new end-user account for a Paid CloudFS account
140
+ #
141
+ # @param username [String] username of the end-user,
142
+ # must be at least 4 characters and less than 256 characters
143
+ # @param password [String] password of the end-user,
144
+ # must be at least 6 characters and has no length limit
145
+ # @param email [String] email of the end-user
146
+ # @param first_name [String] first name of end user
147
+ # @param last_name [String] last name of end user
148
+ # @param log_in_to_created_user [Boolean] authenticate the create users.
149
+ # @return [Account] new user account
150
+ #
151
+ # @raise [Client::Errors::ServiceError, Client::Errors::ArgumentError,
152
+ # Client::Errors::OperationNotAllowedError]
153
+ #
154
+ # @note Created {Account} is not linked,
155
+ # authenticate this session with new account credentials before using it.
156
+ #
157
+ # @example
158
+ # Initialize session, create new account and link session with
159
+ # created account's credentials
160
+ #
161
+ # # Set credentials of prototype account
162
+ # session = Session.new(clientid, secret, end_point)
163
+ #
164
+ # # Set credentials of Paid CloudFS admin account
165
+ # session.admin_credentials={ clientid: clientid, secret: secret }
166
+ #
167
+ # # Create account
168
+ # account = session.create_account(new_username, new_password)
169
+ # session.authenticate(new_username, new_password)
170
+ # account.usage #=> {Fixnum}
171
+ #
172
+ # @review Does not allow account creation if current session has already been
173
+ # authenticated. In such scenario account creation can be made possible
174
+ # but returning new {Account} instance with this session's RESTful client
175
+ # is not possible since session does not allow re-authentication.
176
+ def create_account(username, password, email: nil, first_name: nil,
177
+ last_name: nil, log_in_to_created_user: false)
178
+ validate_session
179
+ fail RestAdapter::Errors::OperationNotAllowedError,
180
+ 'New account creation with already linked session is not possible,
181
+ initialize new session instance' if is_linked?
182
+
183
+ fail RestAdapter::Errors::ArgumentError,
184
+ 'Invalid argument, must pass username' if RestAdapter::Utils.is_blank?(username)
185
+ fail RestAdapter::Errors::ArgumentError,
186
+ 'Invalid argument, must pass password' if RestAdapter::Utils.is_blank?(password)
187
+
188
+ if RestAdapter::Utils.is_blank?(@admin_credentials[:clientid]) ||
189
+ RestAdapter::Utils.is_blank?(@admin_credentials[:secret])
190
+ fail RestAdapter::Errors::ArgumentError,
191
+ 'Please set the admin credentials in order to create account.'
192
+ end
193
+
194
+ admin_client = RestAdapter.new(
195
+ @admin_credentials[:clientid],
196
+ @admin_credentials[:secret], @admin_credentials[:host],
197
+ http_debug: @http_debug)
198
+
199
+ begin
200
+ response = admin_client.create_account(
201
+ username,
202
+ password, email: email,
203
+ first_name: first_name,
204
+ last_name: last_name)
205
+
206
+ if log_in_to_created_user
207
+ admin_client.authenticate(username, password)
208
+ end
209
+
210
+ Account.new(@rest_adapter, ** response)
211
+ ensure
212
+ admin_client.unlink
213
+ end
214
+ end
215
+
216
+ # @see #account
217
+ def get_account
218
+ validate_session
219
+ response = @rest_adapter.get_profile
220
+ Account.new(@rest_adapter, ** response)
221
+ end
222
+
223
+ # @see #user
224
+ def get_user
225
+ validate_session
226
+ response = @rest_adapter.get_profile
227
+ User.new(@rest_adapter, ** response)
228
+ end
229
+
230
+ # Action history lists history of file, folder, and share actions
231
+ #
232
+ # @param start_version [Fixnum] version number to start listing historical
233
+ # actions from,
234
+ # default -10. It can be negative in order to get most recent actions.
235
+ # @param stop_version [Fixnum] version number to stop listing historical
236
+ # actions from (non-inclusive)
237
+ #
238
+ # @return [Array<Hash>] action history items
239
+ #
240
+ # @raise [RestAdapter::Errors::SessionNotLinked,
241
+ # RestAdapter::Errors::ServiceError,
242
+ # RestAdapter::Errors::OperationNotAllowedError]
243
+ def action_history(start_version: -10, stop_version: nil)
244
+ validate_session
245
+ @rest_adapter.list_history(start: start_version, stop: stop_version)
246
+ end
247
+
248
+ # @raise [RestAdapter::Errors::OperationNotAllowedError]
249
+ def validate_session
250
+ fail RestAdapter::Errors::OperationNotAllowedError,
251
+ 'This session has been unlinked, initialize new session instance' if @unlinked
252
+ end
253
+
254
+ private :validate_session, :get_user, :get_account
255
+ end
256
+ end
@@ -0,0 +1,286 @@
1
+ require_relative 'rest_adapter'
2
+ require_relative 'filesystem_common'
3
+
4
+ module CloudFS
5
+ # Share class is used to create and manage shares in end-user's account
6
+ #
7
+ # @author Mrinal Dhillon
8
+ class Share
9
+
10
+ # @return [String] share_key
11
+ attr_reader :share_key
12
+
13
+ # @return [String] name
14
+ attr_reader :name
15
+
16
+ # @return [String] type
17
+ attr_reader :type
18
+
19
+ # @return [String] url
20
+ attr_reader :url
21
+
22
+ # @return [String] short_url
23
+ attr_reader :short_url
24
+
25
+ # @return [String] size
26
+ attr_reader :size
27
+
28
+ # @return [String] application data
29
+ attr_reader :application_data
30
+
31
+ # Set the name of the share
32
+ # @param new_name [String] new name of the share.
33
+ # @param password [String] current password of the share.
34
+ def set_name(new_name, password: nil)
35
+ FileSystemCommon.validate_share_state(self)
36
+ fail RestAdapter::Errors::ArgumentError,
37
+ 'Invalid input, expected new name' if RestAdapter::Utils.is_blank?(new_name)
38
+
39
+ response = @rest_adapter.alter_share_info(
40
+ @share_key,
41
+ current_password: password,
42
+ name: new_name)
43
+
44
+ set_share_info(** response)
45
+ self
46
+ end
47
+
48
+ # @!attribute [r] date_created
49
+ # @return [Time] creation time
50
+ def date_created
51
+ if @date_created
52
+ Time.at(@date_created)
53
+ else
54
+ nil
55
+ end
56
+ end
57
+
58
+ # @!attribute [r] date_content_last_modified
59
+ # @return [Time] modified time
60
+ def date_content_last_modified
61
+ if @date_content_last_modified
62
+ Time.at(@date_content_last_modified)
63
+ else
64
+ nil
65
+ end
66
+ end
67
+
68
+ # @!attribute [r] date_meta_last_modified
69
+ # @return [Time] modified time
70
+ def date_meta_last_modified
71
+ if @date_meta_last_modified
72
+ Time.at(@date_meta_last_modified)
73
+ else
74
+ nil
75
+ end
76
+ end
77
+
78
+ # @param rest_adapter [RestAdapter] cloudfs RESTful api object
79
+ #
80
+ # @param [Hash] properties metadata of share
81
+ # @option properties [String] :share_key
82
+ # @option properties [String] :share_type
83
+ # @option properties [String] :share_name
84
+ # @option properties [String] :url
85
+ # @option properties [String] :short_url
86
+ # @option properties [String] :share_size
87
+ # @option properties [Fixnum] :date_created
88
+ def initialize(rest_adapter, ** properties)
89
+ fail RestAdapter::Errors::ArgumentError,
90
+ 'Invalid RestAdapter, input type must be CloudFS::RestAdapter' unless rest_adapter.is_a?(CloudFS::RestAdapter)
91
+ @rest_adapter = rest_adapter
92
+ set_share_info(** properties)
93
+ end
94
+
95
+ # @see #initialize
96
+ def set_share_info(** params)
97
+ @share_key = params.fetch(:share_key) { fail RestAdapter::Errors::ArgumentError,
98
+ 'missing parameter, share_key must be defined' }
99
+ @type = params[:share_type]
100
+ @name = params[:share_name]
101
+ @url = params[:url]
102
+ @short_url = params[:short_url]
103
+ @size = params[:share_size]
104
+ @date_created = params[:date_created]
105
+ @exists = true
106
+
107
+ if params[:single_item]
108
+ @application_data = params[:single_item][:application_data]
109
+ @date_content_last_modified = params[:single_item][:date_content_last_modified]
110
+ @date_meta_last_modified = params[:single_item][:date_meta_last_modified]
111
+ end
112
+
113
+ @parent_state = {:share_key => @share_key}
114
+
115
+ changed_properties_reset
116
+ end
117
+
118
+ # Reset changed properties
119
+ def changed_properties_reset
120
+ @changed_properties = {}
121
+ end
122
+
123
+ # @return [Boolean] whether the share exists,
124
+ # false only if it has been deleted
125
+ def exists?
126
+ @exists
127
+ end
128
+
129
+
130
+ # List items in this share
131
+ # @return [Array<File, Folder>] list of items
132
+ # @raise [RestAdapter::Errors::SessionNotLinked,
133
+ # RestAdapter::Errors::ServiceError,
134
+ # RestAdapter::Errors::InvalidShareError]
135
+ def list
136
+ FileSystemCommon.validate_share_state(self)
137
+ response = @rest_adapter.browse_share(@share_key).fetch(:items)
138
+ FileSystemCommon.create_items_from_hash_array(
139
+ response,
140
+ @rest_adapter,
141
+ parent_state: @parent_state,
142
+ in_share: true)
143
+ end
144
+
145
+ # Delete this share
146
+ #
147
+ # @return [true]
148
+ #
149
+ # @note Subsequent operations shall fail
150
+ # {RestAdapter::Errors::InvalidShareError}
151
+ #
152
+ # @raise [RestAdapter::Errors::SessionNotLinked,
153
+ # RestAdapter::Errors::ServiceError,
154
+ # RestAdapter::Errors::InvalidShareError]
155
+ def delete
156
+ FileSystemCommon.validate_share_state(self)
157
+ @rest_adapter.delete_share(@share_key)
158
+ @exists = false
159
+ true
160
+ end
161
+
162
+ # Change password of this share
163
+ #
164
+ # @param password [String] new password for this share
165
+ # @param current_password [String] is required if password is already
166
+ # set for this share
167
+ #
168
+ # @return [Share] return self
169
+ #
170
+ # @raise [RestAdapter::Errors::SessionNotLinked,
171
+ # RestAdapter::Errors::ServiceError,
172
+ # RestAdapter::Errors::InvalidShareError]
173
+ def set_password(password, current_password: nil)
174
+ FileSystemCommon.validate_share_state(self)
175
+ fail RestAdapter::Errors::ArgumentError,
176
+ 'Invalid input, expected the password' if RestAdapter::Utils.is_blank?(password)
177
+
178
+ response = @rest_adapter.alter_share_info(
179
+ @share_key,
180
+ current_password: current_password,
181
+ password: password)
182
+
183
+ set_share_info(** response)
184
+ self
185
+ end
186
+
187
+ # Changes, adds, or removes the share’s password or updates the name.
188
+ #
189
+ # @param [Hash] values metadata of share.
190
+ # @option values [String] :current_password
191
+ # @option values [String] :password
192
+ # @option values [String] :name
193
+ #
194
+ # @param password [String] current password of the share.
195
+ #
196
+ # @return [Boolean] based on the success or fail status of the action.
197
+ def change_attributes(values, password: nil)
198
+ current_password = values.has_key?('current_password') ? values['current_password'] : password
199
+ new_password = values.has_key?('password') ? values['password'] : nil
200
+ name = values.has_key?('name') ? values['name'] : nil
201
+
202
+ response = @rest_adapter.alter_share_info(
203
+ @share_key, current_password: current_password, password: new_password, name: name)
204
+
205
+ set_share_info(** response)
206
+ response.has_key?(:share_key)
207
+ end
208
+
209
+ # Save this share's current state.
210
+ # Only name, is committed to this share in user's account
211
+ # @param password [String] current password for this share,
212
+ # if has been set, it is necessary even if share has been unlocked
213
+ #
214
+ # @return [Share] returns self
215
+ #
216
+ # @raise [RestAdapter::Errors::SessionNotLinked,
217
+ # RestAdapter::Errors::ServiceError,
218
+ # RestAdapter::Errors::InvalidShareError]
219
+ def save(password: nil)
220
+ FileSystemCommon.validate_share_state(self)
221
+ if @changed_properties[:name]
222
+ response = @rest_adapter.alter_share_info(
223
+ @share_key,
224
+ current_password: password,
225
+ name: @changed_properties[:name])
226
+
227
+ set_share_info(** response)
228
+ end
229
+ self
230
+ end
231
+
232
+ # Receive contents of this share at specified path in user's filesystem.
233
+ # All items found in share are copied to given location.
234
+ #
235
+ # @param path [String] path in user's account to receive share at,
236
+ # default is "/" root
237
+ # @param exists [String] ('RENAME', 'FAIL', 'OVERWRITE') action to take in
238
+ # case of conflict with existing items at path
239
+ #
240
+ # @return [Array<File, Folder>] items
241
+ #
242
+ # @raise [RestAdapter::Errors::SessionNotLinked,
243
+ # RestAdapter::Errors::ServiceError,
244
+ # RestAdapter::Errors::InvalidShareError]
245
+ def receive(path: nil, exists: 'RENAME')
246
+ FileSystemCommon.validate_share_state(self)
247
+ response = @rest_adapter.receive_share(
248
+ @share_key,
249
+ path: path,
250
+ exists: exists)
251
+
252
+ FileSystemCommon.create_items_from_hash_array(
253
+ response,
254
+ @rest_adapter,
255
+ parent: path)
256
+ end
257
+
258
+ # Refresh this share to latest state
259
+ # @note Locally changed properties i.e. name get discarded
260
+ #
261
+ # @note raises RestAdapter::Errors::ServiceError if share is locked,
262
+ # unlock share if password is set
263
+ #
264
+ # @return [Share] returns self
265
+ #
266
+ # @raise [RestAdapter::Errors::SessionNotLinked,
267
+ # RestAdapter::Errors::ServiceError,
268
+ # RestAdapter::Errors::InvalidShareError]
269
+ def refresh
270
+ FileSystemCommon.validate_share_state(self)
271
+ response = @rest_adapter.browse_share(share_key).fetch(:share)
272
+ set_share_info(** response)
273
+ self
274
+ end
275
+
276
+ # @return [String]
277
+ # @!visibility private
278
+ def to_s
279
+ "#{self.class}: name: #{@name}, size: #{@size}bytes"
280
+ end
281
+
282
+ alias inspect to_s
283
+
284
+ private :set_share_info, :changed_properties_reset
285
+ end
286
+ end