iora-ruby-box 1.14.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.
- data/.document +5 -0
- data/.travis.yml +6 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +51 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +349 -0
- data/Rakefile +32 -0
- data/VERSION +1 -0
- data/lib/ruby-box.rb +21 -0
- data/lib/ruby-box/client.rb +163 -0
- data/lib/ruby-box/collaboration.rb +19 -0
- data/lib/ruby-box/comment.rb +14 -0
- data/lib/ruby-box/discussion.rb +15 -0
- data/lib/ruby-box/event.rb +4 -0
- data/lib/ruby-box/event_response.rb +17 -0
- data/lib/ruby-box/exceptions.rb +22 -0
- data/lib/ruby-box/file.rb +96 -0
- data/lib/ruby-box/folder.rb +113 -0
- data/lib/ruby-box/item.rb +181 -0
- data/lib/ruby-box/session.rb +136 -0
- data/lib/ruby-box/shared_link.rb +10 -0
- data/lib/ruby-box/user.rb +16 -0
- data/lib/ruby-box/web_link.rb +4 -0
- data/ruby-box.gemspec +99 -0
- data/spec/client_spec.rb +64 -0
- data/spec/event_spec.rb +70 -0
- data/spec/file_spec.rb +110 -0
- data/spec/fixtures/comment_create.json +18 -0
- data/spec/fixtures/events.json +161 -0
- data/spec/fixtures/me.json +17 -0
- data/spec/fixtures/users.json +39 -0
- data/spec/folder_spec.rb +181 -0
- data/spec/helper/account.example +3 -0
- data/spec/helper/account.rb +8 -0
- data/spec/integration_spec.rb +387 -0
- data/spec/item_spec.rb +31 -0
- data/spec/me_spec.rb +20 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/users_spec.rb +24 -0
- metadata +214 -0
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'rspec/core/rake_task'
|
15
|
+
RSpec::Core::RakeTask.new(:spec)
|
16
|
+
|
17
|
+
require 'jeweler'
|
18
|
+
Jeweler::Tasks.new do |gem|
|
19
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
20
|
+
gem.name = "ruby-box"
|
21
|
+
gem.homepage = "http://github.com/attachmentsme/ruby-box"
|
22
|
+
gem.license = "MIT"
|
23
|
+
gem.summary = %Q{ruby gem for box.com 2.0 api}
|
24
|
+
gem.description = %Q{ruby gem for box.com 2.0 api}
|
25
|
+
gem.email = "ben@attachments.me"
|
26
|
+
gem.authors = ["Attachments.me"]
|
27
|
+
gem.files.exclude 'spec/*.test'
|
28
|
+
# dependencies defined in Gemfile
|
29
|
+
end
|
30
|
+
Jeweler::RubygemsDotOrgTasks.new
|
31
|
+
|
32
|
+
task :default => :spec
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.14.0
|
data/lib/ruby-box.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'ruby-box/client'
|
2
|
+
require 'ruby-box/session'
|
3
|
+
|
4
|
+
require 'ruby-box/item'
|
5
|
+
require 'ruby-box/file'
|
6
|
+
require 'ruby-box/folder'
|
7
|
+
require 'ruby-box/user'
|
8
|
+
require 'ruby-box/comment'
|
9
|
+
require 'ruby-box/collaboration'
|
10
|
+
require 'ruby-box/discussion'
|
11
|
+
require 'ruby-box/exceptions'
|
12
|
+
require 'ruby-box/event_response'
|
13
|
+
require 'ruby-box/event'
|
14
|
+
require 'ruby-box/web_link'
|
15
|
+
require 'ruby-box/shared_link'
|
16
|
+
|
17
|
+
module RubyBox
|
18
|
+
API_URL = 'https://api.box.com/2.0'
|
19
|
+
UPLOAD_URL = 'https://upload.box.com/api/2.0'
|
20
|
+
ISO_8601_TEST = Regexp.new(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T/)
|
21
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/https'
|
3
|
+
require 'json'
|
4
|
+
require 'net/http/post/multipart'
|
5
|
+
require 'open-uri'
|
6
|
+
|
7
|
+
module RubyBox
|
8
|
+
class Client
|
9
|
+
|
10
|
+
def initialize(session)
|
11
|
+
@session = session
|
12
|
+
end
|
13
|
+
|
14
|
+
def root_folder
|
15
|
+
folder_by_id('0')
|
16
|
+
end
|
17
|
+
|
18
|
+
def folder_by_id(id)
|
19
|
+
folder = Folder.new(@session, {'id' => id})
|
20
|
+
folder.reload_meta
|
21
|
+
end
|
22
|
+
|
23
|
+
def folder(path='/')
|
24
|
+
path = path.sub(/(^\.$)|(^\.\/)/, '') if path # handle folders with leading '.'
|
25
|
+
return root_folder if ['', '/'].member?(path)
|
26
|
+
folder_from_split_path( split_path(path) )
|
27
|
+
end
|
28
|
+
|
29
|
+
def file_by_id(id)
|
30
|
+
file = File.new(@session, {'id' => id})
|
31
|
+
file.reload_meta
|
32
|
+
end
|
33
|
+
|
34
|
+
def file(path)
|
35
|
+
path = split_path( path.sub(/^\.\//, '') )
|
36
|
+
file_name = path.pop
|
37
|
+
folder = folder_from_split_path( path )
|
38
|
+
folder.files(file_name).first if folder
|
39
|
+
end
|
40
|
+
|
41
|
+
def item(path)
|
42
|
+
path = split_path( path.sub(/^\.\//, '') )
|
43
|
+
item_name = path.pop
|
44
|
+
folder = folder_from_split_path( path )
|
45
|
+
|
46
|
+
folder.items.select do |item|
|
47
|
+
item.instance_variable_get(:@raw_item)['name'] and item.name == item_name
|
48
|
+
end.first
|
49
|
+
end
|
50
|
+
|
51
|
+
def download(path)
|
52
|
+
file = file(path)
|
53
|
+
file.download if file
|
54
|
+
end
|
55
|
+
|
56
|
+
def stream(path, opts={})
|
57
|
+
file = file(path)
|
58
|
+
file.stream(opts) if file
|
59
|
+
end
|
60
|
+
|
61
|
+
def search(query, item_limit=100, offset=0)
|
62
|
+
Enumerator.new do |yielder|
|
63
|
+
while true
|
64
|
+
url = "#{RubyBox::API_URL}/search?query=#{URI::encode(query)}&limit=#{item_limit}&offset=#{offset}"
|
65
|
+
resp = @session.get( url )
|
66
|
+
resp['entries'].each do |entry|
|
67
|
+
yielder.yield(RubyBox::Item.factory(@session, entry))
|
68
|
+
end
|
69
|
+
offset += resp['entries'].count
|
70
|
+
break if resp['offset'].to_i + resp['limit'].to_i >= resp['total_count'].to_i
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def create_folder(path)
|
76
|
+
folder = root_folder
|
77
|
+
folder_names = split_path(path)
|
78
|
+
folder_names.each do |folder_name|
|
79
|
+
new_folder = folder.folders(folder_name).first
|
80
|
+
if !new_folder
|
81
|
+
begin
|
82
|
+
new_folder = folder.create_subfolder(folder_name)
|
83
|
+
rescue RubyBox::ItemNameInUse => e
|
84
|
+
new_folder = folder.folders(folder_name).first
|
85
|
+
end
|
86
|
+
end
|
87
|
+
folder = new_folder
|
88
|
+
end
|
89
|
+
folder
|
90
|
+
end
|
91
|
+
|
92
|
+
def upload_data(path, data, overwrite=true)
|
93
|
+
path = split_path(path)
|
94
|
+
file_name = path.pop
|
95
|
+
folder = create_folder(path.join('/'))
|
96
|
+
folder.upload_file(file_name, data, overwrite) if folder
|
97
|
+
end
|
98
|
+
|
99
|
+
def upload_file(local_path, remote_path, overwrite=true)
|
100
|
+
folder = create_folder( remote_path )
|
101
|
+
upload_file_to_folder(local_path, folder, overwrite)
|
102
|
+
end
|
103
|
+
|
104
|
+
def upload_file_by_folder_id(local_path, folder_id, overwrite=true)
|
105
|
+
folder = folder_by_id(folder_id)
|
106
|
+
upload_file_to_folder(local_path, folder, overwrite)
|
107
|
+
end
|
108
|
+
|
109
|
+
def split_path(path)
|
110
|
+
path.gsub!(/(^\/)|(\/$)/, '')
|
111
|
+
path.split('/')
|
112
|
+
end
|
113
|
+
|
114
|
+
def event_response(stream_position=0, stream_type=:all, limit=100)
|
115
|
+
q = fmt_events_args stream_position, stream_type, limit
|
116
|
+
url = "#{RubyBox::API_URL}/events?#{q}"
|
117
|
+
resp = @session.get( url )
|
118
|
+
EventResponse.new(@session, resp)
|
119
|
+
end
|
120
|
+
|
121
|
+
def me
|
122
|
+
resp = @session.get( "#{RubyBox::API_URL}/users/me" )
|
123
|
+
User.new(@session, resp)
|
124
|
+
end
|
125
|
+
|
126
|
+
def users(filter_term = "", limit = 100, offset = 0)
|
127
|
+
url = "#{RubyBox::API_URL}/users?filter_term=#{URI::encode(filter_term)}&limit=#{limit}&offset=#{offset}"
|
128
|
+
resp = @session.get( url )
|
129
|
+
resp['entries'].map do |entry|
|
130
|
+
RubyBox::Item.factory(@session, entry)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def upload_file_to_folder(local_path, folder, overwrite)
|
137
|
+
file_name = local_path.split('/').pop
|
138
|
+
return unless folder
|
139
|
+
::File.open(local_path, 'rb') do |data|
|
140
|
+
folder.upload_file(file_name, data, overwrite)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def folder_from_split_path(path)
|
145
|
+
folder = root_folder
|
146
|
+
path.each do |folder_name|
|
147
|
+
folder = folder.folders(folder_name).first
|
148
|
+
return nil unless folder
|
149
|
+
end
|
150
|
+
folder
|
151
|
+
end
|
152
|
+
|
153
|
+
def fmt_events_args(stream_position, stream_type, limit)
|
154
|
+
unless stream_position.to_s == 'now'
|
155
|
+
stream_position = stream_position.kind_of?(Numeric) ? stream_position : 0
|
156
|
+
end
|
157
|
+
stream_type = [:all, :changes, :sync, :admin_logs].include?(stream_type) ? stream_type : :all
|
158
|
+
limit = limit.kind_of?(Fixnum) ? limit : 100
|
159
|
+
"stream_position=#{stream_position}&stream_type=#{stream_type}&limit=#{limit}"
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module RubyBox
|
2
|
+
class RubyBoxError < StandardError
|
3
|
+
attr_accessor :body, :status
|
4
|
+
|
5
|
+
def initialize(error_json, status, body)
|
6
|
+
@status = status
|
7
|
+
@body = body
|
8
|
+
@error_json = error_json
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](key)
|
12
|
+
@error_json[key]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ObjectNotFound < StandardError; end
|
17
|
+
class AuthError < RubyBoxError; end
|
18
|
+
class RequestError < RubyBoxError; end
|
19
|
+
class ServerError < RubyBoxError; end
|
20
|
+
class ItemNameInUse < RubyBoxError; end
|
21
|
+
class UnshareableResource < StandardError; end
|
22
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module RubyBox
|
2
|
+
class File < Item
|
3
|
+
|
4
|
+
has_many :comments
|
5
|
+
|
6
|
+
def download
|
7
|
+
resp = stream.read
|
8
|
+
end
|
9
|
+
|
10
|
+
def download_url
|
11
|
+
@session.get( file_content_url )["location"]
|
12
|
+
end
|
13
|
+
|
14
|
+
def copy_to( folder_id, name=nil )
|
15
|
+
|
16
|
+
# Allow either a folder_id or a folder object
|
17
|
+
# to be passed in.
|
18
|
+
folder_id = folder_id.id if folder_id.instance_of?(RubyBox::Folder)
|
19
|
+
|
20
|
+
uri = URI.parse( "#{RubyBox::API_URL}/#{resource_name}/#{id}/copy" )
|
21
|
+
request = Net::HTTP::Post.new( uri.request_uri )
|
22
|
+
request.body = JSON.dump({
|
23
|
+
"parent" => {"id" => folder_id},
|
24
|
+
"name" => name
|
25
|
+
})
|
26
|
+
|
27
|
+
resp = @session.request(uri, request)
|
28
|
+
end
|
29
|
+
|
30
|
+
def stream( opts={} )
|
31
|
+
open(download_url, opts)
|
32
|
+
end
|
33
|
+
|
34
|
+
def upload_content( data )
|
35
|
+
url = "#{RubyBox::UPLOAD_URL}/#{resource_name}/content"
|
36
|
+
uri = URI.parse(url)
|
37
|
+
request = Net::HTTP::Post::Multipart.new(uri.path, {
|
38
|
+
"filename" => UploadIO.new(data, "application/pdf", name),
|
39
|
+
"folder_id" => parent.id
|
40
|
+
})
|
41
|
+
|
42
|
+
resp = @session.request(uri, request)
|
43
|
+
if resp['entries']
|
44
|
+
@raw_item = resp['entries'][0]
|
45
|
+
else
|
46
|
+
@raw_item = resp
|
47
|
+
end
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def update_content( data )
|
52
|
+
|
53
|
+
url = "#{RubyBox::UPLOAD_URL}/#{resource_name}/#{id}/content"
|
54
|
+
uri = URI.parse(url)
|
55
|
+
|
56
|
+
request = Net::HTTP::Post::Multipart.new(uri.path, {
|
57
|
+
"filename" => prepare_upload(data, name),
|
58
|
+
"folder_id" => parent.id
|
59
|
+
}, {"if-match" => etag })
|
60
|
+
|
61
|
+
resp = @session.request(uri, request)
|
62
|
+
if resp['entries']
|
63
|
+
@raw_item = resp['entries'][0]
|
64
|
+
else
|
65
|
+
@raw_item = resp
|
66
|
+
end
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_comment(message)
|
71
|
+
RubyBox::Comment.new(@session, {
|
72
|
+
'item' => {'id' => id, 'type' => type},
|
73
|
+
'message' => message
|
74
|
+
}).create
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
def file_content_url
|
79
|
+
"#{RubyBox::API_URL}/#{resource_name}/#{id}/content"
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
def resource_name
|
84
|
+
'files'
|
85
|
+
end
|
86
|
+
|
87
|
+
def has_mini_format?
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
def prepare_upload(data, fname)
|
92
|
+
UploadIO.new(data, "application/pdf", fname)
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module RubyBox
|
2
|
+
class Folder < Item
|
3
|
+
|
4
|
+
has_many :discussions
|
5
|
+
has_many :collaborations
|
6
|
+
has_many_paginated :items
|
7
|
+
|
8
|
+
def files(name=nil, item_limit=100, offset=0, fields=nil)
|
9
|
+
items_by_type(RubyBox::File, name, item_limit, offset, fields)
|
10
|
+
end
|
11
|
+
|
12
|
+
def folders(name=nil, item_limit=100, offset=0, fields=nil)
|
13
|
+
items_by_type(RubyBox::Folder, name, item_limit, offset, fields)
|
14
|
+
end
|
15
|
+
|
16
|
+
def upload_file(filename, data, overwrite=true)
|
17
|
+
file = RubyBox::File.new(@session, {
|
18
|
+
'name' => filename,
|
19
|
+
'parent' => RubyBox::Folder.new(@session, {'id' => id})
|
20
|
+
})
|
21
|
+
|
22
|
+
begin
|
23
|
+
resp = file.upload_content(data) #write a new file. If there is a conflict, update the conflicted file.
|
24
|
+
rescue RubyBox::ItemNameInUse => e
|
25
|
+
|
26
|
+
# if overwrite flag is false, simply raise exception.
|
27
|
+
raise e unless overwrite
|
28
|
+
|
29
|
+
# otherwise let's attempt to overwrite the file.
|
30
|
+
data.rewind
|
31
|
+
|
32
|
+
# The Box API occasionally does not return
|
33
|
+
# context info for an ItemNameInUse exception.
|
34
|
+
# This is a workaround around:
|
35
|
+
begin
|
36
|
+
# were were given context information about this conflict?
|
37
|
+
file = RubyBox::File.new(@session, {
|
38
|
+
'id' => e['context_info']['conflicts'][0]['id']
|
39
|
+
})
|
40
|
+
rescue
|
41
|
+
# we were not given context information about this conflict.
|
42
|
+
# attempt to lookup the file.
|
43
|
+
file = files(filename, 1000).pop
|
44
|
+
end
|
45
|
+
|
46
|
+
raise e unless file # re-raise the ItemNameInUse exception.
|
47
|
+
resp = file.update_content( data )
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_subfolder(name)
|
52
|
+
url = "#{RubyBox::API_URL}/#{resource_name}"
|
53
|
+
uri = URI.parse(url)
|
54
|
+
request = Net::HTTP::Post.new( uri.request_uri )
|
55
|
+
request.body = JSON.dump({ "name" => name, "parent" => {"id" => id} })
|
56
|
+
resp = @session.request(uri, request)
|
57
|
+
RubyBox::Folder.new(@session, resp)
|
58
|
+
end
|
59
|
+
|
60
|
+
# see http://developers.box.com/docs/#collaborations-collaboration-object
|
61
|
+
# for a list of valid roles.
|
62
|
+
def create_collaboration(email, role=:viewer)
|
63
|
+
RubyBox::Collaboration.new(@session, {
|
64
|
+
'item' => {'id' => id, 'type' => type},
|
65
|
+
'accessible_by' => {'login' => email},
|
66
|
+
'role' => role.to_s
|
67
|
+
}).create
|
68
|
+
end
|
69
|
+
|
70
|
+
# see http://developers.box.com/docs/#folders-copy-a-folder
|
71
|
+
# for a description of the behavior
|
72
|
+
def copy_to(destination, name=nil)
|
73
|
+
parent = {'parent' => {'id' => destination.id}}
|
74
|
+
parent.merge!('name' => name) if name
|
75
|
+
|
76
|
+
RubyBox::Folder.new(@session, post(folder_method(:copy), parent))
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
def post(extra_url, body)
|
81
|
+
uri = URI.parse("#{RubyBox::API_URL}/#{extra_url}")
|
82
|
+
post = Net::HTTP::Post.new(uri.request_uri)
|
83
|
+
post.body = JSON.dump(body)
|
84
|
+
@session.request(uri, post)
|
85
|
+
end
|
86
|
+
|
87
|
+
def resource_name
|
88
|
+
'folders'
|
89
|
+
end
|
90
|
+
|
91
|
+
def folder_method(method)
|
92
|
+
"folders/#{id}/#{method}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def has_mini_format?
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
def items_by_type(type, name, item_limit, offset, fields)
|
100
|
+
|
101
|
+
# allow paramters to be set via
|
102
|
+
# a hash rather than a list of arguments.
|
103
|
+
if name.is_a?(Hash)
|
104
|
+
return items_by_type(type, name[:name], name[:item_limit], name[:offset], name[:fields])
|
105
|
+
end
|
106
|
+
|
107
|
+
items(item_limit, offset, fields).select do |item|
|
108
|
+
item.kind_of? type and (name.nil? or item.name.casecmp(name) == 0)
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|