box-api 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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