box-api 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +40 -0
- data/LICENSE.txt +202 -0
- data/README.md +23 -0
- data/Rakefile +6 -0
- data/box-api.gemspec +21 -0
- data/examples/app_data.yml +6 -0
- data/examples/files.rb +36 -0
- data/examples/login.rb +57 -0
- data/lib/box-api.rb +16 -0
- data/lib/box/account.rb +108 -0
- data/lib/box/api.rb +140 -0
- data/lib/box/api/exceptions.rb +75 -0
- data/lib/box/file.rb +37 -0
- data/lib/box/folder.rb +140 -0
- data/lib/box/item.rb +127 -0
- data/spec/account_spec.rb +43 -0
- data/spec/api_spec.rb +19 -0
- data/spec/file_spec.rb +105 -0
- data/spec/folder_spec.rb +117 -0
- data/spec/helper/account.rb +17 -0
- data/spec/helper/account.yml +3 -0
- data/spec/helper/fake_tree.rb +101 -0
- data/spec/item_spec.rb +51 -0
- metadata +139 -0
data/lib/box/api.rb
ADDED
@@ -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
|
data/lib/box/file.rb
ADDED
@@ -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
|
data/lib/box/folder.rb
ADDED
@@ -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
|
data/lib/box/item.rb
ADDED
@@ -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
|