box-api 0.1.6

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,140 @@
1
+ require 'box/api/exceptions'
2
+
3
+ require 'httmultiparty'
4
+
5
+ module Box
6
+ class Api
7
+ include HTTMultiParty # a slight modification to HTTParty, adding multi-part upload support
8
+
9
+ def initialize(key, url = 'https://box.net', upload_url = 'https://upload.box.net', version = '1.0')
10
+ @default_params = { :api_key => key } # add the api_key to every query
11
+
12
+ @base_url = "#{ url }/api/#{ version }" # set the base of the request url
13
+ @upload_url = "#{ upload_url }/api/#{ version }" # uploads use a different url than everything else
14
+ end
15
+
16
+ def query_rest(expected, options = {})
17
+ query_raw('get', "#{ @base_url }/rest", expected, options)['response']
18
+ end
19
+
20
+ def query_download(query, args, options = {})
21
+ url = [ "#{ @base_url }/#{ query }", @auth_token, args ].flatten.compact.join('/') # /download/<auth_token>/<arg1>/<arg2>/<etc>
22
+ query_raw('get', url, nil, options) # note, expected is nil because the return will be raw data
23
+ end
24
+
25
+ def query_upload(query, args, expected, options = {})
26
+ url = [ "#{ @upload_url }/#{ query }", @auth_token, args ].flatten.compact.join('/') # /upload/<auth_token>/<arg1>/<arg2>/<etc>
27
+ query_raw('post', url, expected, options)['response']
28
+ end
29
+
30
+ def query_raw(method, url, expected, options = {})
31
+ response = case method
32
+ when 'get'
33
+ self.class.get(url, :query => @default_params.merge(options))
34
+ when 'post'
35
+ self.class.post(url, :query => @default_params.merge(options), :format => :xml) # known bug with api that only occurs with uploads, will be fixed soon
36
+ end
37
+
38
+ handle_response(response, expected)
39
+ end
40
+
41
+ def handle_response(response, expected = nil)
42
+ if expected
43
+ begin
44
+ status = response['response']['status']
45
+ rescue
46
+ raise UnknownResponse, "Unknown response: #{ response }"
47
+ end
48
+
49
+ unless status == expected # expected is the normal, successful status for this request
50
+ exception = self.class.get_exception(status)
51
+ raise exception, status
52
+ end
53
+ end
54
+
55
+ raise ErrorStatus, response.code unless response.success? # when the http return code is not normal
56
+ response
57
+ end
58
+
59
+ def get_ticket
60
+ query_rest('get_ticket_ok', :action => :get_ticket)
61
+ end
62
+
63
+ def get_auth_token(ticket)
64
+ query_rest('get_auth_token_ok', :action => :get_auth_token, :ticket => ticket)
65
+ end
66
+
67
+ # save the auth token and add it to every request
68
+ def set_auth_token(auth_token)
69
+ @auth_token = auth_token
70
+ @default_params[:auth_token] = auth_token
71
+ end
72
+
73
+ def logout
74
+ query_rest('logout_ok', :action => :logout)
75
+ end
76
+
77
+ def register_new_user(login, password)
78
+ query_rest('successful_register', :action => :register_new_user, :login => login, :password => password)
79
+ end
80
+
81
+ def verify_registration_email(login)
82
+ query_rest('email_ok', :action => :verify_registration_email, :login => login)
83
+ end
84
+
85
+ def get_account_info
86
+ query_rest('get_account_info_ok', :action => :get_account_info)
87
+ end
88
+
89
+ # TODO: Use zip compression to save space
90
+ def get_account_tree(folder_id = 0, *args)
91
+ query_rest('listing_ok', :action => :get_account_tree, :folder_id => folder_id, :params => [ 'nozip' ] + args)
92
+ end
93
+
94
+ def create_folder(parent_id, name, share = 0)
95
+ query_rest('create_ok', :action => :create_folder, :parent_id => parent_id, :name => name, :share => share)
96
+ end
97
+
98
+ def move(target, target_id, destination_id)
99
+ query_rest('s_move_node', :action => :move, :target => target, :target_id => target_id, :destination_id => destination_id)
100
+ end
101
+
102
+ def copy(target, target_id, destination_id)
103
+ query_rest('s_copy_node', :action => :copy, :target => target, :target_id => target_id, :destination_id => destination_id)
104
+ end
105
+
106
+ def rename(target, target_id, new_name)
107
+ query_rest('s_rename_node', :action => :rename, :target => target, :target_id => target_id, :new_name => new_name)
108
+ end
109
+
110
+ def delete(target, target_id)
111
+ query_rest('s_delete_node', :action => :delete, :target => target, :target_id => target_id)
112
+ end
113
+
114
+ def get_file_info(file_id)
115
+ query_rest('s_get_file_info', :action => :get_file_info, :file_id => file_id)
116
+ end
117
+
118
+ def set_description(target, target_id, description)
119
+ query_rest('s_set_description', :action => :set_description, :target => target, :target_id => target_id, :description => description)
120
+ end
121
+
122
+ def download(path, file_id, version = nil)
123
+ ::File.open(path, 'w') do |file|
124
+ file << query_download('download', [ file_id, version ]) # write the response directly to file
125
+ end
126
+ end
127
+
128
+ def upload(path, folder_id, new_copy = false)
129
+ query_upload('upload', folder_id, 'upload_ok', :file => ::File.new(path), :new_copy => new_copy)
130
+ end
131
+
132
+ def overwrite(path, file_id, name = nil)
133
+ query_upload('overwrite', file_id, 'upload_ok', :file => ::File.new(path), :file_name => name)
134
+ end
135
+
136
+ def new_copy(path, file_id, name = nil)
137
+ query_upload('new_copy', file_id, 'upload_ok', :file => ::File.new(path), :new_file_name => name)
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,75 @@
1
+ module Box
2
+ class Api
3
+ class Exception < StandardError; end
4
+
5
+ # HTTP exceptions
6
+ class UnknownResponse < Exception; end
7
+ class ErrorStatus < Exception; end
8
+
9
+ # Common responses
10
+ class Restricted < Exception; end
11
+ class InvalidInput < Exception; end
12
+ class NotAuthorized < Exception; end
13
+ class Generic < Exception; end
14
+ class Unknown < Exception; end
15
+
16
+ # Registration specific responses
17
+ class EmailInvalid < Exception; end
18
+ class EmailTaken < Exception; end
19
+
20
+ # Folder/File specific responses
21
+ class InvalidFolder < Exception; end
22
+ class InvalidName < Exception; end
23
+ class NoAccess < Exception; end
24
+ class NoParent < Exception; end
25
+ class NameTaken < Exception; end
26
+
27
+ # Upload/Download specific responses
28
+ class UploadFailed < Exception; end
29
+ class AccountExceeded < Exception; end
30
+ class SizeExceeded < Exception; end
31
+
32
+ def self.get_exception(status)
33
+ case status
34
+ # Common responses
35
+ when "application_restricted"
36
+ Restricted
37
+ when "wrong_input", "Wrong input params"
38
+ InvalidInput
39
+ when "not_logged_in", "wrong auth token"
40
+ NotAuthorized
41
+ when "e_no_access", "e_access_denied", "access_denied"
42
+ NoAccess
43
+ # Registration specific responses
44
+ when "email_invalid"
45
+ EmailInvalid
46
+ when "email_already_registered"
47
+ EmailTaken
48
+ when "get_auth_token_error", "e_register"
49
+ Generic
50
+ # Folder/File specific responses
51
+ when "e_folder_id"
52
+ InvalidFolder
53
+ when "no_parent"
54
+ NoParent
55
+ when "invalid_folder_name", "e_no_folder_name", "folder_name_too_big", "upload_invalid_file_name"
56
+ InvalidName
57
+ when "e_input_params"
58
+ InvalidInput
59
+ when "e_filename_in_use", "s_folder_exists"
60
+ NameTaken
61
+ when "e_move_node", "e_copy_node", "e_rename_node", "e_set_description"
62
+ Generic
63
+ # Upload/Download specific responses
64
+ when "upload_some_files_failed"
65
+ UploadFailed
66
+ when "not_enough_free_space"
67
+ AccountExceeded
68
+ when "filesize_limit_exceeded"
69
+ SizeExceeded
70
+ else
71
+ Unknown
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,37 @@
1
+ require 'box/item'
2
+
3
+ module Box
4
+ class File < Item
5
+ def self.type; 'file'; end
6
+
7
+ # download the file to the specified path
8
+ def download(path)
9
+ @api.download(path, id)
10
+ end
11
+
12
+ # overwrite this file, using the file at the specified path
13
+ def upload_overwrite(path)
14
+ info = @api.overwrite(path, id)['files']['file']
15
+
16
+ clear_info
17
+ update_info(info)
18
+
19
+ self
20
+ end
21
+
22
+ # upload a new copy of this file, the name being 'file (#).ext' for the #th copy
23
+ def upload_copy(path)
24
+ info = @api.new_copy(path, id)['files']['file']
25
+ parent.delete_info('files')
26
+
27
+ self.class.new(api, parent, info)
28
+ end
29
+
30
+ protected
31
+
32
+ # get the file info
33
+ def get_info
34
+ @api.get_file_info(id)['info']
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,140 @@
1
+ require 'box/item'
2
+ require 'box/file'
3
+
4
+ module Box
5
+ class Folder < Item
6
+ attr_accessor :cached_tree
7
+
8
+ def self.type; 'folder'; end
9
+
10
+ def info(refresh = false)
11
+ return self if @cached_info and not refresh
12
+
13
+ create_sub_items(nil, Box::Folder)
14
+ create_sub_items(nil, Box::File)
15
+
16
+ super
17
+ end
18
+
19
+ # use the cached tree or update it if requested
20
+ def tree(refresh = false)
21
+ return self if @cached_tree and not refresh
22
+
23
+ @cached_info = true # count the info as cached as well
24
+ @cached_tree = true
25
+
26
+ update_info(get_tree)
27
+ force_cached_tree
28
+
29
+ self
30
+ end
31
+
32
+ # create a new folder using this folder as the parent
33
+ def create(name, share = 0)
34
+ info = @api.create_folder(id, name, share)['folder']
35
+
36
+ delete_info('folders')
37
+
38
+ Box::Folder.new(api, self, info)
39
+ end
40
+
41
+ # upload a new file using this folder as the parent
42
+ def upload(path)
43
+ info = @api.upload(path, id)['files']['file']
44
+
45
+ delete_info('files')
46
+
47
+ Box::File.new(api, self, info)
48
+ end
49
+
50
+ # search for items using criteria
51
+ def find(criteria)
52
+ recursive = criteria.delete(:recursive)
53
+ recursive = true if recursive == nil # default to true
54
+
55
+ tree if recursive # get the full tree
56
+
57
+ find!(criteria, recursive)
58
+ end
59
+
60
+ protected
61
+
62
+ # get the folder info
63
+ def get_info
64
+ @api.get_account_tree(id, 'onelevel')['tree']['folder']
65
+ end
66
+
67
+ # get the folder info and all nested items
68
+ def get_tree
69
+ @api.get_account_tree(id)['tree']['folder']
70
+ end
71
+
72
+ def clear_info
73
+ @cached_tree = false
74
+ super
75
+ end
76
+
77
+ # overload Item#update_info to create the subobjects like Files and Folders
78
+ def update_info(info)
79
+ if folders = info.delete('folders')
80
+ create_sub_items(folders, Box::Folder)
81
+ end
82
+
83
+ if files = info.delete('files')
84
+ create_sub_items(files, Box::File)
85
+ end
86
+
87
+ super
88
+ end
89
+
90
+ # create the sub items, so they are objects rather than hashes
91
+ def create_sub_items(items, item_class)
92
+ @data[item_class.types] ||= Array.new
93
+
94
+ return unless items
95
+
96
+ temp = items[item_class.type]
97
+ temp = [ temp ] if temp.class == Hash # lone folders need to be packaged into an array
98
+
99
+ temp.collect do |item_info|
100
+ item_class.new(api, self, item_info).tap do |item|
101
+ @data[item_class.types] << item
102
+ end
103
+ end
104
+ end
105
+
106
+ def force_cached_tree
107
+ create_sub_items(nil, Box::Folder)
108
+ create_sub_items(nil, Box::File)
109
+
110
+ files.each do |file|
111
+ file.cached_info = true
112
+ end
113
+
114
+ folders.each do |folder|
115
+ folder.cached_info = true
116
+ folder.cached_tree = true
117
+
118
+ folder.force_cached_tree
119
+ end
120
+ end
121
+
122
+ def find!(criteria, recursive)
123
+ matches = (files + folders).collect do |item| # search over our files and folders
124
+ match = criteria.all? do |key, value| # make sure all criteria pass
125
+ item.send(key) == value.to_s rescue false
126
+ end
127
+
128
+ item if match # use the item if it is a match
129
+ end
130
+
131
+ if recursive
132
+ folders.each do |folder| # recursive step
133
+ matches += folder.find!(criteria, recursive) # search each folder
134
+ end
135
+ end
136
+
137
+ matches.compact # return the results without nils
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,127 @@
1
+ module Box
2
+ class Item
3
+ attr_accessor :data, :api, :parent
4
+ attr_accessor :cached_info
5
+
6
+ def initialize(api, parent, info)
7
+ @api = api
8
+ @parent = parent
9
+ @data = Hash.new
10
+
11
+ update_info(info) # merges with the info hash, and renames some fields
12
+ end
13
+
14
+ def self.type; raise "Overwrite this method"; end
15
+ def self.types; type + 's'; end
16
+
17
+ # should be a better way of doing this
18
+ def type; self.class.type; end
19
+ def types; self.class.types; end
20
+
21
+ def id; @data['id']; end # overwrite Object#id, which is not what we want
22
+
23
+ # use cached info or update it if requested
24
+ def info(refresh = false)
25
+ return self if @cached_info and not refresh
26
+
27
+ @cached_info = true
28
+ update_info(get_info)
29
+
30
+ self
31
+ end
32
+ # move the item to the destination folder
33
+ def move(destination)
34
+ @api.move(type, id, destination.id)
35
+
36
+ parent.delete_info(self.types)
37
+ destination.delete_info(self.types)
38
+
39
+ @parent = destination
40
+
41
+ self
42
+ end
43
+
44
+ # copy the file (folder not supported in the api) to the destination folder
45
+ def copy(destination)
46
+ @api.copy(type, id, destination.id)
47
+
48
+ destination.delete_info(self.types)
49
+
50
+ self.class.new(api, destination, @data)
51
+ end
52
+
53
+ # rename the item
54
+ def rename(new_name)
55
+ @api.rename(type, id, new_name)
56
+
57
+ update_info('name' => new_name)
58
+
59
+ self
60
+ end
61
+
62
+ # delete the item
63
+ def delete
64
+ @api.delete(type, id)
65
+
66
+ parent.delete_info(self.types)
67
+ @parent = nil
68
+
69
+ self
70
+ end
71
+
72
+ def description(message)
73
+ @api.set_description(type, id, message)
74
+
75
+ self
76
+ end
77
+
78
+ # get the path, starting with /
79
+ def path
80
+ "#{ parent.path + '/' if parent }#{ name }"
81
+ end
82
+
83
+ # use method_missing as to provide an easy way to access the item's properties
84
+ def method_missing(sym, *args, &block)
85
+ str = sym.to_s
86
+
87
+ # return the value if it already exists
88
+ return @data[str] if @data.key?(str)
89
+
90
+ # value didn't exist, so update the info
91
+ self.info
92
+
93
+ # try again
94
+ return @data[str] if @data.key?(str)
95
+
96
+ # we didn't find a value, so it must be invalid
97
+ super
98
+ end
99
+
100
+ protected
101
+
102
+ # sub-classes are meant to implement this
103
+ def get_info(*args); Hash.new; end
104
+
105
+ def update_info(info)
106
+ ninfo = Hash.new
107
+
108
+ # the api is stupid and some fields are named 'file_id' or 'id' inconsistently, so trim the type off
109
+ info.each do |name, value|
110
+ if name.to_s =~ /^#{ type }_(.+)$/; ninfo[$1] = value
111
+ else; ninfo[name.to_s] = value; end
112
+ end
113
+
114
+ @data.merge!(ninfo) # merge in the updated info
115
+ end
116
+
117
+ def delete_info(field)
118
+ @cached_info = false
119
+ @data.delete(field)
120
+ end
121
+
122
+ def clear_info
123
+ @cached_info = false
124
+ @data.clear
125
+ end
126
+ end
127
+ end