File Coverage

blib/lib/Backblaze/B2V2Client.pm
Criterion Covered Total %
statement 66 234 28.2
branch 15 84 17.8
condition 4 49 8.1
subroutine 13 25 52.0
pod 13 17 76.4
total 111 409 27.1


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