File Coverage

lib/Amazon/S3/Bucket.pm
Criterion Covered Total %
statement 54 464 11.6
branch 0 184 0.0
condition 0 138 0.0
subroutine 18 57 31.5
pod 27 29 93.1
total 99 872 11.3


line stmt bran cond sub pod time code
1             package Amazon::S3::Bucket;
2              
3 2     2   9 use strict;
  2         2  
  2         54  
4 2     2   5 use warnings;
  2         4  
  2         66  
5              
6 2     2   641 use Amazon::S3::Constants qw(:all);
  2         7  
  2         736  
7 2     2   976 use Amazon::S3::Util qw(:all);
  2         8  
  2         372  
8              
9 2     2   15 use Carp;
  2         3  
  2         212  
10 2     2   16 use Data::Dumper;
  2         4  
  2         125  
11 2     2   10 use Digest::MD5 qw(md5 md5_hex);
  2         4  
  2         94  
12 2     2   10 use Digest::MD5::File qw(file_md5 file_md5_hex);
  2         4  
  2         51  
13 2     2   322 use English qw(-no_match_vars);
  2         78  
  2         26  
14 2     2   2205 use File::stat;
  2         16637  
  2         165  
15 2     2   1058 use IO::File;
  2         12418  
  2         279  
16 2     2   1170 use IO::Scalar;
  2         9642  
  2         137  
17 2     2   14 use MIME::Base64;
  2         3  
  2         96  
18 2     2   7 use List::Util qw(none pairs);
  2         2  
  2         122  
19 2     2   9 use Scalar::Util qw(reftype);
  2         4  
  2         83  
20 2     2   9 use URI;
  2         3  
  2         41  
21 2     2   6 use XML::Simple; ## no critic (DiscouragedModules)
  2         4  
  2         14  
22              
23 2     2   120 use parent qw(Exporter Class::Accessor::Fast);
  2         4  
  2         17  
