boxr 1.22.0 → 1.23.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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby-tests.yml +52 -0
- data/.gitignore +1 -0
- data/.rspec +0 -1
- data/.rubocop.yml +22 -0
- data/README.md +34 -5
- data/Rakefile +3 -4
- data/bin/boxr +5 -2
- data/boxr.gemspec +30 -25
- data/examples/enterprise_events.rb +15 -11
- data/examples/jwt_auth.rb +6 -5
- data/examples/metadata_search.rb +12 -11
- data/examples/oauth.rb +8 -11
- data/examples/remove_collaborators.rb +6 -5
- data/examples/use_events_to_send_sms.rb +35 -34
- data/examples/use_events_to_warn_about_sharing.rb +35 -31
- data/examples/user_events.rb +8 -7
- data/lib/boxr/auth.rb +41 -36
- data/lib/boxr/chunked_uploads.rb +56 -32
- data/lib/boxr/client.rb +117 -102
- data/lib/boxr/collaborations.rb +21 -14
- data/lib/boxr/collections.rb +5 -5
- data/lib/boxr/comments.rb +13 -13
- data/lib/boxr/errors.rb +25 -21
- data/lib/boxr/files.rb +87 -73
- data/lib/boxr/folders.rb +32 -29
- data/lib/boxr/groups.rb +22 -22
- data/lib/boxr/metadata.rb +23 -22
- data/lib/boxr/search.rb +34 -28
- data/lib/boxr/shared_items.rb +7 -5
- data/lib/boxr/tasks.rb +18 -18
- data/lib/boxr/users.rb +63 -38
- data/lib/boxr/version.rb +3 -1
- data/lib/boxr/watermarking.rb +10 -17
- data/lib/boxr/web_links.rb +14 -17
- data/lib/boxr/webhooks.rb +8 -7
- data/lib/boxr.rb +13 -11
- metadata +86 -28
@@ -1,18 +1,19 @@
|
|
1
|
-
require 'dotenv'
|
1
|
+
require 'dotenv'
|
2
|
+
Dotenv.load('../.env')
|
2
3
|
require 'boxr'
|
3
|
-
require 'mailgun' #make sure you 'gem install mailgun-ruby'
|
4
|
-
|
5
|
-
BOX_TRIGGER_EVENT = 'ITEM_SHARED_UPDATE,COLLABORATION_INVITE'
|
6
|
-
FROM_EMAIL = "box-admin@#{ENV['MAILGUN_SENDING_DOMAIN']}"
|
7
|
-
VALID_COLLABORATION_DOMAIN=ENV['VALID_COLLABORATION_DOMAIN'] #i.e. 'your-company.com'
|
4
|
+
require 'mailgun' # make sure you 'gem install mailgun-ruby'
|
5
|
+
|
6
|
+
BOX_TRIGGER_EVENT = 'ITEM_SHARED_UPDATE,COLLABORATION_INVITE'.freeze
|
7
|
+
FROM_EMAIL = "box-admin@#{ENV['MAILGUN_SENDING_DOMAIN']}".freeze
|
8
|
+
VALID_COLLABORATION_DOMAIN = ENV['VALID_COLLABORATION_DOMAIN'] # i.e. 'your-company.com'
|
8
9
|
|
9
10
|
@mailgun_client = Mailgun::Client.new ENV['MAILGUN_API_KEY']
|
10
11
|
@box_client = Boxr::Client.new(ENV['BOX_DEVELOPER_TOKEN'])
|
11
12
|
|
12
13
|
def file_warning_text(event)
|
13
|
-
%(
|
14
|
+
%(
|
14
15
|
Our records indicate that you just created an open shared link for the document '#{event.source.item_name}'
|
15
|
-
located in the folder '#{event.source.parent.name}'.
|
16
|
+
located in the folder '#{event.source.parent.name}'.
|
16
17
|
|
17
18
|
If you meant to do this, please ignore this email. Otherwise, please update this shared link
|
18
19
|
so that it is no longer open access.
|
@@ -23,8 +24,8 @@ Your Box Admin
|
|
23
24
|
end
|
24
25
|
|
25
26
|
def folder_warning_text(event)
|
26
|
-
%(
|
27
|
-
Our records indicate that you just created an open shared link for the folder '#{event.source.item_name}'.
|
27
|
+
%(
|
28
|
+
Our records indicate that you just created an open shared link for the folder '#{event.source.item_name}'.
|
28
29
|
|
29
30
|
If you meant to do this, please ignore this email. Otherwise, please update this shared link
|
30
31
|
so that it is no longer open access.
|
@@ -35,9 +36,9 @@ Your Box Admin
|
|
35
36
|
end
|
36
37
|
|
37
38
|
def collaboration_warning_text(event)
|
38
|
-
%(
|
39
|
+
%(
|
39
40
|
Our records indicate that you just invited #{event.accessible_by.login} to be an external collaborator
|
40
|
-
with the role of #{event.additional_details.role} in the folder '#{event.source.folder_name}'.
|
41
|
+
with the role of #{event.additional_details.role} in the folder '#{event.source.folder_name}'.
|
41
42
|
|
42
43
|
If you meant to do this, please ignore this email. Otherwise, please remove this collaborator.
|
43
44
|
|
@@ -47,46 +48,49 @@ Your Box Admin
|
|
47
48
|
end
|
48
49
|
|
49
50
|
def send_email_to_box_user(from, to, subject, text)
|
50
|
-
#this example code uses Mailgun to send email, but you can use any standard SMTP server
|
51
|
-
message_params = {:
|
51
|
+
# this example code uses Mailgun to send email, but you can use any standard SMTP server
|
52
|
+
message_params = { from: from, to: to, subject: subject, text: text }
|
52
53
|
@mailgun_client.send_message ENV['MAILGUN_SENDING_DOMAIN'], message_params
|
53
54
|
|
54
55
|
puts "Sent email to #{to} with subject '#{subject}'"
|
55
56
|
end
|
56
57
|
|
57
|
-
#need to look back in time to make sure we get a valid stream position;
|
58
|
-
#normally your app will be persisting the last known stream position and you wouldn't have to look this up
|
58
|
+
# need to look back in time to make sure we get a valid stream position;
|
59
|
+
# normally your app will be persisting the last known stream position and you wouldn't have to look this up
|
59
60
|
now = Time.now.utc
|
60
|
-
start_date = now - (60*60*24) #one day ago
|
61
|
+
start_date = now - (60 * 60 * 24) # one day ago
|
61
62
|
result = @box_client.enterprise_events(created_after: start_date, created_before: now)
|
62
63
|
|
63
|
-
#now that we have the latest stream position let's start monitoring in real-time
|
64
|
-
@box_client.enterprise_events_stream(result.next_stream_position, event_type: BOX_TRIGGER_EVENT,
|
65
|
-
|
64
|
+
# now that we have the latest stream position let's start monitoring in real-time
|
65
|
+
@box_client.enterprise_events_stream(result.next_stream_position, event_type: BOX_TRIGGER_EVENT,
|
66
|
+
refresh_period: 60) do |result|
|
67
|
+
if result.events.count.zero?
|
66
68
|
puts "no new #{BOX_TRIGGER_EVENT} events..."
|
67
69
|
else
|
68
70
|
result.events.each do |e|
|
69
71
|
puts "detected new #{e.event_type}"
|
70
|
-
if e.event_type=='ITEM_SHARED_UPDATE'
|
71
|
-
if e.source.item_type=='file'
|
72
|
+
if e.event_type == 'ITEM_SHARED_UPDATE'
|
73
|
+
if e.source.item_type == 'file'
|
72
74
|
file = @box_client.file(e.source.item_id)
|
73
|
-
if file.shared_link.effective_access=='open'
|
74
|
-
send_email_to_box_user(FROM_EMAIL, e.created_by.login,
|
75
|
+
if file.shared_link.effective_access == 'open'
|
76
|
+
send_email_to_box_user(FROM_EMAIL, e.created_by.login,
|
77
|
+
'WARNING: Open Shared Link Detected', file_warning_text(e))
|
75
78
|
end
|
76
|
-
elsif e.source.item_type=='folder'
|
79
|
+
elsif e.source.item_type == 'folder'
|
77
80
|
folder = @box_client.folder(e.source.item_id)
|
78
|
-
if folder.shared_link.effective_access=='open'
|
79
|
-
send_email_to_box_user(FROM_EMAIL, e.created_by.login,
|
81
|
+
if folder.shared_link.effective_access == 'open'
|
82
|
+
send_email_to_box_user(FROM_EMAIL, e.created_by.login,
|
83
|
+
'WARNING: Open Shared Link Detected', folder_warning_text(e))
|
80
84
|
end
|
81
85
|
end
|
82
|
-
elsif e.event_type=='COLLABORATION_INVITE'
|
86
|
+
elsif e.event_type == 'COLLABORATION_INVITE'
|
83
87
|
email = e.accessible_by.login
|
84
88
|
domain = email.split('@').last
|
85
|
-
unless domain.downcase==VALID_COLLABORATION_DOMAIN.downcase
|
86
|
-
send_email_to_box_user(FROM_EMAIL, e.created_by.login,
|
89
|
+
unless domain.downcase == VALID_COLLABORATION_DOMAIN.downcase
|
90
|
+
send_email_to_box_user(FROM_EMAIL, e.created_by.login,
|
91
|
+
'WARNING: External Collaborator Detected', collaboration_warning_text(e))
|
87
92
|
end
|
88
93
|
end
|
89
94
|
end
|
90
95
|
end
|
91
96
|
end
|
92
|
-
|
data/examples/user_events.rb
CHANGED
@@ -1,23 +1,24 @@
|
|
1
|
-
require 'dotenv'
|
1
|
+
require 'dotenv'
|
2
|
+
Dotenv.load('../.env')
|
2
3
|
require 'boxr'
|
3
4
|
require 'awesome_print'
|
4
5
|
require 'lru_redux'
|
5
6
|
|
6
7
|
client = Boxr::Client.new(ENV['BOX_DEVELOPER_TOKEN'])
|
7
|
-
cache = LruRedux::Cache.new(1000)
|
8
|
+
cache = LruRedux::Cache.new(1000, true)
|
8
9
|
|
9
10
|
stream_position = :now
|
10
|
-
loop do
|
11
|
-
puts
|
11
|
+
loop do
|
12
|
+
puts 'fetching events...'
|
12
13
|
event_response = client.user_events(stream_position)
|
13
14
|
event_response.events.each do |event|
|
14
|
-
#we need to de-dupe the events because we will receive multiple events with the same event_id; Box does this to ensure that we get the event
|
15
|
+
# we need to de-dupe the events because we will receive multiple events with the same event_id; Box does this to ensure that we get the event
|
15
16
|
key = "/box-event/#{event.event_id}"
|
16
|
-
if
|
17
|
+
if cache.fetch(key).nil?
|
17
18
|
cache[key] = true
|
18
19
|
puts event.event_type
|
19
20
|
end
|
20
21
|
end
|
21
22
|
stream_position = event_response.next_stream_position
|
22
23
|
sleep 5
|
23
|
-
end
|
24
|
+
end
|
data/lib/boxr/auth.rb
CHANGED
@@ -1,29 +1,32 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
module Boxr
|
4
|
+
JWT_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
|
5
|
+
TOKEN_EXCHANGE_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'
|
6
|
+
TOKEN_EXCHANGE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange'
|
6
7
|
|
7
|
-
def self.oauth_url(state, host:
|
8
|
-
|
8
|
+
def self.oauth_url(state, host: 'app.box.com', response_type: 'code', scope: nil, folder_id: nil,
|
9
|
+
client_id: ENV['BOX_CLIENT_ID'])
|
10
|
+
template = Addressable::Template.new('https://{host}/api/oauth2/authorize{?query*}')
|
9
11
|
|
10
|
-
query = {
|
11
|
-
|
12
|
-
query[
|
12
|
+
query = { 'response_type' => response_type.to_s, 'state' => state.to_s,
|
13
|
+
'client_id' => client_id.to_s }
|
14
|
+
query['scope'] = scope.to_s unless scope.nil?
|
15
|
+
query['folder_id'] = folder_id.to_s unless folder_id.nil?
|
13
16
|
|
14
|
-
|
15
|
-
uri
|
17
|
+
template.expand({ 'host' => host.to_s, 'query' => query })
|
16
18
|
end
|
17
19
|
|
18
|
-
def self.get_tokens(code=nil, grant_type:
|
20
|
+
def self.get_tokens(code = nil, grant_type: 'authorization_code', assertion: nil, scope: nil,
|
21
|
+
username: nil, client_id: ENV['BOX_CLIENT_ID'], client_secret: ENV['BOX_CLIENT_SECRET'], box_subject_type: nil, box_subject_id: nil)
|
19
22
|
uri = Boxr::Client::AUTH_URI
|
20
23
|
body = "grant_type=#{grant_type}&client_id=#{client_id}&client_secret=#{client_secret}"
|
21
|
-
body
|
22
|
-
body
|
23
|
-
body
|
24
|
-
body
|
25
|
-
body
|
26
|
-
body
|
24
|
+
body += "&code=#{code}" unless code.nil?
|
25
|
+
body += "&scope=#{scope}" unless scope.nil?
|
26
|
+
body += "&username=#{username}" unless username.nil?
|
27
|
+
body += "&assertion=#{assertion}" unless assertion.nil?
|
28
|
+
body += "&box_subject_type=#{box_subject_type}" unless box_subject_type.nil?
|
29
|
+
body += "&box_subject_id=#{box_subject_id}" unless box_subject_id.nil?
|
27
30
|
|
28
31
|
auth_post(uri, body)
|
29
32
|
end
|
@@ -32,25 +35,30 @@ module Boxr
|
|
32
35
|
public_key_id: ENV['JWT_PUBLIC_KEY_ID'], enterprise_id: ENV['BOX_ENTERPRISE_ID'],
|
33
36
|
client_id: ENV['BOX_CLIENT_ID'], client_secret: ENV['BOX_CLIENT_SECRET'])
|
34
37
|
unlocked_private_key = unlock_key(private_key, private_key_password)
|
35
|
-
assertion = jwt_assertion(unlocked_private_key, client_id, enterprise_id,
|
36
|
-
|
38
|
+
assertion = jwt_assertion(unlocked_private_key, client_id, enterprise_id, 'enterprise',
|
39
|
+
public_key_id)
|
40
|
+
get_token(grant_type: JWT_GRANT_TYPE, assertion: assertion, client_id: client_id,
|
41
|
+
client_secret: client_secret)
|
37
42
|
end
|
38
43
|
|
39
44
|
def self.get_user_token(user_id, private_key: ENV['JWT_PRIVATE_KEY'], private_key_password: ENV['JWT_PRIVATE_KEY_PASSWORD'],
|
40
45
|
public_key_id: ENV['JWT_PUBLIC_KEY_ID'], client_id: ENV['BOX_CLIENT_ID'], client_secret: ENV['BOX_CLIENT_SECRET'])
|
41
46
|
unlocked_private_key = unlock_key(private_key, private_key_password)
|
42
|
-
assertion = jwt_assertion(unlocked_private_key, client_id, user_id,
|
43
|
-
get_token(grant_type: JWT_GRANT_TYPE, assertion: assertion, client_id: client_id,
|
47
|
+
assertion = jwt_assertion(unlocked_private_key, client_id, user_id, 'user', public_key_id)
|
48
|
+
get_token(grant_type: JWT_GRANT_TYPE, assertion: assertion, client_id: client_id,
|
49
|
+
client_secret: client_secret)
|
44
50
|
end
|
45
51
|
|
46
|
-
def self.refresh_tokens(refresh_token, client_id: ENV['BOX_CLIENT_ID'],
|
52
|
+
def self.refresh_tokens(refresh_token, client_id: ENV['BOX_CLIENT_ID'],
|
53
|
+
client_secret: ENV['BOX_CLIENT_SECRET'])
|
47
54
|
uri = Boxr::Client::AUTH_URI
|
48
55
|
body = "grant_type=refresh_token&refresh_token=#{refresh_token}&client_id=#{client_id}&client_secret=#{client_secret}"
|
49
56
|
|
50
57
|
auth_post(uri, body)
|
51
58
|
end
|
52
59
|
|
53
|
-
def self.revoke_tokens(token, client_id: ENV['BOX_CLIENT_ID'],
|
60
|
+
def self.revoke_tokens(token, client_id: ENV['BOX_CLIENT_ID'],
|
61
|
+
client_secret: ENV['BOX_CLIENT_SECRET'])
|
54
62
|
uri = Boxr::Client::REVOKE_AUTH_URI
|
55
63
|
body = "client_id=#{client_id}&client_secret=#{client_secret}&token=#{token}"
|
56
64
|
|
@@ -64,20 +72,17 @@ module Boxr
|
|
64
72
|
resource_url = "#{resouce_uri}/#{resource_id}"
|
65
73
|
|
66
74
|
body = "subject_token=#{subject_token}&subject_token_type=#{TOKEN_EXCHANGE_TOKEN_TYPE}&scope=#{scope}&grant_type=#{TOKEN_EXCHANGE_GRANT_TYPE}"
|
67
|
-
body
|
75
|
+
body += "&resource=#{resource_url}" unless resource_id.nil?
|
68
76
|
|
69
77
|
auth_post(uri, body)
|
70
78
|
end
|
71
79
|
|
72
80
|
class << self
|
73
|
-
alias
|
74
|
-
alias
|
75
|
-
alias
|
81
|
+
alias get_token get_tokens
|
82
|
+
alias refresh_token refresh_tokens
|
83
|
+
alias revoke_token revoke_tokens
|
76
84
|
end
|
77
85
|
|
78
|
-
private
|
79
|
-
|
80
|
-
|
81
86
|
def self.jwt_assertion(private_key, iss, sub, box_sub_type, public_key_id)
|
82
87
|
payload = {
|
83
88
|
iss: iss,
|
@@ -91,7 +96,7 @@ module Boxr
|
|
91
96
|
additional_headers = {}
|
92
97
|
additional_headers['kid'] = public_key_id unless public_key_id.nil?
|
93
98
|
|
94
|
-
JWT.encode(payload, private_key,
|
99
|
+
JWT.encode(payload, private_key, 'RS256', additional_headers)
|
95
100
|
end
|
96
101
|
|
97
102
|
def self.auth_post(uri, body)
|
@@ -99,12 +104,12 @@ module Boxr
|
|
99
104
|
|
100
105
|
res = BOX_CLIENT.post(uri, body: body)
|
101
106
|
|
102
|
-
|
103
|
-
body_json = JSON.load(res.body)
|
104
|
-
return BoxrMash.new(body_json)
|
105
|
-
else
|
107
|
+
unless res.status == 200
|
106
108
|
raise BoxrError.new(status: res.status, body: res.body, header: res.header)
|
107
109
|
end
|
110
|
+
|
111
|
+
body_json = JSON.parse(res.body)
|
112
|
+
BoxrMash.new(body_json)
|
108
113
|
end
|
109
114
|
|
110
115
|
def self.unlock_key(private_key, private_key_password)
|
data/lib/boxr/chunked_uploads.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Boxr
|
2
4
|
class Client
|
3
|
-
|
4
5
|
def chunked_upload_create_session_new_file(path_to_file, parent, name: nil)
|
5
|
-
filename = name
|
6
|
+
filename = name || File.basename(path_to_file)
|
6
7
|
|
7
8
|
File.open(path_to_file) do |file|
|
8
9
|
chunked_upload_create_session_new_file_from_io(file, parent, filename)
|
@@ -13,14 +14,15 @@ module Boxr
|
|
13
14
|
parent_id = ensure_id(parent)
|
14
15
|
|
15
16
|
uri = "#{UPLOAD_URI}/files/upload_sessions"
|
16
|
-
body = {folder_id: parent_id, file_size: io.size, file_name: name}
|
17
|
-
session_info,
|
17
|
+
body = { folder_id: parent_id, file_size: io.size, file_name: name }
|
18
|
+
session_info, = post(uri, body, content_type: 'application/json',
|
19
|
+
success_codes: [200, 201, 202])
|
18
20
|
|
19
21
|
session_info
|
20
22
|
end
|
21
23
|
|
22
24
|
def chunked_upload_create_session_new_version(path_to_file, file, name: nil)
|
23
|
-
filename = name
|
25
|
+
filename = name || File.basename(path_to_file)
|
24
26
|
|
25
27
|
File.open(path_to_file) do |io|
|
26
28
|
chunked_upload_create_session_new_version_from_io(io, file, filename)
|
@@ -30,15 +32,16 @@ module Boxr
|
|
30
32
|
def chunked_upload_create_session_new_version_from_io(io, file, name)
|
31
33
|
file_id = ensure_id(file)
|
32
34
|
uri = "#{UPLOAD_URI}/files/#{file_id}/upload_sessions"
|
33
|
-
body = {file_size: io.size, file_name: name}
|
34
|
-
session_info,
|
35
|
+
body = { file_size: io.size, file_name: name }
|
36
|
+
session_info, = post(uri, body, content_type: 'application/json',
|
37
|
+
success_codes: [200, 201, 202])
|
35
38
|
|
36
39
|
session_info
|
37
40
|
end
|
38
41
|
|
39
42
|
def chunked_upload_get_session(session_id)
|
40
43
|
uri = "#{UPLOAD_URI}/files/upload_sessions/#{session_id}"
|
41
|
-
session_info,
|
44
|
+
session_info, = get(uri)
|
42
45
|
|
43
46
|
session_info
|
44
47
|
end
|
@@ -60,7 +63,8 @@ module Boxr
|
|
60
63
|
|
61
64
|
uri = "#{UPLOAD_URI}/files/upload_sessions/#{session_id}"
|
62
65
|
body = data
|
63
|
-
part_info,
|
66
|
+
part_info, = put(uri, body, process_body: false, digest: digest,
|
67
|
+
content_type: 'application/octet-stream', content_range: range, success_codes: [200, 201, 202])
|
64
68
|
|
65
69
|
part_info.part
|
66
70
|
end
|
@@ -70,82 +74,100 @@ module Boxr
|
|
70
74
|
query = {}
|
71
75
|
query[:limit] = limit unless limit.nil?
|
72
76
|
query[:offset] = offset unless offset.nil?
|
73
|
-
parts_info,
|
77
|
+
parts_info, = get(uri, query: query)
|
74
78
|
|
75
79
|
parts_info.entries
|
76
80
|
end
|
77
81
|
|
78
|
-
def chunked_upload_commit(path_to_file, session_id, parts, content_created_at: nil,
|
82
|
+
def chunked_upload_commit(path_to_file, session_id, parts, content_created_at: nil,
|
83
|
+
content_modified_at: nil, if_match: nil, if_non_match: nil)
|
79
84
|
File.open(path_to_file) do |file|
|
80
|
-
chunked_upload_commit_from_io(file, session_id, parts,
|
85
|
+
chunked_upload_commit_from_io(file, session_id, parts,
|
86
|
+
content_created_at: content_created_at, content_modified_at: content_modified_at, if_match: if_match, if_non_match: if_non_match)
|
81
87
|
end
|
82
88
|
end
|
83
89
|
|
84
|
-
def chunked_upload_commit_from_io(io, session_id, parts, content_created_at: nil,
|
90
|
+
def chunked_upload_commit_from_io(io, session_id, parts, content_created_at: nil,
|
91
|
+
content_modified_at: nil, if_match: nil, if_non_match: nil)
|
85
92
|
io.pos = 0
|
86
93
|
digest = Digest::SHA1.new
|
87
|
-
while (buf = io.read(8 * 1024**2)) && buf.size
|
94
|
+
while (buf = io.read(8 * 1024**2)) && buf.size.positive?
|
88
95
|
digest.update(buf)
|
89
96
|
end
|
90
97
|
io.rewind
|
91
98
|
digest = "sha=#{digest.base64digest}"
|
92
99
|
|
93
100
|
attributes = {}
|
94
|
-
|
95
|
-
|
101
|
+
unless content_created_at.nil?
|
102
|
+
attributes[:content_created_at] =
|
103
|
+
content_created_at.to_datetime.rfc3339
|
104
|
+
end
|
105
|
+
unless content_modified_at.nil?
|
106
|
+
attributes[:content_modified_at] =
|
107
|
+
content_modified_at.to_datetime.rfc3339
|
108
|
+
end
|
96
109
|
|
97
110
|
uri = "#{UPLOAD_URI}/files/upload_sessions/#{session_id}/commit"
|
98
111
|
body = {
|
99
112
|
parts: parts,
|
100
113
|
attributes: attributes
|
101
114
|
}
|
102
|
-
commit_info,
|
115
|
+
commit_info, = post(uri, body, process_body: true, digest: digest,
|
116
|
+
content_type: 'application/json', if_match: if_match, if_non_match: if_non_match, success_codes: [200, 201, 202])
|
103
117
|
|
104
118
|
commit_info
|
105
119
|
end
|
106
120
|
|
107
121
|
def chunked_upload_abort_session(session_id)
|
108
122
|
uri = "#{UPLOAD_URI}/files/upload_sessions/#{session_id}"
|
109
|
-
abort_info,
|
123
|
+
abort_info, = delete(uri)
|
110
124
|
|
111
125
|
abort_info
|
112
126
|
end
|
113
127
|
|
114
|
-
def chunked_upload_file(path_to_file, parent, name: nil, n_threads: 1, content_created_at: nil,
|
115
|
-
|
128
|
+
def chunked_upload_file(path_to_file, parent, name: nil, n_threads: 1, content_created_at: nil,
|
129
|
+
content_modified_at: nil)
|
130
|
+
filename = name || File.basename(path_to_file)
|
116
131
|
|
117
132
|
File.open(path_to_file) do |file|
|
118
|
-
chunked_upload_file_from_io(file, parent, filename, n_threads: n_threads,
|
133
|
+
chunked_upload_file_from_io(file, parent, filename, n_threads: n_threads,
|
134
|
+
content_created_at: content_created_at, content_modified_at: content_modified_at)
|
119
135
|
end
|
120
136
|
end
|
121
137
|
|
122
|
-
def chunked_upload_file_from_io(io, parent, name, n_threads: 1, content_created_at: nil,
|
138
|
+
def chunked_upload_file_from_io(io, parent, name, n_threads: 1, content_created_at: nil,
|
139
|
+
content_modified_at: nil)
|
123
140
|
session = nil
|
124
141
|
file_info = nil
|
125
142
|
|
126
143
|
session = chunked_upload_create_session_new_file_from_io(io, parent, name)
|
127
144
|
|
128
|
-
file_info = chunked_upload_to_session_from_io(io, session, n_threads: n_threads,
|
145
|
+
file_info = chunked_upload_to_session_from_io(io, session, n_threads: n_threads,
|
146
|
+
content_created_at: nil, content_modified_at: nil)
|
129
147
|
file_info
|
130
148
|
ensure
|
131
149
|
chunked_upload_abort_session(session.id) if file_info.nil? && !session.nil?
|
132
150
|
end
|
133
151
|
|
134
|
-
def chunked_upload_new_version_of_file(path_to_file, file, name: nil, n_threads: 1,
|
135
|
-
|
152
|
+
def chunked_upload_new_version_of_file(path_to_file, file, name: nil, n_threads: 1,
|
153
|
+
content_created_at: nil, content_modified_at: nil)
|
154
|
+
filename = name || File.basename(path_to_file)
|
136
155
|
|
137
156
|
File.open(path_to_file) do |io|
|
138
|
-
chunked_upload_new_version_of_file_from_io(io, file, filename, n_threads: n_threads,
|
157
|
+
chunked_upload_new_version_of_file_from_io(io, file, filename, n_threads: n_threads,
|
158
|
+
content_created_at: content_created_at, content_modified_at: content_modified_at)
|
139
159
|
end
|
140
160
|
end
|
141
161
|
|
142
|
-
def chunked_upload_new_version_of_file_from_io(io, file, name, n_threads: 1,
|
162
|
+
def chunked_upload_new_version_of_file_from_io(io, file, name, n_threads: 1,
|
163
|
+
content_created_at: nil, content_modified_at: nil)
|
143
164
|
session = nil
|
144
165
|
file_info = nil
|
145
166
|
|
146
167
|
session = chunked_upload_create_session_new_version_from_io(io, file, name)
|
147
168
|
|
148
|
-
file_info = chunked_upload_to_session_from_io(io, session, n_threads: n_threads,
|
169
|
+
file_info = chunked_upload_to_session_from_io(io, session, n_threads: n_threads,
|
170
|
+
content_created_at: nil, content_modified_at: nil)
|
149
171
|
file_info
|
150
172
|
ensure
|
151
173
|
chunked_upload_abort_session(session.id) if file_info.nil? && !session.nil?
|
@@ -155,7 +177,8 @@ module Boxr
|
|
155
177
|
|
156
178
|
PARALLEL_GEM_REQUIREMENT = Gem::Requirement.create('~> 1.0').freeze
|
157
179
|
|
158
|
-
def chunked_upload_to_session_from_io(io, session, n_threads: 1, content_created_at: nil,
|
180
|
+
def chunked_upload_to_session_from_io(io, session, n_threads: 1, content_created_at: nil,
|
181
|
+
content_modified_at: nil)
|
159
182
|
content_ranges = []
|
160
183
|
offset = 0
|
161
184
|
loop do
|
@@ -168,7 +191,9 @@ module Boxr
|
|
168
191
|
|
169
192
|
parts =
|
170
193
|
if n_threads > 1
|
171
|
-
|
194
|
+
unless gem_parallel_available?
|
195
|
+
raise BoxrError.new(boxr_message: "parallel chunked uploads requires gem parallel (#{PARALLEL_GEM_REQUIREMENT}) to be loaded")
|
196
|
+
end
|
172
197
|
|
173
198
|
Parallel.map(content_ranges, in_threads: n_threads) do |content_range|
|
174
199
|
File.open(io.path) do |io_dup|
|
@@ -187,11 +212,10 @@ module Boxr
|
|
187
212
|
end
|
188
213
|
|
189
214
|
def gem_parallel_available?
|
190
|
-
gem_spec
|
215
|
+
gem_spec = Gem.loaded_specs['parallel']
|
191
216
|
return false if gem_spec.nil?
|
192
217
|
|
193
218
|
PARALLEL_GEM_REQUIREMENT.satisfied_by?(gem_spec.version) && defined?(Parallel)
|
194
219
|
end
|
195
|
-
|
196
220
|
end
|
197
221
|
end
|