File Coverage

blib/lib/Backblaze/B2V2Client.pm
Criterion Covered Total %
statement 69 253 27.2
branch 15 86 17.4
condition 4 59 6.7
subroutine 13 25 52.0
pod 13 17 76.4
total 114 440 25.9


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__