| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Backblaze::B2V2Client; |
|
2
|
|
|
|
|
|
|
# API client library for V2 of the API to Backblaze B2 object storage |
|
3
|
|
|
|
|
|
|
# Allows for creating/deleting buckets, listing files in buckets, and uploading/downloading files |
|
4
|
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
$Backblaze::B2V2Client::VERSION = '1.6'; |
|
6
|
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
# our dependencies: |
|
8
|
1
|
|
|
1
|
|
953
|
use Cpanel::JSON::XS; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
62
|
|
|
9
|
1
|
|
|
1
|
|
557
|
use Digest::SHA qw(sha1_hex); |
|
|
1
|
|
|
|
|
3257
|
|
|
|
1
|
|
|
|
|
88
|
|
|
10
|
1
|
|
|
1
|
|
533
|
use MIME::Base64; |
|
|
1
|
|
|
|
|
645
|
|
|
|
1
|
|
|
|
|
66
|
|
|
11
|
1
|
|
|
1
|
|
938
|
use Path::Tiny; |
|
|
1
|
|
|
|
|
11915
|
|
|
|
1
|
|
|
|
|
57
|
|
|
12
|
1
|
|
|
1
|
|
567
|
use URI::Escape; |
|
|
1
|
|
|
|
|
1521
|
|
|
|
1
|
|
|
|
|
61
|
|
|
13
|
1
|
|
|
1
|
|
927
|
use WWW::Mechanize; |
|
|
1
|
|
|
|
|
166893
|
|
|
|
1
|
|
|
|
|
49
|
|
|
14
|
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
# I wish I could apply this to my diet. |
|
16
|
1
|
|
|
1
|
|
10
|
use strict; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
23
|
|
|
17
|
1
|
|
|
1
|
|
5
|
use warnings; |
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
3332
|
|
|
18
|
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
# object constructor; will automatically authorize this session |
|
20
|
|
|
|
|
|
|
sub new { |
|
21
|
1
|
|
|
1
|
1
|
693
|
my $class = shift; |
|
22
|
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
# required args are the account ID and application_key |
|
24
|
1
|
|
|
|
|
3
|
my ($application_key_id, $application_key) = @_; |
|
25
|
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
# cannot operate without these |
|
27
|
1
|
50
|
33
|
|
|
16
|
if (!$application_key_id || !$application_key) { |
|
28
|
0
|
|
|
|
|
0
|
die "ERROR: Cannot create B2V5Client object without both application_key_id and application_key arguments.\n"; |
|
29
|
|
|
|
|
|
|
} |
|
30
|
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
# initiate class with my keys + WWW::Mechanize object |
|
32
|
1
|
|
|
|
|
8
|
my $self = bless { |
|
33
|
|
|
|
|
|
|
'application_key_id' => $application_key_id, |
|
34
|
|
|
|
|
|
|
'application_key' => $application_key, |
|
35
|
|
|
|
|
|
|
'mech' => WWW::Mechanize->new( |
|
36
|
|
|
|
|
|
|
timeout => 60, |
|
37
|
|
|
|
|
|
|
autocheck => 0, |
|
38
|
|
|
|
|
|
|
cookie_jar => {}, |
|
39
|
|
|
|
|
|
|
keep_alive => 1, |
|
40
|
|
|
|
|
|
|
), |
|
41
|
|
|
|
|
|
|
}, $class; |
|
42
|
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
# now start our B2 session via method below |
|
44
|
1
|
|
|
|
|
19139
|
$self->b2_authorize_account(); # this adds more goodness to $self for use in the other methods |
|
45
|
|
|
|
|
|
|
|
|
46
|
1
|
|
|
|
|
6
|
return $self; |
|
47
|
|
|
|
|
|
|
} |
|
48
|
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
# method to start your backblaze session: authorize the account and get your api URL's |
|
50
|
|
|
|
|
|
|
sub b2_authorize_account { |
|
51
|
1
|
|
|
1
|
0
|
3
|
my $self = shift; |
|
52
|
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
# prepare our authorization header |
|
54
|
1
|
|
|
|
|
17
|
my $encoded_auth_string = encode_base64($self->{application_key_id}.':'.$self->{application_key}); |
|
55
|
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
# add that header in |
|
57
|
1
|
|
|
|
|
7
|
$self->{mech}->add_header( 'Authorization' => 'Basic '.$encoded_auth_string ); |
|
58
|
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
# call the b2_talker() method to authenticate our session |
|
60
|
1
|
|
|
|
|
16
|
$self->b2_talker('url' => 'https://api.backblazeb2.com/b2api/v2/b2_authorize_account' ); |
|
61
|
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
# if we succeeded, load in our authentication and prepare to proceed |
|
63
|
1
|
50
|
|
|
|
6
|
if ($self->{current_status} eq 'OK') { |
|
64
|
|
|
|
|
|
|
|
|
65
|
1
|
|
|
|
|
6
|
$self->{account_id} = $self->{b2_response}{accountId}; |
|
66
|
1
|
|
|
|
|
5
|
$self->{api_url} = $self->{b2_response}{apiUrl}; |
|
67
|
1
|
|
|
|
|
4
|
$self->{account_authorization_token} = $self->{b2_response}{authorizationToken}; |
|
68
|
1
|
|
|
|
|
4
|
$self->{download_url} = $self->{b2_response}{downloadUrl}; |
|
69
|
|
|
|
|
|
|
# for uploading large files |
|
70
|
1
|
|
50
|
|
|
7
|
$self->{recommended_part_size} = $self->{b2_response}{recommendedPartSize} || 104857600; |
|
71
|
|
|
|
|
|
|
# ready! |
|
72
|
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
# otherwise, not ready! |
|
74
|
|
|
|
|
|
|
} else { |
|
75
|
0
|
|
|
|
|
0
|
$self->{b2_login_error} = 1; |
|
76
|
|
|
|
|
|
|
} |
|
77
|
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
# return current status |
|
79
|
1
|
|
|
|
|
3
|
return $self->{current_status}; |
|
80
|
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
} |
|
82
|
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
# method to download a file by ID; probably most commonly used |
|
84
|
|
|
|
|
|
|
sub b2_download_file_by_id { |
|
85
|
1
|
|
|
1
|
1
|
1321
|
my $self = shift; |
|
86
|
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
# required arg is the file ID |
|
88
|
|
|
|
|
|
|
# option arg is a target directory to auto-save the new file into |
|
89
|
1
|
|
|
|
|
4
|
my ($file_id, $save_to_location) = @_; |
|
90
|
|
|
|
|
|
|
|
|
91
|
1
|
50
|
|
|
|
9
|
if (!$file_id) { |
|
92
|
0
|
|
|
|
|
0
|
$self->error_tracker('The file_id must be provided for b2_download_file_by_id().'); |
|
93
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
94
|
|
|
|
|
|
|
} |
|
95
|
|
|
|
|
|
|
|
|
96
|
|
|
|
|
|
|
# send the request, as a GET |
|
97
|
|
|
|
|
|
|
$self->b2_talker( |
|
98
|
|
|
|
|
|
|
'url' => $self->{download_url}.'/b2api/v2/b2_download_file_by_id?fileId='.$file_id, |
|
99
|
|
|
|
|
|
|
'authorization' => $self->{account_authorization_token}, |
|
100
|
1
|
|
|
|
|
10
|
); |
|
101
|
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
# if the file was found, you will have the relevant headers in %{ $self->{b2_response} } |
|
103
|
|
|
|
|
|
|
# as well as the file's contents in $self->{b2_response}{file_contents} |
|
104
|
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
# if they provided a save-to location (a directory) and the file was found, let's save it out |
|
106
|
1
|
50
|
33
|
|
|
11
|
if ($self->{current_status} eq 'OK' && $save_to_location) { |
|
107
|
0
|
|
|
|
|
0
|
$self->save_downloaded_file($save_to_location); |
|
108
|
|
|
|
|
|
|
} |
|
109
|
|
|
|
|
|
|
|
|
110
|
|
|
|
|
|
|
# return current status |
|
111
|
1
|
|
|
|
|
7
|
return $self->{current_status}; |
|
112
|
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
} |
|
114
|
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
# method to download a file via the bucket name + file name |
|
116
|
|
|
|
|
|
|
sub b2_download_file_by_name { |
|
117
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
118
|
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
# required args are the bucket name and file name |
|
120
|
0
|
|
|
|
|
0
|
my ($bucket_name, $file_name, $save_to_location) = @_; |
|
121
|
|
|
|
|
|
|
|
|
122
|
0
|
0
|
0
|
|
|
0
|
if (!$bucket_name || !$file_name) { |
|
123
|
0
|
|
|
|
|
0
|
$self->error_tracker('The bucket_name and file_name must be provided for b2_download_file_by_name().'); |
|
124
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
125
|
|
|
|
|
|
|
} |
|
126
|
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
# send the request, as a GET |
|
128
|
|
|
|
|
|
|
$self->b2_talker( |
|
129
|
|
|
|
|
|
|
'url' => $self->{download_url}.'/file/'.uri_escape($bucket_name).'/'.uri_escape($file_name), |
|
130
|
|
|
|
|
|
|
'authorization' => $self->{account_authorization_token}, |
|
131
|
0
|
|
|
|
|
0
|
); |
|
132
|
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
# if the file was found, you will have the relevant headers in %{ $self->{b2_response} } |
|
135
|
|
|
|
|
|
|
# as well as the file's contents in $self->{b2_response}{file_contents} |
|
136
|
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
# if they provided a save-to location (a directory) and the file was found, let's save it out |
|
138
|
0
|
0
|
0
|
|
|
0
|
if ($self->{current_status} eq 'OK' && $save_to_location) { |
|
139
|
0
|
|
|
|
|
0
|
$self->save_downloaded_file($save_to_location); |
|
140
|
|
|
|
|
|
|
} |
|
141
|
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
# return current status |
|
143
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
144
|
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
} |
|
146
|
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
# method to save downloaded files into a target location |
|
148
|
|
|
|
|
|
|
# only call after successfully calling b2_download_file_by_id() or b2_download_file_by_name() |
|
149
|
|
|
|
|
|
|
sub save_downloaded_file { |
|
150
|
0
|
|
|
0
|
0
|
0
|
my $self = shift; |
|
151
|
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
# required arg is a valid directory on this file system |
|
153
|
0
|
|
|
|
|
0
|
my ($save_to_location) = @_; |
|
154
|
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
# error out if that location don't exist |
|
156
|
0
|
0
|
0
|
|
|
0
|
if (!$save_to_location || !(-d "$save_to_location") ) { |
|
157
|
0
|
|
|
|
|
0
|
$self->error_tracker("Can not auto-save file without a valid location. $save_to_location"); |
|
158
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
159
|
|
|
|
|
|
|
} |
|
160
|
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
# make sure they actually downloaded a file |
|
162
|
0
|
0
|
0
|
|
|
0
|
if ( !$self->{b2_response}{'X-Bz-File-Name'} || !length($self->{b2_response}{file_contents}) ) { |
|
163
|
0
|
|
|
|
|
0
|
$self->error_tracker("Can not auto-save without first downloading a file."); |
|
164
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
165
|
|
|
|
|
|
|
} |
|
166
|
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
# still here? do the save |
|
168
|
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
# add the filename |
|
170
|
0
|
|
|
|
|
0
|
$save_to_location .= '/'.$self->{b2_response}{'X-Bz-File-Name'}; |
|
171
|
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
# i really love Path::Tiny |
|
173
|
0
|
|
|
|
|
0
|
path($save_to_location)->spew_raw( $self->{b2_response}{file_contents} ); |
|
174
|
|
|
|
|
|
|
|
|
175
|
|
|
|
|
|
|
# return current status |
|
176
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
177
|
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
} |
|
179
|
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
# method to upload a file into Backblaze B2 |
|
181
|
|
|
|
|
|
|
sub b2_upload_file { |
|
182
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
183
|
|
|
|
|
|
|
|
|
184
|
0
|
|
|
|
|
0
|
my (%args) = @_; |
|
185
|
|
|
|
|
|
|
# this must include valid entries for 'new_file_name' and 'bucket_name' |
|
186
|
|
|
|
|
|
|
# and it has to include either the raw file contents in 'file_contents' |
|
187
|
|
|
|
|
|
|
# or a valid location in 'file_location' |
|
188
|
|
|
|
|
|
|
# also, you can include 'content_type' (which would be the MIME Type' |
|
189
|
|
|
|
|
|
|
# if you do not want B2 to auto-determine the MIME/content-type |
|
190
|
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
# did they provide a file location or path? |
|
192
|
0
|
0
|
0
|
|
|
0
|
if ($args{file_location} && -e "$args{file_location}") { |
|
193
|
0
|
|
|
|
|
0
|
$args{file_contents} = path( $args{file_location} )->slurp_raw; |
|
194
|
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
# if they didn't provide a file-name, use the one on this file |
|
196
|
0
|
|
|
|
|
0
|
$args{new_file_name} = path( $args{file_location} )->basename; |
|
197
|
|
|
|
|
|
|
} |
|
198
|
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
# were these file contents either provided or found? |
|
200
|
0
|
0
|
|
|
|
0
|
if (!length($args{file_contents})) { |
|
201
|
0
|
|
|
|
|
0
|
$self->error_tracker(qq{You must provide either a valid 'file_location' or 'file_contents' arg for b2_upload_file().}); |
|
202
|
0
|
|
|
|
|
0
|
return 'Error'; |
|
203
|
|
|
|
|
|
|
} |
|
204
|
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
# check the other needed args |
|
206
|
0
|
0
|
0
|
|
|
0
|
if (!$args{bucket_name} || !$args{new_file_name}) { |
|
207
|
0
|
|
|
|
|
0
|
$self->error_tracker(qq{You must provide 'bucket_name' and 'new_file_name' args for b2_upload_file().}); |
|
208
|
0
|
|
|
|
|
0
|
return 'Error'; |
|
209
|
|
|
|
|
|
|
} |
|
210
|
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
# default content-type |
|
212
|
0
|
|
0
|
|
|
0
|
$args{content_type} ||= 'b2/x-auto'; |
|
213
|
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
# OK, let's continue: get the upload URL and authorization token for this bucket |
|
215
|
0
|
|
|
|
|
0
|
$self->b2_get_upload_url( $args{bucket_name} ); |
|
216
|
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
# send the special request |
|
218
|
|
|
|
|
|
|
$self->b2_talker( |
|
219
|
|
|
|
|
|
|
'url' => $self->{bucket_info}{ $args{bucket_name} }{upload_url}, |
|
220
|
|
|
|
|
|
|
'authorization' => $self->{bucket_info}{ $args{bucket_name} }{authorization_token}, |
|
221
|
|
|
|
|
|
|
'file_contents' => $args{file_contents}, |
|
222
|
|
|
|
|
|
|
'special_headers' => { |
|
223
|
|
|
|
|
|
|
'X-Bz-File-Name' => uri_escape( $args{new_file_name} ), |
|
224
|
|
|
|
|
|
|
'X-Bz-Content-Sha1' => sha1_hex( $args{file_contents} ), |
|
225
|
|
|
|
|
|
|
'Content-Type' => $args{content_type}, |
|
226
|
|
|
|
|
|
|
}, |
|
227
|
0
|
|
|
|
|
0
|
); |
|
228
|
|
|
|
|
|
|
# b2_talker will handle the rest |
|
229
|
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
# return current status |
|
231
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
232
|
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
} |
|
234
|
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
# method to get the information needed to upload into a specific B2 bucket |
|
236
|
|
|
|
|
|
|
sub b2_get_upload_url { |
|
237
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
238
|
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
# the bucket name is required |
|
240
|
0
|
|
|
|
|
0
|
my ($bucket_name) = @_; |
|
241
|
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
# bucket_name is required |
|
243
|
0
|
0
|
|
|
|
0
|
if (!$bucket_name) { |
|
244
|
0
|
|
|
|
|
0
|
$self->error_tracker('The bucket_name must be provided for b2_get_upload_url().'); |
|
245
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
246
|
|
|
|
|
|
|
} |
|
247
|
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
# no need to proceed if we already have done for this bucket this during this session |
|
249
|
|
|
|
|
|
|
# return if $self->{bucket_info}{$bucket_name}{upload_url}; |
|
250
|
|
|
|
|
|
|
# COMMENTED OUT: It seems like B2 wants a new upload_url endpoint for each upload, |
|
251
|
|
|
|
|
|
|
# and we may want to upload multiple files into each bucket...so this won't work |
|
252
|
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
# if we don't have the info for the bucket name, retrieve the bucket's ID |
|
254
|
0
|
0
|
|
|
|
0
|
if (ref($self->{buckets}{$bucket_name}) ne 'HASH') { |
|
255
|
0
|
|
|
|
|
0
|
$self->b2_list_buckets($bucket_name); |
|
256
|
|
|
|
|
|
|
} |
|
257
|
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
# send the request |
|
259
|
|
|
|
|
|
|
$self->b2_talker( |
|
260
|
|
|
|
|
|
|
'url' => $self->{api_url}.'/b2api/v2/b2_get_upload_url', |
|
261
|
|
|
|
|
|
|
'authorization' => $self->{account_authorization_token}, |
|
262
|
|
|
|
|
|
|
'post_params' => { |
|
263
|
|
|
|
|
|
|
'bucketId' => $self->{buckets}{$bucket_name}{bucket_id}, |
|
264
|
|
|
|
|
|
|
}, |
|
265
|
0
|
|
|
|
|
0
|
); |
|
266
|
|
|
|
|
|
|
|
|
267
|
|
|
|
|
|
|
# if we succeeded, get the info for this bucket |
|
268
|
0
|
0
|
|
|
|
0
|
if ($self->{current_status} eq 'OK') { |
|
269
|
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
$self->{bucket_info}{$bucket_name} = { |
|
271
|
|
|
|
|
|
|
'upload_url' => $self->{b2_response}{uploadUrl}, |
|
272
|
|
|
|
|
|
|
'authorization_token' => $self->{b2_response}{authorizationToken}, |
|
273
|
0
|
|
|
|
|
0
|
}; |
|
274
|
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
} |
|
276
|
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
# send the status for consistency |
|
278
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
279
|
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
} |
|
281
|
|
|
|
|
|
|
|
|
282
|
|
|
|
|
|
|
# method to get information on one bucket or all buckets |
|
283
|
|
|
|
|
|
|
# specify the bucket-name to search by name |
|
284
|
|
|
|
|
|
|
sub b2_list_buckets { |
|
285
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
286
|
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
# optional first arg is a target bucket name |
|
288
|
|
|
|
|
|
|
# optional second arg tells us to auto-create a bucket, if the name is provided but it was not found |
|
289
|
0
|
|
|
|
|
0
|
my ($bucket_name, $auto_create_bucket) = @_; |
|
290
|
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
# send the request |
|
292
|
|
|
|
|
|
|
$self->b2_talker( |
|
293
|
|
|
|
|
|
|
'url' => $self->{api_url}.'/b2api/v2/b2_list_buckets', |
|
294
|
|
|
|
|
|
|
'authorization' => $self->{account_authorization_token}, |
|
295
|
|
|
|
|
|
|
'post_params' => { |
|
296
|
|
|
|
|
|
|
'accountId' => $self->{account_id}, |
|
297
|
0
|
|
|
|
|
0
|
'bucketName' => $bucket_name, |
|
298
|
|
|
|
|
|
|
}, |
|
299
|
|
|
|
|
|
|
); |
|
300
|
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
# if we succeeded, load in all the found buckets to $self->{buckets} |
|
302
|
|
|
|
|
|
|
# that will be a hash of info, keyed by name |
|
303
|
|
|
|
|
|
|
|
|
304
|
0
|
0
|
|
|
|
0
|
if ($self->{current_status} eq 'OK') { |
|
305
|
0
|
|
|
|
|
0
|
foreach my $bucket_info (@{ $self->{b2_response}{buckets} }) { |
|
|
0
|
|
|
|
|
0
|
|
|
306
|
0
|
|
|
|
|
0
|
$bucket_name = $$bucket_info{bucketName}; |
|
307
|
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
$self->{buckets}{$bucket_name} = { |
|
309
|
|
|
|
|
|
|
'bucket_id' => $$bucket_info{bucketId}, |
|
310
|
|
|
|
|
|
|
'bucket_type' => $$bucket_info{bucketType}, |
|
311
|
0
|
|
|
|
|
0
|
}; |
|
312
|
|
|
|
|
|
|
} |
|
313
|
|
|
|
|
|
|
} else { |
|
314
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
315
|
|
|
|
|
|
|
} |
|
316
|
|
|
|
|
|
|
|
|
317
|
|
|
|
|
|
|
# if that bucket was not found, maybe they want to go ahead and create it? |
|
318
|
0
|
0
|
0
|
|
|
0
|
if ($bucket_name && !$self->{buckets}{$bucket_name} && $auto_create_bucket) { |
|
|
|
|
0
|
|
|
|
|
|
319
|
0
|
|
|
|
|
0
|
$self->b2_bucket_maker($bucket_name); |
|
320
|
|
|
|
|
|
|
# this will call back to me and get the info |
|
321
|
|
|
|
|
|
|
} |
|
322
|
|
|
|
|
|
|
|
|
323
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
324
|
|
|
|
|
|
|
|
|
325
|
|
|
|
|
|
|
} |
|
326
|
|
|
|
|
|
|
|
|
327
|
|
|
|
|
|
|
# method to retrieve file names / info from a bucket |
|
328
|
|
|
|
|
|
|
# this client library is bucket-name-centric, so it looks for the bucket name as a arg |
|
329
|
|
|
|
|
|
|
# if there are more than 1000 files, then call this repeatedly |
|
330
|
|
|
|
|
|
|
our $B2_MAX_FILE_COUNT = 1000; |
|
331
|
|
|
|
|
|
|
sub b2_list_file_names { |
|
332
|
0
|
|
|
0
|
1
|
0
|
my ($self, $bucket_name, %args) = @_; |
|
333
|
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
# bucket_name is required |
|
335
|
0
|
0
|
|
|
|
0
|
if (!$bucket_name) { |
|
336
|
0
|
|
|
|
|
0
|
$self->error_tracker('The bucket_name must be provided for b2_list_file_names().'); |
|
337
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
338
|
|
|
|
|
|
|
} |
|
339
|
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
# we need the bucket ID |
|
341
|
|
|
|
|
|
|
# if we don't have the info for the bucket name, retrieve the bucket's ID |
|
342
|
0
|
0
|
|
|
|
0
|
if (ref($self->{buckets}{$bucket_name}) ne 'HASH') { |
|
343
|
0
|
|
|
|
|
0
|
$self->b2_list_buckets($bucket_name); |
|
344
|
|
|
|
|
|
|
} |
|
345
|
|
|
|
|
|
|
|
|
346
|
|
|
|
|
|
|
# retrieve the files |
|
347
|
|
|
|
|
|
|
$self->b2_talker( |
|
348
|
|
|
|
|
|
|
'url' => $self->{api_url}.'/b2api/v2/b2_list_file_names', |
|
349
|
|
|
|
|
|
|
'authorization' => $self->{account_authorization_token}, |
|
350
|
|
|
|
|
|
|
'post_params' => { |
|
351
|
|
|
|
|
|
|
'bucketId' => $self->{buckets}{$bucket_name}{bucket_id}, |
|
352
|
|
|
|
|
|
|
'prefix' => $args{prefix} // undef, |
|
353
|
|
|
|
|
|
|
'delimiter' => $args{delimiter} // undef, |
|
354
|
|
|
|
|
|
|
'startFileName' => $args{startFileName} // $self->{buckets}{$bucket_name}{next_file_name}, |
|
355
|
0
|
|
0
|
|
|
0
|
'maxFileCount' => $args{maxFileCount} // $B2_MAX_FILE_COUNT, |
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
356
|
|
|
|
|
|
|
}, |
|
357
|
|
|
|
|
|
|
); |
|
358
|
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
# if we succeeded, read in the files |
|
360
|
0
|
0
|
|
|
|
0
|
if ($self->{current_status} eq 'OK') { |
|
361
|
0
|
|
|
|
|
0
|
$self->{buckets}{$bucket_name}{next_file_name} = $self->{b2_response}{nextFileName}; |
|
362
|
|
|
|
|
|
|
|
|
363
|
|
|
|
|
|
|
# i am not going to waste the CPU cycles de-camelizing these sub-keys |
|
364
|
|
|
|
|
|
|
# add to our possibly-started array of file info for this bucket |
|
365
|
|
|
|
|
|
|
push( |
|
366
|
0
|
|
|
|
|
0
|
@{ $self->{buckets}{$bucket_name}{files} }, |
|
367
|
0
|
|
|
|
|
0
|
@{ $self->{b2_response}{files} } |
|
|
0
|
|
|
|
|
0
|
|
|
368
|
|
|
|
|
|
|
); |
|
369
|
|
|
|
|
|
|
|
|
370
|
|
|
|
|
|
|
# kindly return the request results as a refernce (arrayref) |
|
371
|
0
|
|
|
|
|
0
|
return $self->{b2_response}{files}; |
|
372
|
|
|
|
|
|
|
|
|
373
|
|
|
|
|
|
|
# otherwise, return an error |
|
374
|
|
|
|
|
|
|
} else { |
|
375
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
376
|
|
|
|
|
|
|
} |
|
377
|
|
|
|
|
|
|
|
|
378
|
|
|
|
|
|
|
|
|
379
|
|
|
|
|
|
|
} |
|
380
|
|
|
|
|
|
|
|
|
381
|
|
|
|
|
|
|
# method to get info for a specific file |
|
382
|
|
|
|
|
|
|
# I assume you have the File ID for the file |
|
383
|
|
|
|
|
|
|
sub b2_get_file_info { |
|
384
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
385
|
|
|
|
|
|
|
|
|
386
|
|
|
|
|
|
|
# required arg is the file ID |
|
387
|
0
|
|
|
|
|
0
|
my ($file_id) = @_; |
|
388
|
|
|
|
|
|
|
|
|
389
|
0
|
0
|
|
|
|
0
|
if (!$file_id) { |
|
390
|
0
|
|
|
|
|
0
|
$self->error_tracker('The file_id must be provided for b2_get_file_info().'); |
|
391
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
392
|
|
|
|
|
|
|
} |
|
393
|
|
|
|
|
|
|
|
|
394
|
|
|
|
|
|
|
# kick out if we already have it |
|
395
|
0
|
0
|
|
|
|
0
|
return 'Error' if ref($self->{file_info}{$file_id}) eq 'HASH'; |
|
396
|
|
|
|
|
|
|
|
|
397
|
|
|
|
|
|
|
# retrieve the file information |
|
398
|
|
|
|
|
|
|
$self->b2_talker( |
|
399
|
|
|
|
|
|
|
'url' => $self->{api_url}.'/b2api/v2/b2_get_file_info', |
|
400
|
|
|
|
|
|
|
'authorization' => $self->{account_authorization_token}, |
|
401
|
0
|
|
|
|
|
0
|
'post_params' => { |
|
402
|
|
|
|
|
|
|
'fileId' => $file_id, |
|
403
|
|
|
|
|
|
|
}, |
|
404
|
|
|
|
|
|
|
); |
|
405
|
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
# if we succeeded, read in the information |
|
407
|
0
|
0
|
|
|
|
0
|
if ($self->{current_status} eq 'OK') { |
|
408
|
|
|
|
|
|
|
# i am not going to waste the CPU cycles de-camelizing these sub-keys |
|
409
|
0
|
|
|
|
|
0
|
$self->{file_info}{$file_id} = $self->{b2_response}; |
|
410
|
|
|
|
|
|
|
} |
|
411
|
|
|
|
|
|
|
|
|
412
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
413
|
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
} |
|
415
|
|
|
|
|
|
|
|
|
416
|
|
|
|
|
|
|
# combo method to create a bucket |
|
417
|
|
|
|
|
|
|
sub b2_bucket_maker { |
|
418
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
419
|
|
|
|
|
|
|
|
|
420
|
0
|
|
|
|
|
0
|
my ($bucket_name, $disable_encryption) = @_; |
|
421
|
|
|
|
|
|
|
|
|
422
|
|
|
|
|
|
|
# can't proceed without the bucket_name |
|
423
|
0
|
0
|
|
|
|
0
|
if (!$bucket_name) { |
|
424
|
0
|
|
|
|
|
0
|
$self->error_tracker('The bucket_name must be provided for b2_bucket_maker().'); |
|
425
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
426
|
|
|
|
|
|
|
} |
|
427
|
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
# prepare the basics for our request |
|
429
|
|
|
|
|
|
|
my $post_params = { |
|
430
|
|
|
|
|
|
|
'accountId' => $self->{account_id}, |
|
431
|
0
|
|
|
|
|
0
|
'bucketName' => $bucket_name, |
|
432
|
|
|
|
|
|
|
'bucketType' => 'allPrivate', |
|
433
|
|
|
|
|
|
|
}; |
|
434
|
|
|
|
|
|
|
|
|
435
|
|
|
|
|
|
|
# unless instructed otherwise, we should encrypt the files in this bucket |
|
436
|
0
|
0
|
|
|
|
0
|
unless ($disable_encryption) { |
|
437
|
|
|
|
|
|
|
$$post_params{defaultServerSideEncryption} = { |
|
438
|
0
|
|
|
|
|
0
|
'mode' => 'SSE-B2', |
|
439
|
|
|
|
|
|
|
'algorithm' => 'AES256', |
|
440
|
|
|
|
|
|
|
}; |
|
441
|
|
|
|
|
|
|
} |
|
442
|
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
# create the bucket... |
|
444
|
|
|
|
|
|
|
$self->b2_talker( |
|
445
|
|
|
|
|
|
|
'url' => $self->{api_url}.'/b2api/v2/b2_create_bucket', |
|
446
|
|
|
|
|
|
|
'authorization' => $self->{account_authorization_token}, |
|
447
|
0
|
|
|
|
|
0
|
'post_params' => $post_params, |
|
448
|
|
|
|
|
|
|
); |
|
449
|
|
|
|
|
|
|
|
|
450
|
0
|
0
|
|
|
|
0
|
if ($self->{current_status} eq 'OK') { # if successful... |
|
451
|
|
|
|
|
|
|
|
|
452
|
|
|
|
|
|
|
# stash our new bucket into $self->{buckets} |
|
453
|
|
|
|
|
|
|
$self->{buckets}{$bucket_name} = { |
|
454
|
|
|
|
|
|
|
'bucket_id' => $self->{b2_response}{bucketId}, |
|
455
|
0
|
|
|
|
|
0
|
'bucket_type' => 'allPrivate', |
|
456
|
|
|
|
|
|
|
}; |
|
457
|
|
|
|
|
|
|
|
|
458
|
|
|
|
|
|
|
} |
|
459
|
|
|
|
|
|
|
|
|
460
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
461
|
|
|
|
|
|
|
|
|
462
|
|
|
|
|
|
|
} |
|
463
|
|
|
|
|
|
|
|
|
464
|
|
|
|
|
|
|
# method to delete a bucket -- please don't use ;) |
|
465
|
|
|
|
|
|
|
sub b2_delete_bucket { |
|
466
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
467
|
|
|
|
|
|
|
|
|
468
|
0
|
|
|
|
|
0
|
my ($bucket_name) = @_; |
|
469
|
|
|
|
|
|
|
|
|
470
|
|
|
|
|
|
|
# bucket_id is required |
|
471
|
0
|
0
|
|
|
|
0
|
if (!$bucket_name) { |
|
472
|
0
|
|
|
|
|
0
|
$self->error_tracker('The bucket_name must be provided for b2_delete_bucket().'); |
|
473
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
474
|
|
|
|
|
|
|
} |
|
475
|
|
|
|
|
|
|
|
|
476
|
|
|
|
|
|
|
# resolve that bucket_name to a bucket_id |
|
477
|
0
|
|
|
|
|
0
|
$self->b2_list_buckets($bucket_name); |
|
478
|
|
|
|
|
|
|
|
|
479
|
|
|
|
|
|
|
# send the request |
|
480
|
|
|
|
|
|
|
$self->b2_talker( |
|
481
|
|
|
|
|
|
|
'url' => $self->{api_url}.'/b2api/v2/b2_delete_bucket', |
|
482
|
|
|
|
|
|
|
'authorization' => $self->{account_authorization_token}, |
|
483
|
|
|
|
|
|
|
'post_params' => { |
|
484
|
|
|
|
|
|
|
'accountId' => $self->{account_id}, |
|
485
|
|
|
|
|
|
|
'bucketId' => $self->{buckets}{$bucket_name}{bucket_id}, |
|
486
|
|
|
|
|
|
|
}, |
|
487
|
0
|
|
|
|
|
0
|
); |
|
488
|
|
|
|
|
|
|
|
|
489
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
490
|
|
|
|
|
|
|
} |
|
491
|
|
|
|
|
|
|
|
|
492
|
|
|
|
|
|
|
# method to delete a stored file object. B2 thinks of these as 'versions,' |
|
493
|
|
|
|
|
|
|
# but if you use unique names, one version = one file |
|
494
|
|
|
|
|
|
|
sub b2_delete_file_version { |
|
495
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
496
|
|
|
|
|
|
|
|
|
497
|
|
|
|
|
|
|
# required arguments are the file_name and file_id for the target file |
|
498
|
0
|
|
|
|
|
0
|
my ($file_name, $file_id) = @_; |
|
499
|
|
|
|
|
|
|
|
|
500
|
|
|
|
|
|
|
# bucket_id is required |
|
501
|
0
|
0
|
0
|
|
|
0
|
if (!$file_name || !$file_id) { |
|
502
|
0
|
|
|
|
|
0
|
$self->error_tracker('The file_name and file_id args must be provided for b2_delete_file_version().'); |
|
503
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
504
|
|
|
|
|
|
|
} |
|
505
|
|
|
|
|
|
|
|
|
506
|
|
|
|
|
|
|
# send the request |
|
507
|
|
|
|
|
|
|
$self->b2_talker( |
|
508
|
|
|
|
|
|
|
'url' => $self->{api_url}.'/b2api/v2/b2_delete_file_version', |
|
509
|
|
|
|
|
|
|
'authorization' => $self->{account_authorization_token}, |
|
510
|
0
|
|
|
|
|
0
|
'post_params' => { |
|
511
|
|
|
|
|
|
|
'fileName' => $file_name, |
|
512
|
|
|
|
|
|
|
'fileId' => $file_id, |
|
513
|
|
|
|
|
|
|
}, |
|
514
|
|
|
|
|
|
|
); |
|
515
|
|
|
|
|
|
|
|
|
516
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
517
|
|
|
|
|
|
|
|
|
518
|
|
|
|
|
|
|
} |
|
519
|
|
|
|
|
|
|
|
|
520
|
|
|
|
|
|
|
# method to upload a large file (>100MB) |
|
521
|
|
|
|
|
|
|
sub b2_upload_large_file { |
|
522
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
523
|
0
|
|
|
|
|
0
|
my (%args) = @_; |
|
524
|
|
|
|
|
|
|
# this must include valid entries for 'new_file_name' and 'bucket_name' |
|
525
|
|
|
|
|
|
|
# and it has to a valid location in 'file_location' (Do not load in file contents) |
|
526
|
|
|
|
|
|
|
# also, you can include 'content_type' (which would be the MIME Type' |
|
527
|
|
|
|
|
|
|
# if you do not want B2 to auto-determine the MIME/content-type |
|
528
|
|
|
|
|
|
|
|
|
529
|
|
|
|
|
|
|
# did they provide a file location or path? |
|
530
|
0
|
0
|
0
|
|
|
0
|
if ($args{file_location} && -e "$args{file_location}") { |
|
531
|
|
|
|
|
|
|
# if they didn't provide a file-name, use the one on this file |
|
532
|
0
|
|
|
|
|
0
|
$args{new_file_name} = path( $args{file_location} )->basename; |
|
533
|
|
|
|
|
|
|
} else { |
|
534
|
0
|
|
|
|
|
0
|
$self->error_tracker(qq{You must provide a valid 'file_location' arg for b2_upload_large_file().}); |
|
535
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
536
|
|
|
|
|
|
|
} |
|
537
|
|
|
|
|
|
|
|
|
538
|
|
|
|
|
|
|
# protect my sanity... |
|
539
|
0
|
|
|
|
|
0
|
my ($bucket_name, $file_contents_part, $file_location, $large_file_id, $part_number, $remaining_file_size, $sha1_array, $size_sent, $stat); |
|
540
|
0
|
|
|
|
|
0
|
$file_location = $args{file_location}; |
|
541
|
0
|
|
|
|
|
0
|
$bucket_name = $args{bucket_name}; |
|
542
|
|
|
|
|
|
|
|
|
543
|
|
|
|
|
|
|
# must be 100MB or bigger |
|
544
|
0
|
|
|
|
|
0
|
$stat = path($file_location)->stat; |
|
545
|
0
|
0
|
|
|
|
0
|
if ($stat->size < $self->{recommended_part_size} ) { |
|
546
|
0
|
|
|
|
|
0
|
$self->error_tracker(qq{Please use b2_upload_large_file() for files larger than $self->{recommended_part_size} .}); |
|
547
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
548
|
|
|
|
|
|
|
} |
|
549
|
|
|
|
|
|
|
|
|
550
|
|
|
|
|
|
|
# need a bucket name |
|
551
|
0
|
0
|
|
|
|
0
|
if (!$bucket_name) { |
|
552
|
0
|
|
|
|
|
0
|
$self->error_tracker(qq{You must provide a valid 'bucket_name' arg for b2_upload_large_file().}); |
|
553
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
554
|
|
|
|
|
|
|
} |
|
555
|
|
|
|
|
|
|
|
|
556
|
|
|
|
|
|
|
# default content-type |
|
557
|
0
|
|
0
|
|
|
0
|
$args{content_type} ||= 'b2/x-auto'; |
|
558
|
|
|
|
|
|
|
|
|
559
|
|
|
|
|
|
|
# get the bucket ID |
|
560
|
0
|
|
|
|
|
0
|
$self->b2_list_buckets($bucket_name); |
|
561
|
|
|
|
|
|
|
|
|
562
|
|
|
|
|
|
|
# kick off the upload in the API |
|
563
|
|
|
|
|
|
|
$self->b2_talker( |
|
564
|
|
|
|
|
|
|
'url' => $self->{api_url}.'/b2api/v2/b2_start_large_file', |
|
565
|
|
|
|
|
|
|
'authorization' => $self->{account_authorization_token}, |
|
566
|
|
|
|
|
|
|
'post_params' => { |
|
567
|
|
|
|
|
|
|
'bucketId' => $self->{buckets}{$bucket_name}{bucket_id}, |
|
568
|
|
|
|
|
|
|
'fileName' => $args{new_file_name}, |
|
569
|
|
|
|
|
|
|
'contentType' => $args{content_type}, |
|
570
|
|
|
|
|
|
|
}, |
|
571
|
0
|
|
|
|
|
0
|
); |
|
572
|
|
|
|
|
|
|
|
|
573
|
|
|
|
|
|
|
# these are all needed for each b2_upload_part web call |
|
574
|
0
|
|
|
|
|
0
|
$large_file_id = $self->{b2_response}{fileId}; |
|
575
|
0
|
0
|
|
|
|
0
|
return 'Error' if !$large_file_id; # there was an error in the request |
|
576
|
|
|
|
|
|
|
|
|
577
|
|
|
|
|
|
|
# open the large file |
|
578
|
0
|
|
|
|
|
0
|
open(FH, $file_location); |
|
579
|
|
|
|
|
|
|
|
|
580
|
0
|
|
|
|
|
0
|
$remaining_file_size = $stat->size; |
|
581
|
|
|
|
|
|
|
|
|
582
|
0
|
|
|
|
|
0
|
$part_number = 1; |
|
583
|
|
|
|
|
|
|
|
|
584
|
|
|
|
|
|
|
# cycle thru each chunk of the file |
|
585
|
0
|
|
|
|
|
0
|
while ($remaining_file_size >= 0) { |
|
586
|
|
|
|
|
|
|
# how much to send? |
|
587
|
0
|
0
|
|
|
|
0
|
if ($remaining_file_size < $self->{recommended_part_size} ) { |
|
588
|
0
|
|
|
|
|
0
|
$size_sent = $remaining_file_size; |
|
589
|
|
|
|
|
|
|
} else { |
|
590
|
0
|
|
|
|
|
0
|
$size_sent = $self->{recommended_part_size} ; |
|
591
|
|
|
|
|
|
|
} |
|
592
|
|
|
|
|
|
|
|
|
593
|
|
|
|
|
|
|
# get the next upload url for this part |
|
594
|
|
|
|
|
|
|
$self->b2_talker( |
|
595
|
|
|
|
|
|
|
'url' => $self->{api_url}.'/b2api/v2/b2_get_upload_part_url', |
|
596
|
|
|
|
|
|
|
'authorization' => $self->{account_authorization_token}, |
|
597
|
0
|
|
|
|
|
0
|
'post_params' => { |
|
598
|
|
|
|
|
|
|
'fileId' => $large_file_id, |
|
599
|
|
|
|
|
|
|
}, |
|
600
|
|
|
|
|
|
|
); |
|
601
|
|
|
|
|
|
|
|
|
602
|
|
|
|
|
|
|
# read in that section of the file and prep the SHA |
|
603
|
0
|
|
|
|
|
0
|
sysread FH, $file_contents_part, $size_sent; |
|
604
|
0
|
|
|
|
|
0
|
push(@$sha1_array,sha1_hex( $file_contents_part )); |
|
605
|
|
|
|
|
|
|
|
|
606
|
|
|
|
|
|
|
# upload that part |
|
607
|
|
|
|
|
|
|
$self->b2_talker( |
|
608
|
|
|
|
|
|
|
'url' => $self->{b2_response}{uploadUrl}, |
|
609
|
|
|
|
|
|
|
'authorization' => $self->{b2_response}{authorizationToken}, |
|
610
|
0
|
|
|
|
|
0
|
'special_headers' => { |
|
611
|
|
|
|
|
|
|
'X-Bz-Content-Sha1' => $$sha1_array[-1], |
|
612
|
|
|
|
|
|
|
'X-Bz-Part-Number' => $part_number, |
|
613
|
|
|
|
|
|
|
'Content-Length' => $size_sent, |
|
614
|
|
|
|
|
|
|
}, |
|
615
|
|
|
|
|
|
|
'file_contents' => $file_contents_part, |
|
616
|
|
|
|
|
|
|
); |
|
617
|
|
|
|
|
|
|
|
|
618
|
|
|
|
|
|
|
# advance |
|
619
|
0
|
|
|
|
|
0
|
$part_number++; |
|
620
|
0
|
|
|
|
|
0
|
$remaining_file_size -= $self->{recommended_part_size} ; |
|
621
|
|
|
|
|
|
|
} |
|
622
|
|
|
|
|
|
|
|
|
623
|
|
|
|
|
|
|
# close the file |
|
624
|
0
|
|
|
|
|
0
|
close FH; |
|
625
|
|
|
|
|
|
|
|
|
626
|
|
|
|
|
|
|
# and tell B2 |
|
627
|
|
|
|
|
|
|
$self->b2_talker( |
|
628
|
|
|
|
|
|
|
'url' => $self->{api_url}.'/b2api/v2/b2_finish_large_file', |
|
629
|
|
|
|
|
|
|
'authorization' => $self->{account_authorization_token}, |
|
630
|
0
|
|
|
|
|
0
|
'post_params' => { |
|
631
|
|
|
|
|
|
|
'fileId' => $large_file_id, |
|
632
|
|
|
|
|
|
|
'partSha1Array' => $sha1_array, |
|
633
|
|
|
|
|
|
|
}, |
|
634
|
|
|
|
|
|
|
); |
|
635
|
|
|
|
|
|
|
|
|
636
|
|
|
|
|
|
|
# phew, i'm tired... |
|
637
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
638
|
|
|
|
|
|
|
} |
|
639
|
|
|
|
|
|
|
|
|
640
|
|
|
|
|
|
|
|
|
641
|
|
|
|
|
|
|
# generic method to handle communication to B2 |
|
642
|
|
|
|
|
|
|
sub b2_talker { |
|
643
|
2
|
|
|
2
|
1
|
6
|
my $self = shift; |
|
644
|
|
|
|
|
|
|
|
|
645
|
|
|
|
|
|
|
# args hash must include 'url' for the target API endpoint URL |
|
646
|
|
|
|
|
|
|
# most other requests will also include a 'post_params' hashref, and 'authorization' value for the header |
|
647
|
|
|
|
|
|
|
# for the b2_upload_file function, there will be several other headers + a file_contents arg |
|
648
|
2
|
|
|
|
|
9
|
my (%args) = @_; |
|
649
|
|
|
|
|
|
|
|
|
650
|
2
|
50
|
|
|
|
9
|
if (!$args{url}) { |
|
651
|
0
|
|
|
|
|
0
|
$self->error_tracker('Can not use b2_talker() without an endpoint URL.'); |
|
652
|
|
|
|
|
|
|
} |
|
653
|
|
|
|
|
|
|
|
|
654
|
|
|
|
|
|
|
# if they sent an Authorization header, set that value |
|
655
|
2
|
100
|
|
|
|
9
|
if ($args{authorization}) { |
|
656
|
1
|
|
|
|
|
7
|
$self->{mech}->delete_header( 'Authorization' ); |
|
657
|
1
|
|
|
|
|
17
|
$self->{mech}->add_header( 'Authorization' => $args{authorization} ); |
|
658
|
|
|
|
|
|
|
} |
|
659
|
|
|
|
|
|
|
|
|
660
|
2
|
|
|
|
|
17
|
my ($response, $response_code, $error_message, $header, @header_keys); |
|
661
|
|
|
|
|
|
|
|
|
662
|
|
|
|
|
|
|
# short-circuit if we had difficulty logging in previously |
|
663
|
2
|
50
|
|
|
|
8
|
if ($self->{b2_login_error}) { |
|
664
|
|
|
|
|
|
|
|
|
665
|
|
|
|
|
|
|
# track the error / set current state |
|
666
|
0
|
|
|
|
|
0
|
$self->error_tracker("Problem logging into Backblaze. Please check the 'errors' array in this object.", $args{url}); |
|
667
|
|
|
|
|
|
|
|
|
668
|
0
|
|
|
|
|
0
|
return $self->{current_status}; |
|
669
|
|
|
|
|
|
|
} |
|
670
|
|
|
|
|
|
|
|
|
671
|
|
|
|
|
|
|
# are we uploading a file? |
|
672
|
2
|
50
|
|
|
|
14
|
if ($args{url} =~ /b2_upload_file|b2_upload_part/) { |
|
|
|
50
|
|
|
|
|
|
|
673
|
|
|
|
|
|
|
|
|
674
|
|
|
|
|
|
|
# add the special headers |
|
675
|
0
|
|
|
|
|
0
|
@header_keys = keys %{ $args{special_headers} }; |
|
|
0
|
|
|
|
|
0
|
|
|
676
|
0
|
|
|
|
|
0
|
foreach $header (@header_keys) { |
|
677
|
0
|
|
|
|
|
0
|
$self->{mech}->delete_header( $header ); |
|
678
|
0
|
|
|
|
|
0
|
$self->{mech}->add_header( $header => $args{special_headers}{$header} ); |
|
679
|
|
|
|
|
|
|
} |
|
680
|
|
|
|
|
|
|
|
|
681
|
|
|
|
|
|
|
# now upload the file |
|
682
|
0
|
|
|
|
|
0
|
eval { |
|
683
|
0
|
|
|
|
|
0
|
$response = $self->{mech}->post( $args{url}, content => $args{file_contents} ); |
|
684
|
|
|
|
|
|
|
|
|
685
|
|
|
|
|
|
|
# we want this to be 200 |
|
686
|
0
|
|
|
|
|
0
|
$response_code = $response->{_rc}; |
|
687
|
|
|
|
|
|
|
|
|
688
|
0
|
|
|
|
|
0
|
$self->{b2_response} = decode_json( $self->{mech}->content() ); |
|
689
|
|
|
|
|
|
|
|
|
690
|
|
|
|
|
|
|
}; |
|
691
|
|
|
|
|
|
|
|
|
692
|
|
|
|
|
|
|
# remove those special headers, cleaned-up for next time |
|
693
|
0
|
|
|
|
|
0
|
foreach $header (@header_keys) { |
|
694
|
0
|
|
|
|
|
0
|
$self->{mech}->delete_header( $header ); |
|
695
|
|
|
|
|
|
|
} |
|
696
|
|
|
|
|
|
|
|
|
697
|
|
|
|
|
|
|
# if not uploading and they sent POST params, we are doing a POST |
|
698
|
|
|
|
|
|
|
} elsif (ref($args{post_params}) eq 'HASH') { |
|
699
|
0
|
|
|
|
|
0
|
eval { |
|
700
|
|
|
|
|
|
|
# send the POST |
|
701
|
0
|
|
|
|
|
0
|
$response = $self->{mech}->post( $args{url}, content => encode_json($args{post_params}) ); |
|
702
|
|
|
|
|
|
|
|
|
703
|
|
|
|
|
|
|
# we want this to be 200 |
|
704
|
0
|
|
|
|
|
0
|
$response_code = $response->code; |
|
705
|
|
|
|
|
|
|
|
|
706
|
|
|
|
|
|
|
# decode results |
|
707
|
0
|
|
|
|
|
0
|
$self->{b2_response} = decode_json( $self->{mech}->content() ); |
|
708
|
|
|
|
|
|
|
}; |
|
709
|
|
|
|
|
|
|
|
|
710
|
|
|
|
|
|
|
# otherwise, we are doing a GET |
|
711
|
|
|
|
|
|
|
} else { |
|
712
|
|
|
|
|
|
|
|
|
713
|
|
|
|
|
|
|
# attempt the GET |
|
714
|
2
|
|
|
|
|
4
|
eval { |
|
715
|
2
|
|
|
|
|
8
|
$response = $self->{mech}->get( $args{url} ); |
|
716
|
|
|
|
|
|
|
|
|
717
|
|
|
|
|
|
|
# we want this to be 200 |
|
718
|
2
|
|
|
|
|
1778336
|
$response_code = $response->code; |
|
719
|
|
|
|
|
|
|
|
|
720
|
|
|
|
|
|
|
# did we download a file? |
|
721
|
2
|
100
|
|
|
|
80
|
if ($response->header( 'X-Bz-File-Name' )) { |
|
|
|
50
|
|
|
|
|
|
|
722
|
|
|
|
|
|
|
|
|
723
|
|
|
|
|
|
|
# grab those needed headers |
|
724
|
1
|
|
|
|
|
51
|
foreach $header ('Content-Length','Content-Type','X-Bz-File-Id','X-Bz-File-Name','X-Bz-Content-Sha1') { |
|
725
|
5
|
|
|
|
|
182
|
$self->{b2_response}{$header} = $response->header( $header ); |
|
726
|
|
|
|
|
|
|
} |
|
727
|
|
|
|
|
|
|
|
|
728
|
|
|
|
|
|
|
# and the file itself |
|
729
|
1
|
|
|
|
|
45
|
$self->{b2_response}{file_contents} = $self->{mech}->content(); |
|
730
|
|
|
|
|
|
|
|
|
731
|
|
|
|
|
|
|
} elsif ($response_code eq '200') { # no, regular JSON, decode results |
|
732
|
1
|
|
|
|
|
137
|
$self->{b2_response} = decode_json( $self->{mech}->content() ); |
|
733
|
|
|
|
|
|
|
} |
|
734
|
|
|
|
|
|
|
}; |
|
735
|
|
|
|
|
|
|
} |
|
736
|
|
|
|
|
|
|
|
|
737
|
|
|
|
|
|
|
# there is a problem if there is a problem |
|
738
|
2
|
50
|
33
|
|
|
118
|
if ($@ || $response_code ne '200') { |
|
739
|
0
|
0
|
|
|
|
0
|
if ($self->{b2_response}{message}) { |
|
740
|
0
|
|
|
|
|
0
|
$error_message = 'API Message: '.$self->{b2_response}{message}; |
|
741
|
|
|
|
|
|
|
} else { |
|
742
|
0
|
|
|
|
|
0
|
$error_message = 'Error: '.$@; |
|
743
|
|
|
|
|
|
|
} |
|
744
|
|
|
|
|
|
|
|
|
745
|
|
|
|
|
|
|
# track the error / set current state |
|
746
|
0
|
|
|
|
|
0
|
$self->error_tracker($error_message, $args{url}, $response_code); |
|
747
|
|
|
|
|
|
|
|
|
748
|
|
|
|
|
|
|
# otherwise, we are in pretty good shape |
|
749
|
|
|
|
|
|
|
} else { |
|
750
|
|
|
|
|
|
|
|
|
751
|
2
|
|
|
|
|
8
|
$self->{current_status} = 'OK'; |
|
752
|
|
|
|
|
|
|
} |
|
753
|
|
|
|
|
|
|
|
|
754
|
2
|
|
|
|
|
11
|
return $self->{current_status}; |
|
755
|
|
|
|
|
|
|
|
|
756
|
|
|
|
|
|
|
} |
|
757
|
|
|
|
|
|
|
|
|
758
|
|
|
|
|
|
|
# for tracking errors into $self->{errrors}[]; |
|
759
|
|
|
|
|
|
|
sub error_tracker { |
|
760
|
0
|
|
|
0
|
0
|
0
|
my $self = shift; |
|
761
|
|
|
|
|
|
|
|
|
762
|
0
|
|
|
|
|
0
|
my ($error_message, $url, $response_code) = @_; |
|
763
|
|
|
|
|
|
|
# required is the error message; optional is the URL we were trying to call, |
|
764
|
|
|
|
|
|
|
# and the HTTP status code returned in that API call |
|
765
|
|
|
|
|
|
|
|
|
766
|
0
|
0
|
|
|
|
0
|
return 'Error - No Message' if !$error_message; |
|
767
|
|
|
|
|
|
|
|
|
768
|
|
|
|
|
|
|
# defaults |
|
769
|
0
|
|
0
|
|
|
0
|
$url ||= 'N/A'; |
|
770
|
0
|
|
0
|
|
|
0
|
$response_code ||= 'N/A'; |
|
771
|
|
|
|
|
|
|
|
|
772
|
|
|
|
|
|
|
# we must currently be in an error state |
|
773
|
0
|
|
|
|
|
0
|
$self->{current_status} = 'Error'; |
|
774
|
|
|
|
|
|
|
|
|
775
|
|
|
|
|
|
|
# track the error |
|
776
|
0
|
|
|
|
|
0
|
push(@{ $self->{errors} }, { |
|
|
0
|
|
|
|
|
0
|
|
|
777
|
|
|
|
|
|
|
'error_message' => $error_message, |
|
778
|
|
|
|
|
|
|
'url' => $url, |
|
779
|
|
|
|
|
|
|
'response_code' => $response_code, |
|
780
|
|
|
|
|
|
|
}); |
|
781
|
|
|
|
|
|
|
|
|
782
|
0
|
|
|
|
|
0
|
return 'Error'; |
|
783
|
|
|
|
|
|
|
|
|
784
|
|
|
|
|
|
|
} |
|
785
|
|
|
|
|
|
|
|
|
786
|
|
|
|
|
|
|
# please tell me the lastest error message |
|
787
|
|
|
|
|
|
|
sub latest_error { |
|
788
|
2
|
|
|
2
|
0
|
11
|
my $self = shift; |
|
789
|
|
|
|
|
|
|
|
|
790
|
|
|
|
|
|
|
# don't fall for the old "Modification of non-creatable array value attempted" trick |
|
791
|
2
|
50
|
|
|
|
19
|
return 'No error message found' if !$self->{errors}[0]; |
|
792
|
|
|
|
|
|
|
|
|
793
|
0
|
|
|
|
|
|
my $error = $self->{errors}[-1]; |
|
794
|
0
|
|
|
|
|
|
return $$error{error_message}.' ('.$$error{response_code}.')'; |
|
795
|
|
|
|
|
|
|
|
|
796
|
|
|
|
|
|
|
} |
|
797
|
|
|
|
|
|
|
|
|
798
|
|
|
|
|
|
|
1; |
|
799
|
|
|
|
|
|
|
|
|
800
|
|
|
|
|
|
|
__END__ |