File Coverage

blib/lib/Backblaze/B2V2Client.pm
Criterion Covered Total %
statement 63 231 27.2
branch 15 84 17.8
condition 4 49 8.1
subroutine 12 24 50.0
pod 13 17 76.4
total 107 405 26.4


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