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.
- 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
|