File Coverage

lib/Amazon/S3/Bucket.pm
Criterion Covered Total %
statement 48 448 10.7
branch 0 182 0.0
condition 0 121 0.0
subroutine 16 52 30.7
pod 27 29 93.1
total 91 832 10.9


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