File Coverage

blib/lib/Backblaze/B2V2Client.pm
Criterion Covered Total %
statement 66 237 27.8
branch 15 86 17.4
condition 4 49 8.1
subroutine 13 25 52.0
pod 13 17 76.4
total 111 414 26.8


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.5';
6              
7             # our dependencies:
8 1     1   1034 use Cpanel::JSON::XS;
  1         3  
  1         88  
9 1     1   592 use Digest::SHA qw(sha1_hex);
  1         3494  
  1         115  
10 1     1   626 use MIME::Base64;
  1         662  
  1         62  
11 1     1   945 use Path::Tiny;
  1         12332  
  1         104  
12 1     1   772 use URI::Escape;
  1         1611  
  1         75  
13 1     1   1018 use WWW::Mechanize;
  1         178105  
  1         65  
14              
15             # I wish I could apply this to my diet.
16 1     1   15 use strict;
  1         3  
  1         31  
17 1     1   6 use warnings;
  1         2  
  1         3086  
18              
19             # object constructor; will automatically authorize this session
20             sub new {
21 1     1 1 687 my $class = shift;
22              
23             # required args are the account ID and application_key
24 1         4 my ($application_key_id, $application_key) = @_;
25              
26             # cannot operate without these
27 1 50 33     23 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         14 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         20601 $self->b2_authorize_account(); # this adds more goodness to $self for use in the other methods
45              
46 1         8 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         22 my $encoded_auth_string = encode_base64($self->{application_key_id}.':'.$self->{application_key});
55              
56             # add that header in
57 1         10 $self->{mech}->add_header( 'Authorization' => 'Basic '.$encoded_auth_string );
58              
59             # call the b2_talker() method to authenticate our session
60 1         25 $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         6 $self->{account_id} = $self->{b2_response}{accountId};
66 1         4 $self->{api_url} = $self->{b2_response}{apiUrl};
67 1         8 $self->{account_authorization_token} = $self->{b2_response}{authorizationToken};
68 1         5 $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             }
79              
80             # method to download a file by ID; probably most commonly used
81             sub b2_download_file_by_id {
82 1     1 1 11026 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       13 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         11 );
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     17 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, $disable_encryption) = @_;
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             # prepare the basics for our request
399             my $post_params = {
400             'accountId' => $self->{account_id},
401 0         0 'bucketName' => $bucket_name,
402             'bucketType' => 'allPrivate',
403             };
404              
405             # unless instructed otherwise, we should encrypt the files in this bucket
406 0 0       0 unless ($disable_encryption) {
407             $$post_params{defaultServerSideEncryption} = {
408 0         0 'mode' => 'SSE-B2',
409             'algorithm' => 'AES256',
410             };
411             }
412              
413             # create the bucket...
414             $self->b2_talker(
415             'url' => $self->{api_url}.'/b2api/v2/b2_create_bucket',
416             'authorization' => $self->{account_authorization_token},
417 0         0 'post_params' => $post_params,
418             );
419              
420 0 0       0 if ($self->{current_status} eq 'OK') { # if successful...
421              
422             # stash our new bucket into $self->{buckets}
423             $self->{buckets}{$bucket_name} = {
424             'bucket_id' => $self->{b2_response}{bucketId},
425 0         0 'bucket_type' => 'allPrivate',
426             };
427              
428             }
429              
430             }
431              
432             # method to delete a bucket -- please don't use ;)
433             sub b2_delete_bucket {
434 0     0 1 0 my $self = shift;
435              
436 0         0 my ($bucket_name) = @_;
437              
438             # bucket_id is required
439 0 0       0 if (!$bucket_name) {
440 0         0 $self->error_tracker('The bucket_name must be provided for b2_delete_bucket().');
441 0         0 return;
442             }
443              
444             # resolve that bucket_name to a bucket_id
445 0         0 $self->b2_list_buckets($bucket_name);
446              
447             # send the request
448             $self->b2_talker(
449             'url' => $self->{api_url}.'/b2api/v2/b2_delete_bucket',
450             'authorization' => $self->{account_authorization_token},
451             'post_params' => {
452             'accountId' => $self->{account_id},
453             'bucketId' => $self->{buckets}{$bucket_name}{bucket_id},
454             },
455 0         0 );
456              
457             }
458              
459             # method to delete a stored file object. B2 thinks of these as 'versions,'
460             # but if you use unique names, one version = one file
461             sub b2_delete_file_version {
462 0     0 1 0 my $self = shift;
463              
464             # required arguments are the file_name and file_id for the target file
465 0         0 my ($file_name, $file_id) = @_;
466              
467             # bucket_id is required
468 0 0 0     0 if (!$file_name || !$file_id) {
469 0         0 $self->error_tracker('The file_name and file_id args must be provided for b2_delete_file_version().');
470 0         0 return;
471             }
472              
473             # send the request
474             $self->b2_talker(
475             'url' => $self->{api_url}.'/b2api/v2/b2_delete_file_version',
476             'authorization' => $self->{account_authorization_token},
477 0         0 'post_params' => {
478             'fileName' => $file_name,
479             'fileId' => $file_id,
480             },
481             );
482              
483              
484             }
485              
486             # method to upload a large file (>100MB)
487             sub b2_upload_large_file {
488 0     0 1 0 my $self = shift;
489 0         0 my (%args) = @_;
490             # this must include valid entries for 'new_file_name' and 'bucket_name'
491             # and it has to a valid location in 'file_location' (Do not load in file contents)
492             # also, you can include 'content_type' (which would be the MIME Type'
493             # if you do not want B2 to auto-determine the MIME/content-type
494              
495             # did they provide a file location or path?
496 0 0 0     0 if ($args{file_location} && -e "$args{file_location}") {
497             # if they didn't provide a file-name, use the one on this file
498 0         0 $args{new_file_name} = path( $args{file_location} )->basename;
499             } else {
500 0         0 $self->error_tracker(qq{You must provide a valid 'file_location' arg for b2_upload_large_file().});
501 0         0 return;
502             }
503              
504             # protect my sanity...
505 0         0 my ($bucket_name, $file_contents_part, $file_location, $large_file_id, $part_number, $remaining_file_size, $sha1_array, $size_sent, $stat);
506 0         0 $file_location = $args{file_location};
507 0         0 $bucket_name = $args{bucket_name};
508              
509             # must be 100MB or bigger
510 0         0 $stat = path($file_location)->stat;
511 0 0       0 if ($stat->size < $self->{recommended_part_size} ) {
512 0         0 $self->error_tracker(qq{Please use b2_upload_large_file() for files larger than $self->{recommended_part_size} .});
513 0         0 return;
514             }
515              
516             # need a bucket name
517 0 0       0 if (!$bucket_name) {
518 0         0 $self->error_tracker(qq{You must provide a valid 'bucket_name' arg for b2_upload_large_file().});
519 0         0 return;
520             }
521              
522             # default content-type
523 0   0     0 $args{content_type} ||= 'b2/x-auto';
524              
525             # get the bucket ID
526 0         0 $self->b2_list_buckets($bucket_name);
527              
528             # kick off the upload in the API
529             $self->b2_talker(
530             'url' => $self->{api_url}.'/b2api/v2/b2_start_large_file',
531             'authorization' => $self->{account_authorization_token},
532             'post_params' => {
533             'bucketId' => $self->{buckets}{$bucket_name}{bucket_id},
534             'fileName' => $args{new_file_name},
535             'contentType' => $args{content_type},
536             },
537 0         0 );
538              
539             # these are all needed for each b2_upload_part web call
540 0         0 $large_file_id = $self->{b2_response}{fileId};
541 0 0       0 return if !$large_file_id; # there was an error in the request
542              
543             # open the large file
544 0         0 open(FH, $file_location);
545              
546 0         0 $remaining_file_size = $stat->size;
547              
548 0         0 $part_number = 1;
549              
550             # cycle thru each chunk of the file
551 0         0 while ($remaining_file_size >= 0) {
552             # how much to send?
553 0 0       0 if ($remaining_file_size < $self->{recommended_part_size} ) {
554 0         0 $size_sent = $remaining_file_size;
555             } else {
556 0         0 $size_sent = $self->{recommended_part_size} ;
557             }
558              
559             # get the next upload url for this part
560             $self->b2_talker(
561             'url' => $self->{api_url}.'/b2api/v2/b2_get_upload_part_url',
562             'authorization' => $self->{account_authorization_token},
563 0         0 'post_params' => {
564             'fileId' => $large_file_id,
565             },
566             );
567              
568             # read in that section of the file and prep the SHA
569 0         0 sysread FH, $file_contents_part, $size_sent;
570 0         0 push(@$sha1_array,sha1_hex( $file_contents_part ));
571              
572             # upload that part
573             $self->b2_talker(
574             'url' => $self->{b2_response}{uploadUrl},
575             'authorization' => $self->{b2_response}{authorizationToken},
576 0         0 'special_headers' => {
577             'X-Bz-Content-Sha1' => $$sha1_array[-1],
578             'X-Bz-Part-Number' => $part_number,
579             'Content-Length' => $size_sent,
580             },
581             'file_contents' => $file_contents_part,
582             );
583              
584             # advance
585 0         0 $part_number++;
586 0         0 $remaining_file_size -= $self->{recommended_part_size} ;
587             }
588              
589             # close the file
590 0         0 close FH;
591              
592             # and tell B2
593             $self->b2_talker(
594             'url' => $self->{api_url}.'/b2api/v2/b2_finish_large_file',
595             'authorization' => $self->{account_authorization_token},
596 0         0 'post_params' => {
597             'fileId' => $large_file_id,
598             'partSha1Array' => $sha1_array,
599             },
600             );
601              
602             # phew, i'm tired...
603             }
604              
605              
606             # generic method to handle communication to B2
607             sub b2_talker {
608 2     2 1 6 my $self = shift;
609              
610             # args hash must include 'url' for the target API endpoint URL
611             # most other requests will also include a 'post_params' hashref, and 'authorization' value for the header
612             # for the b2_upload_file function, there will be several other headers + a file_contents arg
613 2         11 my (%args) = @_;
614              
615 2 50       10 if (!$args{url}) {
616 0         0 $self->error_tracker('Can not use b2_talker() without an endpoint URL.');
617             }
618              
619             # if they sent an Authorization header, set that value
620 2 100       11 if ($args{authorization}) {
621 1         10 $self->{mech}->delete_header( 'Authorization' );
622 1         21 $self->{mech}->add_header( 'Authorization' => $args{authorization} );
623             }
624              
625 2         21 my ($response, $response_code, $error_message, $header, @header_keys);
626              
627             # short-circuit if we had difficulty logging in previously
628 2 50       10 if ($self->{b2_login_error}) {
629              
630             # track the error / set current state
631 0         0 $self->error_tracker("Problem logging into Backblaze. Please check the 'errors' array in this object.", $args{url});
632              
633 0         0 return;
634             }
635              
636             # are we uploading a file?
637 2 50       17 if ($args{url} =~ /b2_upload_file|b2_upload_part/) {
    50          
638              
639             # add the special headers
640 0         0 @header_keys = keys %{ $args{special_headers} };
  0         0  
641 0         0 foreach $header (@header_keys) {
642 0         0 $self->{mech}->delete_header( $header );
643 0         0 $self->{mech}->add_header( $header => $args{special_headers}{$header} );
644             }
645              
646             # now upload the file
647 0         0 eval {
648 0         0 $response = $self->{mech}->post( $args{url}, content => $args{file_contents} );
649              
650             # we want this to be 200
651 0         0 $response_code = $response->{_rc};
652              
653 0         0 $self->{b2_response} = decode_json( $self->{mech}->content() );
654              
655             };
656              
657             # remove those special headers, cleaned-up for next time
658 0         0 foreach $header (@header_keys) {
659 0         0 $self->{mech}->delete_header( $header );
660             }
661              
662             # if not uploading and they sent POST params, we are doing a POST
663             } elsif (ref($args{post_params}) eq 'HASH') {
664 0         0 eval {
665             # send the POST
666 0         0 $response = $self->{mech}->post( $args{url}, content => encode_json($args{post_params}) );
667              
668             # we want this to be 200
669 0         0 $response_code = $response->code;
670              
671             # decode results
672 0         0 $self->{b2_response} = decode_json( $self->{mech}->content() );
673             };
674              
675             # otherwise, we are doing a GET
676             } else {
677              
678             # attempt the GET
679 2         6 eval {
680 2         15 $response = $self->{mech}->get( $args{url} );
681              
682             # we want this to be 200
683 2         1829935 $response_code = $response->code;
684              
685             # did we download a file?
686 2 100       59 if ($response->header( 'X-Bz-File-Name' )) {
    50          
687              
688             # grab those needed headers
689 1         53 foreach $header ('Content-Length','Content-Type','X-Bz-File-Id','X-Bz-File-Name','X-Bz-Content-Sha1') {
690 5         189 $self->{b2_response}{$header} = $response->header( $header );
691             }
692              
693             # and the file itself
694 1         52 $self->{b2_response}{file_contents} = $self->{mech}->content();
695              
696             } elsif ($response_code eq '200') { # no, regular JSON, decode results
697 1         157 $self->{b2_response} = decode_json( $self->{mech}->content() );
698             }
699             };
700             }
701              
702             # there is a problem if there is a problem
703 2 50 33     138 if ($@ || $response_code ne '200') {
704 0 0       0 if ($self->{b2_response}{message}) {
705 0         0 $error_message = 'API Message: '.$self->{b2_response}{message};
706             } else {
707 0         0 $error_message = 'Error: '.$@;
708             }
709              
710             # track the error / set current state
711 0         0 $self->error_tracker($error_message, $args{url}, $response_code);
712              
713             # otherwise, we are in pretty good shape
714             } else {
715              
716 2         14 $self->{current_status} = 'OK';
717             }
718              
719             }
720              
721             # for tracking errors into $self->{errrors}[];
722             sub error_tracker {
723 0     0 0 0 my $self = shift;
724              
725 0         0 my ($error_message, $url, $response_code) = @_;
726             # required is the error message; optional is the URL we were trying to call,
727             # and the HTTP status code returned in that API call
728              
729 0 0       0 return if !$error_message;
730              
731             # defaults
732 0   0     0 $url ||= 'N/A';
733 0   0     0 $response_code ||= 'N/A';
734              
735             # we must currently be in an error state
736 0         0 $self->{current_status} = 'Error';
737              
738             # track the error
739 0         0 push(@{ $self->{errors} }, {
  0         0  
740             'error_message' => $error_message,
741             'url' => $url,
742             'response_code' => $response_code,
743             });
744              
745             }
746              
747             # please tell me the lastest error message
748             sub latest_error {
749 2     2 0 13 my $self = shift;
750              
751             # don't fall for the old "Modification of non-creatable array value attempted" trick
752 2 50       21 return 'No error message found' if !$self->{errors}[0];
753              
754 0           my $error = $self->{errors}[-1];
755 0           return $$error{error_message}.' ('.$$error{response_code}.')';
756              
757             }
758              
759             1;
760              
761             __END__