24              
25             our $VERSION = '2.0.2'; ## no critic (RequireInterpolation)
26              
27             __PACKAGE__->mk_accessors(
28             qw(
29             bucket
30             creation_date
31             account
32             buffer_size
33             region
34             logger
35             verify_region
36             ),
37             );
38              
39             ########################################################################
40             sub new {
41             ########################################################################
42 0     0 1   my ( $class, @args ) = @_;
43              
44 0           my $options = get_parameters(@args);
45              
46 0   0       $options->{buffer_size} ||= $DEFAULT_BUFFER_SIZE;
47              
48 0           my $self = $class->SUPER::new($options);
49              
50 0 0         croak 'no bucket'
51             if !$self->bucket;
52              
53 0 0         croak 'no account'
54             if !$self->account;
55              
56 0 0         if ( !$self->logger ) {
57 0           $self->logger( $self->account->get_logger );
58             }
59              
60             # now each bucket maintains its own region
61 0 0 0       if ( !$self->region && $self->verify_region ) {
    0          
62 0           my $region;
63              
64 0 0         if ( !$self->account->err ) {
65 0   0       $region = $self->get_location_constraint() // 'us-east-1';
66             }
67              
68 0   0       $self->logger->debug( sprintf "bucket: %s region: %s\n",
69             $self->bucket, ( $region // $EMPTY ) );
70              
71 0           $self->region($region);
72             }
73             elsif ( !$self->region ) {
74 0           $self->region( $self->account->region );
75             }
76              
77 0           return $self;
78             }
79              
80             ########################################################################
81             sub _uri {
82             ########################################################################
83 0     0     my ( $self, $key ) = @_;
84              
85 0 0         if ($key) {
86 0           $key =~ s/^\///xsm;
87             }
88              
89 0           my $account = $self->account;
90              
91 0           my $uri = $self->bucket . $SLASH;
92              
93 0 0         if ($key) {
94 0           $uri .= urlencode($key);
95             }
96              
97 0 0         if ( $account->dns_bucket_names ) {
98 0           $uri =~ s/^\///xsm;
99             }
100              
101 0           return $uri;
102             }
103              
104             ########################################################################
105             sub add_key {
106             ########################################################################
107 0     0 1   my ( $self, $key, $value, $conf ) = @_;
108              
109 0 0 0       croak 'must specify key'
110             if !$key || !length $key;
111              
112 0   0       $conf //= {};
113              
114 0           my $account = $self->account;
115              
116 0           my $headers = delete $conf->{headers};
117 0   0       $headers //= {};
118              
119 0 0         if ( $conf->{acl_short} ) {
120 0           $account->_validate_acl_short( $conf->{acl_short} );
121              
122 0           $conf->{'x-amz-acl'} = $conf->{acl_short};
123              
124 0           delete $conf->{acl_short};
125             }
126              
127 0           $headers = { %{$conf}, %{$headers} };
  0            
  0            
128              
129 0           set_md5_header( data => $value, headers => $headers );
130              
131 0 0         if ( ref $value ) {
132 0           $value = _content_sub( ${$value}, $self->buffer_size );
  0            
133              
134 0           $headers->{'x-amz-content-sha256'} = 'UNSIGNED-PAYLOAD';
135             }
136              
137             # If we're pushing to a bucket that's under
138             # DNS flux, we might get a 307 Since LWP doesn't support actually
139             # waiting for a 100 Continue response, we'll just send a HEAD first
140             # to see what's going on
141 0           my $retval = eval {
142 0           return $self->_add_key(
143             { headers => $headers,
144             data => $value,
145             key => $key,
146             },
147             );
148             };
149              
150             # one more try? if someone specified the wrong region, we'll get a
151             # 301 and you'll only know the region of redirection - no location
152             # header provided...
153 0 0         if ($EVAL_ERROR) {
154 0           my $rsp = $account->last_response;
155              
156 0 0         if ( $rsp->code eq $HTTP_MOVED_PERMANENTLY ) {
157 0           $self->region( $rsp->headers->{'x-amz-bucket-region'} );
158             }
159              
160 0           $retval = $self->_add_key(
161             { headers => $headers,
162             data => $value,
163             key => $key,
164             },
165             );
166             }
167              
168 0           return $retval;
169             }
170              
171             ########################################################################
172             sub _add_key {
173             ########################################################################
174 0     0     my ( $self, @args ) = @_;
175              
176 0           my ( $data, $headers, $key ) = @{ $args[0] }{qw{data headers key}};
  0            
177              
178 0           my $account = $self->account;
179              
180 0 0         if ( ref $data ) {
181 0           return $account->_send_request_expect_nothing_probed(
182             { method => 'PUT',
183             path => $self->_uri($key),
184             headers => $headers,
185             data => $data,
186             region => $self->region,
187             },
188             );
189             }
190             else {
191 0           return $account->_send_request_expect_nothing(
192             { method => 'PUT',
193             path => $self->_uri($key),
194             headers => $headers,
195             data => $data,
196             region => $self->region,
197             },
198             );
199             }
200             }
201              
202             ########################################################################
203             sub add_key_filename {
204             ########################################################################
205 0     0 1   my ( $self, $key, $value, $conf ) = @_;
206              
207 0           return $self->add_key( $key, \$value, $conf );
208             }
209              
210             ########################################################################
211             sub upload_multipart_object {
212             ########################################################################
213 0     0 0   my ( $self, @args ) = @_;
214              
215 0           my $logger = $self->logger;
216              
217 0           my $parameters = get_parameters(@args);
218              
219             croak 'no key!'
220 0 0         if !$parameters->{key};
221              
222             croak 'either data, callback or fh must be set!'
223 0 0 0       if !$parameters->{data} && !$parameters->{callback} && !$parameters->{fh};
      0        
224              
225             croak 'callback must be a reference to a subroutine!'
226             if $parameters->{callback}
227 0 0 0       && reftype( $parameters->{callback} ) ne 'CODE';
228              
229 0   0       $parameters->{abort_on_error} //= $TRUE;
230 0   0       $parameters->{chunk_size} //= $MIN_MULTIPART_UPLOAD_CHUNK_SIZE;
231              
232 0 0 0       if ( !$parameters->{callback} && !$parameters->{fh} ) {
233             #...but really nobody should be passing a >5MB scalar
234             my $data
235 0 0         = ref $parameters->{data} ? $parameters->{data} : \$parameters->{data};
236              
237 0           $parameters->{fh} = IO::Scalar->new($data);
238             }
239              
240             # ...having a file handle implies, we use this callback
241 0 0         if ( $parameters->{fh} ) {
242 0           my $fh = $parameters->{fh};
243              
244 0           $fh->seek( 0, 2 );
245              
246 0           my $length = $fh->tell;
247 0           $fh->seek( 0, 0 );
248              
249 0     0     $logger->trace( sub { return sprintf 'length of object: %s', $length; } );
  0            
250              
251 0 0         croak 'length of the object must be >= '
252             . $MIN_MULTIPART_UPLOAD_CHUNK_SIZE
253             if $length < $MIN_MULTIPART_UPLOAD_CHUNK_SIZE;
254              
255             my $chunk_size
256             = ( $parameters->{chunk_size} && $parameters->{chunk_size} )
257             > $MIN_MULTIPART_UPLOAD_CHUNK_SIZE
258             ? $parameters->{chunk_size}
259 0 0 0       : $MIN_MULTIPART_UPLOAD_CHUNK_SIZE;
260              
261             $parameters->{callback} = sub {
262             return
263 0 0   0     if !$length;
264              
265 0           my $bytes_read = 0;
266              
267 0 0         my $n = $length >= $chunk_size ? $chunk_size : $length;
268              
269 0           $logger->trace( sprintf 'reading %d bytes', $n );
270              
271 0           my $buffer;
272              
273 0           my $bytes = $fh->read( $buffer, $n, $bytes_read );
274 0           $logger->trace( sprintf 'read %d bytes', $bytes );
275              
276 0           $bytes_read += $bytes;
277              
278 0           $length -= $bytes;
279              
280 0           $logger->trace( sprintf '%s bytes left to read', $length );
281              
282 0           return ( \$buffer, $bytes );
283 0           };
284             }
285              
286 0   0       my $headers = $parameters->{headers} || {};
287              
288 0           my $id = $self->initiate_multipart_upload( $parameters->{key}, $headers );
289              
290 0           $logger->trace( sprintf 'multipart id: %s', $id );
291              
292 0           my $part = 1;
293              
294 0           my %parts;
295              
296 0           my $key = $parameters->{key};
297              
298 0           my $retval = eval {
299 0           while (1) {
300 0           my ( $buffer, $length ) = $parameters->{callback}->();
301 0 0         last if !$buffer;
302              
303 0           my $etag = $self->upload_part_of_multipart_upload(
304             { id => $id,
305             key => $key,
306             data => $buffer,
307             part => $part,
308             },
309             );
310              
311 0           $parts{ $part++ } = $etag;
312             }
313              
314 0           $self->complete_multipart_upload( $parameters->{key}, $id, \%parts );
315             };
316              
317 0 0 0       if ( $EVAL_ERROR && $parameters->{abort_on_error} ) {
318 0           $self->abort_multipart_upload( $key, $id );
319 0           %parts = ();
320             }
321              
322 0           return \%parts;
323             }
324              
325             # Initiates a multipart upload operation. This is necessary for uploading
326             # files > 5Gb to Amazon S3
327             #
328             # returns: upload ID assigned by Amazon (used to identify this
329             # particular upload in other operations)
330             ########################################################################
331             sub initiate_multipart_upload {
332             ########################################################################
333 0     0 1   my ( $self, $key, $headers ) = @_;
334              
335 0 0         croak 'Object key is required'
336             if !$key;
337              
338 0           my $acct = $self->account;
339              
340 0           my $request = $acct->_make_request(
341             { region => $self->region,
342             method => 'POST',
343             path => $self->_uri($key) . '?uploads=',
344             headers => $headers,
345             },
346             );
347              
348 0           my $response = $acct->_do_http($request);
349              
350 0           $acct->_croak_if_response_error($response);
351              
352 0           my $r = $acct->_xpc_of_content( $response->content );
353              
354 0           return $r->{UploadId};
355             }
356              
357             #
358             # Upload a part of a file as part of a multipart upload operation
359             # Each part must be at least 5mb (except for the last piece).
360             # This returns the Amazon-generated eTag for the uploaded file segment.
361             # It is necessary to keep track of the eTag for each part number
362             # The complete operation will want a sequential list of all the part
363             # numbers along with their eTags.
364             #
365             ########################################################################
366             sub upload_part_of_multipart_upload {
367             ########################################################################
368 0     0 1   my ( $self, @args ) = @_;
369              
370 0           my ( $key, $upload_id, $part_number, $data, $length );
371              
372 0 0         if ( @args == 1 ) {
373 0 0         if ( reftype( $args[0] ) eq 'HASH' ) {
    0          
374             ( $key, $upload_id, $part_number, $data, $length )
375 0           = @{ $args[0] }{qw{ key id part data length}};
  0            
376             }
377             elsif ( reftype( $args[0] ) eq 'ARRAY' ) {
378 0           ( $key, $upload_id, $part_number, $data, $length ) = @{ $args[0] };
  0            
379             }
380             }
381             else {
382 0           ( $key, $upload_id, $part_number, $data, $length ) = @args;
383             }
384              
385             # argh...wish we didn't have to do this!
386 0 0         if ( ref $data ) {
387 0           $data = ${$data};
  0            
388             }
389              
390 0   0       $length = $length || length $data;
391              
392 0 0         croak 'Object key is required'
393             if !$key;
394              
395 0 0         croak 'Upload id is required'
396             if !$upload_id;
397              
398 0 0         croak 'Part Number is required'
399             if !$part_number;
400              
401 0           my $headers = {};
402 0           my $acct = $self->account;
403              
404 0           set_md5_header( data => $data, headers => $headers );
405              
406 0           my $path = create_api_uri(
407             path => $self->_uri($key),
408             partNumber => ${part_number},
409             uploadId => ${upload_id}
410             );
411              
412 0           my $params = $QUESTION_MARK
413             . create_query_string(
414             partNumber => ${part_number},
415             uploadId => ${upload_id}
416             );
417              
418             $self->logger->debug(
419             sub {
420 0     0     return Dumper(
421             [ part => $part_number,
422             length => length $data,
423             path => $path,
424             ]
425             );
426             }
427 0           );
428              
429 0           my $request = $acct->_make_request(
430             { region => $self->region,
431             method => 'PUT',
432             path => $self->_uri($key) . $params,
433             #path => $path,
434             headers => $headers,
435             data => $data,
436             },
437             );
438              
439 0           my $response = $acct->_do_http($request);
440              
441 0           $acct->_croak_if_response_error($response);
442              
443             # We'll need to save the etag for later when completing the transaction
444 0           my $etag = $response->header('ETag');
445              
446 0 0         if ($etag) {
447 0           $etag =~ s/^"//xsm;
448 0           $etag =~ s/"$//xsm;
449             }
450              
451 0           return $etag;
452             }
453              
454             #
455             # Inform Amazon that the multipart upload has been completed
456             # You must supply a hash of part Numbers => eTags
457             # For amazon to use to put the file together on their servers.
458             #
459             ########################################################################
460             sub complete_multipart_upload {
461             ########################################################################
462 0     0 1   my ( $self, $key, $upload_id, $parts_hr ) = @_;
463              
464 0           $self->logger->debug( Dumper( [ $key, $upload_id, $parts_hr ] ) );
465              
466 0 0         croak 'Object key is required'
467             if !$key;
468              
469 0 0         croak 'Upload id is required'
470             if !$upload_id;
471              
472 0 0         croak 'Part number => etag hashref is required'
473             if ref $parts_hr ne 'HASH';
474              
475             # The complete command requires sending a block of xml containing all
476             # the part numbers and their associated etags (returned from the upload)
477 0           my $content = _create_multipart_upload_request($parts_hr);
478              
479 0           $self->logger->debug("content: \n$content");
480              
481 0           my $md5 = md5($content);
482 0           my $md5_base64 = encode_base64($md5);
483 0           chomp $md5_base64;
484              
485 0           my $headers = {
486             'Content-MD5' => $md5_base64,
487             'Content-Length' => length $content,
488             'Content-Type' => 'application/xml',
489             };
490              
491 0           my $acct = $self->account;
492 0           my $params = "?uploadId=${upload_id}";
493              
494 0           my $request = $acct->_make_request(
495             { region => $self->region,
496             method => 'POST',
497             path => $self->_uri($key) . $params,
498             headers => $headers,
499             data => $content,
500             },
501             );
502              
503 0           my $response = $acct->_do_http($request);
504              
505 0 0         if ( $response->code !~ /\A2\d\d\z/xsm ) {
506 0           $acct->_remember_errors( $response->content, 1 );
507 0           croak $response->status_line;
508             }
509              
510 0           return $TRUE;
511             }
512              
513             ########################################################################
514             sub abort_multipart_upload {
515             ########################################################################
516 0     0 1   my ( $self, $key, $upload_id ) = @_;
517              
518 0 0         croak 'Object key is required'
519             if !$key;
520              
521 0 0         croak 'Upload id is required'
522             if !$upload_id;
523              
524 0           my $acct = $self->account;
525 0           my $params = "?uploadId=${upload_id}";
526              
527 0           my $request = $acct->_make_request(
528             { region => $self->region,
529             method => 'DELETE',
530             path => $self->_uri($key) . $params,
531             },
532             );
533              
534 0           my $response = $acct->_do_http($request);
535              
536 0           $acct->_croak_if_response_error($response);
537              
538 0           return $TRUE;
539             }
540              
541             #
542             # List all the uploaded parts for an ongoing multipart upload
543             # It returns the block of XML returned from Amazon
544             #
545             ########################################################################
546             sub list_multipart_upload_parts {
547             ########################################################################
548 0     0 1   my ( $self, $key, $upload_id, $headers ) = @_;
549              
550 0 0         croak 'Object key is required'
551             if !$key;
552              
553 0 0         croak 'Upload id is required'
554             if !$upload_id;
555              
556 0           my $acct = $self->account;
557 0           my $params = "?uploadId=${upload_id}";
558              
559 0           my $request = $acct->_make_request(
560             { region => $self->region,
561             method => 'GET',
562             path => $self->_uri($key) . $params,
563             headers => $headers,
564             },
565             );
566              
567 0           my $response = $acct->_do_http($request);
568              
569 0           $acct->_croak_if_response_error($response);
570              
571             # Just return the XML, let the caller figure out what to do with it
572 0           return $response->content;
573             }
574              
575             # List all the currently active multipart upload operations
576             # Returns the block of XML returned from Amazon
577             ########################################################################
578             sub list_multipart_uploads {
579             ########################################################################
580 0     0 1   my ( $self, $headers ) = @_;
581              
582 0           my $acct = $self->account;
583              
584 0           my $request = $acct->_make_request(
585             { region => $self->region,
586             method => 'GET',
587             path => $self->_uri() . '?uploads',
588             headers => $headers,
589             },
590             );
591              
592 0           my $response = $acct->_do_http($request);
593              
594 0           $acct->_croak_if_response_error($response);
595              
596             # Just return the XML, let the caller figure out what to do with it
597 0           return $response->content;
598             }
599              
600             ########################################################################
601             sub head_key {
602             ########################################################################
603 0     0 1   my ( $self, $key ) = @_;
604              
605 0           return $self->get_key( $key, 'HEAD' );
606             }
607              
608             ########################################################################
609             sub get_key_v2 {
610             ########################################################################
611 0     0 0   my ( $self, $key, $method, $headers ) = @_;
612              
613 0           return $self->_get_key( $key, $method, undef, $headers );
614             }
615              
616             ########################################################################
617             sub get_key {
618             ########################################################################
619 0     0 1   my ( $self, @args ) = @_;
620              
621 0           my ( $key, $method, $headers, $uri_params );
622              
623 0 0         if ( ref $args[0] ) {
624             ( $key, $method, $headers, $uri_params )
625 0           = @{ $args[0] }{qw(key method headers uri_params)};
  0            
626             }
627             else {
628 0           ( $key, $method, $headers, $uri_params ) = @args;
629             }
630              
631 0           return $self->_get_key(
632             key => $key,
633             method => $method,
634             filename => undef,
635             headers => $headers,
636             uri_params => $uri_params,
637             );
638             }
639              
640             ########################################################################
641             sub _get_key {
642             ########################################################################
643 0     0     my ( $self, @args ) = @_;
644              
645 0           my $parameters = get_parameters(@args);
646              
647             my ( $key, $method, $filename, $headers, $uri_params )
648 0           = @{$parameters}{qw(key method filename headers uri_params)};
  0            
649              
650 0   0       $method //= 'GET';
651              
652 0           my $uri = $self->_uri($key);
653              
654 0 0 0       if ( $uri_params && keys %{$uri_params} ) {
  0            
655 0           $uri = $QUESTION_MARK . create_query_string($uri_params);
656             }
657              
658 0 0         if ( ref $filename ) {
659 0           $filename = ${$filename};
  0            
660             }
661              
662 0           my $acct = $self->account;
663              
664 0           my $request = $acct->_make_request(
665             { region => $self->region,
666             method => $method,
667             path => $uri,
668             headers => $headers,
669             },
670             );
671              
672 0           my $response = $acct->_do_http( $request, $filename );
673              
674             return
675 0 0         if $response->code eq $HTTP_NOT_FOUND;
676              
677 0           $acct->_croak_if_response_error($response);
678              
679 0           my $etag = $response->header('ETag');
680              
681 0 0         if ($etag) {
682 0           $etag =~ s/^"//xsm;
683 0           $etag =~ s/"$//xsm;
684             }
685              
686 0   0       my $retval = {
      0        
      0        
      0        
687             content_length => ( $response->content_length || 0 ),
688             content_type => scalar $response->content_type,
689             etag => $etag,
690             value => ( $response->content // $EMPTY ),
691             content_range => ( $response->header('Content-Range') || $EMPTY ),
692             last_modified => ( $response->header('Last-Modified') || $EMPTY ),
693             };
694              
695             # Validate against data corruption by verifying the MD5 (only if not partial)
696 0 0 0       if ( $method eq 'GET' && $response->code ne $HTTP_PARTIAL_CONTENT ) {
697             my $md5
698             = ( $filename and -f $filename )
699             ? file_md5_hex($filename)
700 0 0 0       : md5_hex( $retval->{value} );
701              
702             # Some S3-compatible providers return an all-caps MD5 value in the
703             # etag so it should be lc'd for comparison.
704 0 0         croak "Computed and Response MD5's do not match: $md5 : $etag"
705             if $md5 ne lc $etag;
706             }
707              
708 0           foreach my $header ( $response->headers->header_field_names ) {
709 0 0         next if $header !~ /x-amz-meta-/ixsm;
710 0           $retval->{ lc $header } = $response->header($header);
711             }
712              
713 0           return $retval;
714             }
715              
716             ########################################################################
717             sub get_key_filename {
718             ########################################################################
719 0     0 1   my ( $self, @args ) = @_;
720              
721 0           my ( $key, $method, $filename, $headers, $uri_params );
722              
723 0 0         if ( ref $args[0] ) {
724             ( $key, $method, $filename, $headers, $uri_params )
725 0           = @{ $args[0] }{qw(key method filename headers uri_params)};
  0            
726             }
727             else {
728 0           ( $key, $method, $filename, $headers, $uri_params ) = @args;
729             }
730              
731 0 0         if ( !defined $filename ) {
732 0           $filename = $key;
733             }
734              
735 0           return $self->_get_key(
736             key => $key,
737             method => $method,
738             filename => \$filename,
739             headers => $headers,
740             uri_params => $uri_params,
741             );
742             }
743              
744             ########################################################################
745             # See: https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObject.html
746             #
747             # Note that in this request the bucket object is the destination you
748             # specify the source bucket in the key (bucket-name/source-key) or the
749             # header x-amz-copy-source
750             ########################################################################
751             sub copy_object {
752             ########################################################################
753 0     0 1   my ( $self, @args ) = @_;
754              
755 0           my $parameters = get_parameters(@args);
756              
757             my ( $source, $key, $bucket, $headers_in )
758 0           = @{$parameters}{qw(source key bucket headers)};
  0            
759              
760 0   0       $headers_in //= {};
761              
762 0           my %request_headers;
763              
764 0 0         if ( reftype($headers_in) eq 'ARRAY' ) {
    0          
765 0           %request_headers = @{$headers_in};
  0            
766             }
767             elsif ( reftype($headers_in) eq 'HASH' ) {
768 0           %request_headers = %{$headers_in};
  0            
769             }
770             else {
771 0 0 0       croak 'headers must be hash or array'
772             if !ref($headers_in) || reftype($headers_in) ne 'HASH';
773             }
774              
775             croak 'source or x-amz-copy-source must be specified'
776 0 0 0       if !$source && !exists $request_headers{'x-amz-copy-source'};
777              
778 0 0         croak 'no key'
779             if !$key;
780              
781 0           my $acct = $self->account;
782 0   0       $bucket //= $self->bucket();
783              
784 0 0         if ( !$request_headers{'x-amz-copy-source'} ) {
785              
786 0           $request_headers{'x-amz-copy-source'} = sprintf '%s/%s', $bucket,
787             urlencode($source);
788             }
789              
790 0   0       $request_headers{'x-amz-tagging-directive'} //= 'COPY';
791              
792 0           $key = $self->_uri($key);
793              
794 0           my $request = $acct->_make_request(
795             method => 'PUT',
796             path => $key,
797             headers => \%request_headers,
798             );
799              
800 0           my $response = $acct->_do_http($request);
801              
802 0 0         if ( $response->code !~ /\A2\d{2}\z/xsm ) {
803 0           $acct->_remember_errors( $response->content, 1 );
804 0           croak $response->status_line;
805             }
806              
807 0           return $acct->_xpc_of_content( $response->content );
808             }
809              
810             ########################################################################
811             sub delete_key {
812             ########################################################################
813 0     0 1   my ( $self, $key, $version ) = @_;
814              
815 0 0 0       croak 'must specify key'
816             if !$key && length $key;
817              
818 0           my $account = $self->account;
819              
820 0           my $path = $self->_uri($key);
821              
822 0 0         if ($version) {
823 0           $path = '?versionId=' . $version;
824             }
825              
826 0           return $account->_send_request_expect_nothing(
827             { method => 'DELETE',
828             region => $self->region,
829             path => $path,
830             headers => {},
831             },
832             );
833             }
834              
835             ########################################################################
836             sub _format_delete_keys {
837             ########################################################################
838 0     0     my (@args) = @_;
839              
840 0           my @keys;
841              
842 0 0         if ( ref $args[0] ) {
    0          
843 0 0         if ( reftype( $args[0] ) eq 'ARRAY' ) { # list of keys, no version ids
    0          
844 0           foreach my $key ( @{ $args[0] } ) {
  0            
845 0 0 0       if ( ref($key) && reftype($key) eq 'HASH' ) {
846              
847             push @keys,
848             {
849             Key => [ $key->{Key} ],
850             defined $key->{VersionId}
851 0 0         ? ( VersionId => [ $key->{VersionId} ] )
852             : (),
853             };
854             }
855             else { # array of keys
856 0           push @keys, { Key => [$key], };
857             }
858             }
859             }
860             elsif ( reftype( $args[0] ) eq 'CODE' ) { # sub that returns key, version id
861 0           while ( my (@object) = $args[0]->() ) {
862 0 0 0       last if !@object || !defined $object[0];
863              
864 0 0         push @keys,
865             {
866             Key => [ $object[0] ],
867             defined $object[1] ? ( VersionId => [ $object[1] ] ) : (),
868             };
869             }
870             }
871             else { # list of keys
872 0           croak 'argument must be array or list';
873             }
874             }
875             elsif (@args) {
876 0           @keys = map { { Key => [$_] } } @args;
  0            
877             }
878             else {
879 0           croak 'must specify keys';
880             }
881              
882 0 0         croak 'must not exceed ' . $MAX_DELETE_KEYS . ' keys'
883             if @keys > $MAX_DELETE_KEYS;
884              
885 0           return \@keys;
886             }
887              
888             # @args => list of keys
889             # $args[0] => array of hashes (Key, [VersionId]) VersionId is optional
890             # $args[0] => array of scalars (keys)
891             # $args[0] => code reference that returns key, version id or empty
892             # $args[0] => hash ({ quiet => 1, keys => $keys})
893              
894             # Throws exception if no keys or in wrong format...
895             ########################################################################
896             sub delete_keys {
897             ########################################################################
898 0     0 1   my ( $self, @args ) = @_;
899              
900 0           my ( $keys, $quiet_mode, $headers );
901              
902 0 0 0       if ( ref $args[0] && reftype( $args[0] ) eq 'HASH' ) {
903 0           ( $keys, $quiet_mode, $headers ) = @{ $args[0] }{qw(keys quiet headers)};
  0            
904 0           $keys = _format_delete_keys($keys);
905             }
906             else {
907 0           $keys = _format_delete_keys(@args);
908             }
909              
910 0 0         if ( defined $quiet_mode ) {
911 0 0         $quiet_mode = $quiet_mode ? 'true' : 'false';
912             }
913             else {
914 0           $quiet_mode = 'false';
915             }
916              
917 0           my $content = {
918             xmlns => $S3_XMLNS,
919             Quiet => [$quiet_mode],
920             Object => $keys,
921             };
922              
923 0           my $xml_content = XMLout(
924             $content,
925             RootName => 'Delete',
926             XMLDecl => $XMLDECL,
927             );
928              
929 0           my $account = $self->account;
930              
931 0           my $md5 = md5($xml_content);
932 0           my $md5_base64 = encode_base64($md5);
933              
934 0           chomp $md5_base64;
935              
936 0   0       $headers //= {};
937              
938 0           $headers->{'Content-MD5'} = $md5_base64;
939              
940 0           return $account->_send_request(
941             { method => 'POST',
942             region => $self->region,
943             path => $self->_uri() . '?delete',
944             headers => $headers,
945             data => $xml_content,
946             },
947             );
948             }
949              
950             ########################################################################
951             sub delete_bucket {
952             ########################################################################
953 0     0 1   my ($self) = @_;
954              
955 0 0         croak 'Unexpected arguments'
956             if @_ > 1;
957              
958 0           return $self->account->delete_bucket($self);
959             }
960              
961             ########################################################################
962             sub list_v2 {
963             ########################################################################
964 0     0 1   my ( $self, $conf ) = @_;
965              
966 0   0       $conf ||= {};
967              
968 0           $conf->{bucket} = $self->bucket;
969 0           $conf->{'list-type'} = '2';
970              
971 0 0         if ( $conf->{'marker'} ) {
972 0           $conf->{'continuation-token'} = delete $conf->{'marker'};
973             }
974              
975 0           return $self->list($conf);
976             }
977              
978             ########################################################################
979             sub list {
980             ########################################################################
981 0     0 1   my ( $self, $conf ) = @_;
982              
983 0   0       $conf ||= {};
984              
985 0           $conf->{bucket} = $self->bucket;
986              
987 0           return $self->account->list_bucket($conf);
988             }
989              
990             ########################################################################
991             sub list_all_v2 {
992             ########################################################################
993 0     0 1   my ( $self, $conf ) = @_;
994              
995 0   0       $conf //= {};
996              
997 0           $conf->{bucket} = $self->bucket;
998              
999 0           return $self->account->list_bucket_all_v2($conf);
1000             }
1001              
1002             ########################################################################
1003             sub list_all {
1004             ########################################################################
1005 0     0 1   my ( $self, $conf ) = @_;
1006              
1007 0   0       $conf //= {};
1008              
1009 0           $conf->{bucket} = $self->bucket;
1010              
1011 0           return $self->account->list_bucket_all($conf);
1012             }
1013              
1014             ########################################################################
1015             sub get_acl {
1016             ########################################################################
1017 0     0 1   my ( $self, $key, $headers ) = @_;
1018              
1019 0           my $account = $self->account;
1020              
1021 0   0       my $request = $account->_make_request(
1022             { region => $self->region,
1023             method => 'GET',
1024             path => $self->_uri($key) . '?acl=',
1025             headers => $headers // {},
1026             },
1027             );
1028              
1029 0           my $old_redirectable = $account->ua->requests_redirectable;
1030 0           $account->ua->requests_redirectable( [] );
1031              
1032 0           my $response = $account->_do_http($request);
1033              
1034 0 0         if ( $response->code =~ /^30/xsm ) {
1035 0           my $xpc = $account->_xpc_of_content( $response->content );
1036 0           my $uri = URI->new( $response->header('location') );
1037              
1038 0           my $old_host = $account->host;
1039 0           $account->host( $uri->host );
1040              
1041 0           $request = $account->_make_request(
1042             { region => $self->region,
1043             method => 'GET',
1044             path => $uri->path,
1045             headers => {},
1046             },
1047             );
1048              
1049 0           $response = $account->_do_http($request);
1050              
1051 0           $account->ua->requests_redirectable($old_redirectable);
1052 0           $account->host($old_host);
1053             }
1054              
1055 0           my $content;
1056              
1057             # do we test for NOT FOUND, returning undef?
1058 0 0         if ( $response->code ne $HTTP_NOT_FOUND ) {
1059 0           $account->_croak_if_response_error($response);
1060 0           $content = $response->content;
1061             }
1062              
1063 0           return $content;
1064             }
1065              
1066             ########################################################################
1067             sub set_acl {
1068             ########################################################################
1069 0     0 1   my ( $self, $conf ) = @_;
1070              
1071 0           my $account = $self->account;
1072              
1073 0   0       $conf //= {};
1074              
1075             croak 'need either acl_xml or acl_short'
1076 0 0 0       if !$conf->{acl_xml} && !$conf->{acl_short};
1077              
1078             croak 'cannot provide both acl_xml and acl_short'
1079 0 0 0       if $conf->{acl_xml} && $conf->{acl_short};
1080              
1081 0           my $path = $self->_uri( $conf->{key} ) . '?acl';
1082              
1083 0           my $headers = $conf->{headers};
1084              
1085 0 0         if ( $conf->{acl_short} ) {
1086 0   0       $headers->{'x-amz-acl'} //= $conf->{acl_short};
1087             }
1088              
1089 0   0       my $xml = $conf->{acl_xml} // $EMPTY;
1090              
1091 0           $headers->{'Content-Length'} = length $xml;
1092              
1093 0           return $account->_send_request_expect_nothing(
1094             { method => 'PUT',
1095             path => $path,
1096             headers => $headers,
1097             data => $xml,
1098             region => $self->region,
1099             },
1100             );
1101             }
1102              
1103             ########################################################################
1104             sub get_location_constraint {
1105             ########################################################################
1106 0     0 1   my ( $self, @args ) = @_;
1107              
1108 0           my $parameters = get_parameters(@args);
1109              
1110             my ( $bucket, $headers, $region )
1111 0           = @{$parameters}{qw(bucket headers region)};
  0            
1112              
1113 0           my $account = $self->account;
1114 0   0       $bucket //= $self->bucket;
1115              
1116 0   0       my $location = $account->_send_request(
1117             { region => $region // $self->region,
1118             method => 'GET',
1119             path => $bucket . '/?location=',
1120             headers => $headers,
1121             },
1122             );
1123              
1124 0 0         return $location
1125             if $location;
1126              
1127 0 0         croak $account->errstr
1128             if $account->_remember_errors($location);
1129              
1130 0           return;
1131             }
1132              
1133             ########################################################################
1134             sub last_response {
1135             ########################################################################
1136 0     0 1   my ($self) = @_;
1137              
1138 0           return $self->account->last_response;
1139             }
1140              
1141             ########################################################################
1142             sub err {
1143             ########################################################################
1144 0     0 1   my ($self) = @_;
1145              
1146 0           return $self->account->err;
1147             }
1148              
1149             ########################################################################
1150             sub errstr {
1151             ########################################################################
1152 0     0 1   my ($self) = @_;
1153              
1154 0           return $self->account->errstr;
1155             }
1156              
1157             ########################################################################
1158             sub error {
1159             ########################################################################
1160 0     0 1   my ($self) = @_;
1161              
1162 0           return $self->account->error;
1163             }
1164              
1165             ########################################################################
1166             sub _content_sub {
1167             ########################################################################
1168 0     0     my ( $filename, $buffer_size ) = @_;
1169              
1170 0           my $stat = stat $filename;
1171              
1172 0           my $remaining = $stat->size;
1173 0   0       my $blksize = $stat->blksize || $buffer_size;
1174              
1175 0 0 0       croak "$filename not a readable file with fixed size"
1176             if !-r $filename || !$remaining;
1177              
1178 0 0         my $fh = IO::File->new( $filename, 'r' )
1179             or croak "Could not open $filename: $OS_ERROR";
1180              
1181 0           $fh->binmode;
1182              
1183             return sub {
1184 0     0     my $buffer;
1185              
1186             # upon retries the file is closed and we must reopen it
1187 0 0         if ( !$fh->opened ) {
1188 0 0         $fh = IO::File->new( $filename, 'r' )
1189             or croak "Could not open $filename: $OS_ERROR";
1190              
1191 0           $fh->binmode;
1192              
1193 0           $remaining = $stat->size;
1194             }
1195              
1196 0           my $read = $fh->read( $buffer, $blksize );
1197              
1198 0 0         if ( !$read ) {
1199 0 0 0       croak
1200             "Error while reading upload content $filename ($remaining remaining) $OS_ERROR"
1201             if $OS_ERROR and $remaining;
1202              
1203 0 0         $fh->close # otherwise, we found EOF
1204             or croak "close of upload content $filename failed: $OS_ERROR";
1205              
1206 0   0       $buffer ||= $EMPTY; # LWP expects an empty string on finish, read returns 0
1207             }
1208              
1209 0           $remaining -= length $buffer;
1210              
1211 0           return $buffer;
1212 0           };
1213             }
1214              
1215             ########################################################################
1216             sub _create_multipart_upload_request {
1217             ########################################################################
1218 0     0     my ($parts_hr) = @_;
1219              
1220 0           my @parts;
1221              
1222 0           foreach my $part_num ( sort { $a <=> $b } keys %{$parts_hr} ) {
  0            
  0            
1223             push @parts,
1224             {
1225             PartNumber => $part_num,
1226 0           ETag => $parts_hr->{$part_num},
1227             };
1228             }
1229              
1230 0           return create_xml_request(
1231             { CompleteMultipartUpload => { Part => \@parts } } );
1232             }
1233              
1234             1;
1235              
1236             __END__