| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
# You may distribute under the terms of either the GNU General Public License |
|
2
|
|
|
|
|
|
|
# or the Artistic License (the same terms as Perl itself) |
|
3
|
|
|
|
|
|
|
# |
|
4
|
|
|
|
|
|
|
# (C) Paul Evans, 2013-2014 -- leonerd@leonerd.org.uk |
|
5
|
|
|
|
|
|
|
|
|
6
|
|
|
|
|
|
|
package Net::Async::Webservice::S3; |
|
7
|
|
|
|
|
|
|
|
|
8
|
10
|
|
|
10
|
|
1493652
|
use strict; |
|
|
10
|
|
|
|
|
24
|
|
|
|
10
|
|
|
|
|
394
|
|
|
9
|
10
|
|
|
10
|
|
59
|
use warnings; |
|
|
10
|
|
|
|
|
20
|
|
|
|
10
|
|
|
|
|
352
|
|
|
10
|
10
|
|
|
10
|
|
67
|
use base qw( IO::Async::Notifier ); |
|
|
10
|
|
|
|
|
59
|
|
|
|
10
|
|
|
|
|
10899
|
|
|
11
|
|
|
|
|
|
|
|
|
12
|
|
|
|
|
|
|
our $VERSION = '0.18'; |
|
13
|
|
|
|
|
|
|
|
|
14
|
10
|
|
|
10
|
|
31847
|
use Carp; |
|
|
10
|
|
|
|
|
23
|
|
|
|
10
|
|
|
|
|
626
|
|
|
15
|
|
|
|
|
|
|
|
|
16
|
10
|
|
|
10
|
|
8367
|
use Digest::HMAC_SHA1; |
|
|
10
|
|
|
|
|
351855
|
|
|
|
10
|
|
|
|
|
499
|
|
|
17
|
10
|
|
|
10
|
|
96
|
use Digest::MD5 qw( md5 ); |
|
|
10
|
|
|
|
|
770
|
|
|
|
10
|
|
|
|
|
588
|
|
|
18
|
10
|
|
|
10
|
|
2462
|
use Future 0.21; # ->then_with_f |
|
|
10
|
|
|
|
|
25339
|
|
|
|
10
|
|
|
|
|
357
|
|
|
19
|
10
|
|
|
10
|
|
10075
|
use Future::Utils 0.16 qw( repeat try_repeat fmap1 ); |
|
|
10
|
|
|
|
|
24155
|
|
|
|
10
|
|
|
|
|
831
|
|
|
20
|
10
|
|
|
10
|
|
316909
|
use HTTP::Date qw( time2str ); |
|
|
10
|
|
|
|
|
226912
|
|
|
|
10
|
|
|
|
|
845
|
|
|
21
|
10
|
|
|
10
|
|
9288
|
use HTTP::Request; |
|
|
10
|
|
|
|
|
72161
|
|
|
|
10
|
|
|
|
|
291
|
|
|
22
|
10
|
|
|
10
|
|
9193
|
use IO::Async::Timer::Countdown; |
|
|
10
|
|
|
|
|
22092
|
|
|
|
10
|
|
|
|
|
337
|
|
|
23
|
10
|
|
|
10
|
|
78
|
use List::Util qw( sum ); |
|
|
10
|
|
|
|
|
31
|
|
|
|
10
|
|
|
|
|
1252
|
|
|
24
|
10
|
|
|
10
|
|
9575
|
use MIME::Base64 qw( encode_base64 ); |
|
|
10
|
|
|
|
|
8509
|
|
|
|
10
|
|
|
|
|
773
|
|
|
25
|
10
|
|
|
10
|
|
67
|
use Scalar::Util qw( blessed ); |
|
|
10
|
|
|
|
|
19
|
|
|
|
10
|
|
|
|
|
729
|
|
|
26
|
10
|
|
|
10
|
|
64
|
use URI::Escape qw( uri_escape_utf8 ); |
|
|
10
|
|
|
|
|
18
|
|
|
|
10
|
|
|
|
|
547
|
|
|
27
|
10
|
|
|
10
|
|
17472
|
use XML::LibXML; |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
use XML::LibXML::XPathContext; |
|
29
|
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
my $libxml = XML::LibXML->new; |
|
31
|
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
use constant DEFAULT_S3_HOST => "s3.amazonaws.com"; |
|
33
|
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
=head1 NAME |
|
35
|
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
C - use Amazon's S3 web service with C |
|
37
|
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
=head1 SYNOPSIS |
|
39
|
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
use IO::Async::Loop; |
|
41
|
|
|
|
|
|
|
use Net::Async::Webservice::S3; |
|
42
|
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
my $loop = IO::Async::Loop->new; |
|
44
|
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
my $s3 = Net::Async::Webservice::S3->new( |
|
46
|
|
|
|
|
|
|
access_key => ..., |
|
47
|
|
|
|
|
|
|
secret_key => ..., |
|
48
|
|
|
|
|
|
|
bucket => "my-bucket-here", |
|
49
|
|
|
|
|
|
|
); |
|
50
|
|
|
|
|
|
|
$loop->add( $s3 ); |
|
51
|
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
my $put_f = $s3->put_object( |
|
53
|
|
|
|
|
|
|
key => "the-key", |
|
54
|
|
|
|
|
|
|
value => "A new value for the key\n"; |
|
55
|
|
|
|
|
|
|
); |
|
56
|
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
my $get_f = $s3->get_object( |
|
58
|
|
|
|
|
|
|
key => "another-key", |
|
59
|
|
|
|
|
|
|
); |
|
60
|
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
$loop->await_all( $put_f, $get_f ); |
|
62
|
|
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
print "The value is:\n", $get_f->get; |
|
64
|
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
=head1 DESCRIPTION |
|
66
|
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
This module provides a webservice API around Amazon's S3 web service for use |
|
68
|
|
|
|
|
|
|
in an L-based program. Each S3 operation is represented by a method |
|
69
|
|
|
|
|
|
|
that returns a L; this future, if successful, will eventually return |
|
70
|
|
|
|
|
|
|
the result of the operation. |
|
71
|
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
=cut |
|
73
|
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
sub _init |
|
75
|
|
|
|
|
|
|
{ |
|
76
|
|
|
|
|
|
|
my $self = shift; |
|
77
|
|
|
|
|
|
|
my ( $args ) = @_; |
|
78
|
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
$args->{http} ||= do { |
|
80
|
|
|
|
|
|
|
require Net::Async::HTTP; |
|
81
|
|
|
|
|
|
|
Net::Async::HTTP->VERSION( '0.33' ); # 'timeout' and 'stall_timeout' failures |
|
82
|
|
|
|
|
|
|
my $http = Net::Async::HTTP->new; |
|
83
|
|
|
|
|
|
|
$self->add_child( $http ); |
|
84
|
|
|
|
|
|
|
$http; |
|
85
|
|
|
|
|
|
|
}; |
|
86
|
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
$args->{max_retries} //= 3; |
|
88
|
|
|
|
|
|
|
$args->{list_max_keys} //= 1000; |
|
89
|
|
|
|
|
|
|
$args->{read_size} //= 64*1024; # 64 KiB |
|
90
|
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
# S3 docs suggest > 100MB should use multipart. They don't actually |
|
92
|
|
|
|
|
|
|
# document what size of parts to use, but we'll use that again. |
|
93
|
|
|
|
|
|
|
$args->{part_size} //= 100*1024*1024; |
|
94
|
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
$args->{host} //= DEFAULT_S3_HOST; |
|
96
|
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
return $self->SUPER::_init( @_ ); |
|
98
|
|
|
|
|
|
|
} |
|
99
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
=head1 PARAMETERS |
|
101
|
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
The following named parameters may be passed to C or C: |
|
103
|
|
|
|
|
|
|
|
|
104
|
|
|
|
|
|
|
=over 8 |
|
105
|
|
|
|
|
|
|
|
|
106
|
|
|
|
|
|
|
=item http => Net::Async::HTTP |
|
107
|
|
|
|
|
|
|
|
|
108
|
|
|
|
|
|
|
Optional. Allows the caller to provide a specific asynchronous HTTP user agent |
|
109
|
|
|
|
|
|
|
object to use. This will be invoked with a single method, as documented by |
|
110
|
|
|
|
|
|
|
L: |
|
111
|
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
$response_f = $http->do_request( request => $request, ... ) |
|
113
|
|
|
|
|
|
|
|
|
114
|
|
|
|
|
|
|
If absent, a new instance will be created and added as a child notifier of |
|
115
|
|
|
|
|
|
|
this object. If a value is supplied, it will be used as supplied and I |
|
116
|
|
|
|
|
|
|
specifically added as a child notifier. In this case, the caller must ensure |
|
117
|
|
|
|
|
|
|
it gets added to the underlying L instance, if required. |
|
118
|
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
=item access_key => STRING |
|
120
|
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
=item secret_key => STRING |
|
122
|
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
The twenty-character Access Key ID and forty-character Secret Key to use for |
|
124
|
|
|
|
|
|
|
authenticating requests to S3. |
|
125
|
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
=item ssl => BOOL |
|
127
|
|
|
|
|
|
|
|
|
128
|
|
|
|
|
|
|
Optional. If given a true value, will use C URLs over SSL. Defaults to |
|
129
|
|
|
|
|
|
|
off. This feature requires the optional L module if using |
|
130
|
|
|
|
|
|
|
L. |
|
131
|
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
=item bucket => STRING |
|
133
|
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
Optional. If supplied, gives the default bucket name to use, at which point it |
|
135
|
|
|
|
|
|
|
is optional to supply to the remaining methods. |
|
136
|
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
=item prefix => STRING |
|
138
|
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
Optional. If supplied, this prefix string is prepended to any key names passed |
|
140
|
|
|
|
|
|
|
in methods, and stripped from the response from C. It can be used |
|
141
|
|
|
|
|
|
|
to keep operations of the object contained within the named key space. If this |
|
142
|
|
|
|
|
|
|
string is supplied, don't forget that it should end with the path delimiter in |
|
143
|
|
|
|
|
|
|
use by the key naming scheme (for example C>). |
|
144
|
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
=item host => STRING |
|
146
|
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
Optional. Sets the hostname to talk to the S3 service. Usually the default of |
|
148
|
|
|
|
|
|
|
C is sufficient. This setting allows for communication with |
|
149
|
|
|
|
|
|
|
other service providers who provide the same API as S3. |
|
150
|
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
=item max_retries => INT |
|
152
|
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
Optional. Maximum number of times to retry a failed operation. Defaults to 3. |
|
154
|
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
=item list_max_keys => INT |
|
156
|
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
Optional. Maximum number of keys at a time to request from S3 for the |
|
158
|
|
|
|
|
|
|
C method. Larger values may be more efficient as fewer roundtrips |
|
159
|
|
|
|
|
|
|
will be required per method call. Defaults to 1000. |
|
160
|
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
=item part_size => INT |
|
162
|
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
Optional. Size in bytes to break content for using multipart upload. If an |
|
164
|
|
|
|
|
|
|
object key's size is no larger than this value, multipart upload will not be |
|
165
|
|
|
|
|
|
|
used. Defaults to 100 MiB. |
|
166
|
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
=item read_size => INT |
|
168
|
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
Optional. Size in bytes to read per call to the C<$gen_value> content |
|
170
|
|
|
|
|
|
|
generation function in C. Defaults to 64 KiB. Be aware that too |
|
171
|
|
|
|
|
|
|
large a value may lead to the C stall timer failing to be invoked on slow |
|
172
|
|
|
|
|
|
|
enough connections, causing spurious timeouts. |
|
173
|
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
=item timeout => NUM |
|
175
|
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
Optional. If configured, this is passed into individual requests of the |
|
177
|
|
|
|
|
|
|
underlying C object, except for the actual content C or |
|
178
|
|
|
|
|
|
|
C operations. It is therefore used by C, C, |
|
179
|
|
|
|
|
|
|
and the multi-part metadata operations used by C. To apply an |
|
180
|
|
|
|
|
|
|
overall timeout to an individual C or C operation, |
|
181
|
|
|
|
|
|
|
pass a specific C argument to those methods specifically. |
|
182
|
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
=item stall_timeout => NUM |
|
184
|
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
Optional. If configured, this is passed into the underlying |
|
186
|
|
|
|
|
|
|
C object and used for all content uploads and downloads. |
|
187
|
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
=item put_concurrent => INT |
|
189
|
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
Optional. If configured, gives a default value for the C parameter |
|
191
|
|
|
|
|
|
|
to C. |
|
192
|
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
=back |
|
194
|
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
=cut |
|
196
|
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
sub configure |
|
198
|
|
|
|
|
|
|
{ |
|
199
|
|
|
|
|
|
|
my $self = shift; |
|
200
|
|
|
|
|
|
|
my %args = @_; |
|
201
|
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
foreach (qw( http access_key secret_key ssl bucket prefix host max_retries |
|
203
|
|
|
|
|
|
|
list_max_keys part_size read_size timeout stall_timeout |
|
204
|
|
|
|
|
|
|
put_concurrent )) { |
|
205
|
|
|
|
|
|
|
exists $args{$_} and $self->{$_} = delete $args{$_}; |
|
206
|
|
|
|
|
|
|
} |
|
207
|
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
$self->SUPER::configure( %args ); |
|
209
|
|
|
|
|
|
|
} |
|
210
|
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
=head1 METHODS |
|
212
|
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
The following methods all support the following common arguments: |
|
214
|
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
=over 8 |
|
216
|
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
=item timeout => NUM |
|
218
|
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
=item stall_timeout => NUM |
|
220
|
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
Optional. Passed directly to the underlying C<< Net::Async::HTTP->request >> |
|
222
|
|
|
|
|
|
|
method. |
|
223
|
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
=back |
|
225
|
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
Each method below that yields a C is documented in the form |
|
227
|
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
$s3->METHOD( ARGS ) ==> YIELDS |
|
229
|
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
Where the C part indicates the values that will eventually be returned |
|
231
|
|
|
|
|
|
|
by the C method on the returned Future object, if it succeeds. |
|
232
|
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
=cut |
|
234
|
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
sub _make_request |
|
236
|
|
|
|
|
|
|
{ |
|
237
|
|
|
|
|
|
|
my $self = shift; |
|
238
|
|
|
|
|
|
|
my %args = @_; |
|
239
|
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
my $method = $args{method}; |
|
241
|
|
|
|
|
|
|
defined $args{content} or croak "Missing 'content'"; |
|
242
|
|
|
|
|
|
|
|
|
243
|
|
|
|
|
|
|
my @params; |
|
244
|
|
|
|
|
|
|
foreach my $key ( sort keys %{ $args{query_params} } ) { |
|
245
|
|
|
|
|
|
|
next unless defined( my $value = $args{query_params}->{$key} ); |
|
246
|
|
|
|
|
|
|
$key =~ s/_/-/g; |
|
247
|
|
|
|
|
|
|
push @params, $key . "=" . uri_escape_utf8( $value, "^A-Za-z0-9_-" ); |
|
248
|
|
|
|
|
|
|
} |
|
249
|
|
|
|
|
|
|
|
|
250
|
|
|
|
|
|
|
my $bucket = $args{bucket} // $self->{bucket}; |
|
251
|
|
|
|
|
|
|
my $path = $args{abs_path} // join "", grep { defined } $self->{prefix}, $args{path}; |
|
252
|
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
my $scheme = $self->{ssl} ? "https" : "http"; |
|
254
|
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
my $uri; |
|
256
|
|
|
|
|
|
|
if( length $bucket <= 63 and $bucket =~ m{^[A-Z0-9][A-Z0-9.-]+$}i ) { |
|
257
|
|
|
|
|
|
|
$uri = "$scheme://$bucket.$self->{host}/$path"; |
|
258
|
|
|
|
|
|
|
} |
|
259
|
|
|
|
|
|
|
else { |
|
260
|
|
|
|
|
|
|
$uri = "$scheme://$self->{host}/$bucket/$path"; |
|
261
|
|
|
|
|
|
|
} |
|
262
|
|
|
|
|
|
|
$uri .= "?" . join( "&", @params ) if @params; |
|
263
|
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
my $s3 = $self->{s3}; |
|
265
|
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
my $meta = $args{meta} || {}; |
|
267
|
|
|
|
|
|
|
|
|
268
|
|
|
|
|
|
|
my @headers = ( |
|
269
|
|
|
|
|
|
|
Date => time2str( time ), |
|
270
|
|
|
|
|
|
|
%{ $args{headers} || {} }, |
|
271
|
|
|
|
|
|
|
( map { +"X-Amz-Meta-$_" => $meta->{$_} } sort keys %$meta ), |
|
272
|
|
|
|
|
|
|
); |
|
273
|
|
|
|
|
|
|
|
|
274
|
|
|
|
|
|
|
my $request = HTTP::Request->new( $method, $uri, \@headers, $args{content} ); |
|
275
|
|
|
|
|
|
|
$request->content_length( length $args{content} ); |
|
276
|
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
$self->_gen_auth_header( $request, $bucket, $path ); |
|
278
|
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
return $request; |
|
280
|
|
|
|
|
|
|
} |
|
281
|
|
|
|
|
|
|
|
|
282
|
|
|
|
|
|
|
sub _gen_auth_header |
|
283
|
|
|
|
|
|
|
{ |
|
284
|
|
|
|
|
|
|
my $self = shift; |
|
285
|
|
|
|
|
|
|
my ( $request, $bucket, $path ) = @_; |
|
286
|
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
# See also |
|
288
|
|
|
|
|
|
|
# http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#ConstructingTheAuthenticationHeader |
|
289
|
|
|
|
|
|
|
|
|
290
|
|
|
|
|
|
|
my $canon_resource = "/$bucket/$path"; |
|
291
|
|
|
|
|
|
|
if( defined $request->uri->query and length $request->uri->query ) { |
|
292
|
|
|
|
|
|
|
my %params = $request->uri->query_form; |
|
293
|
|
|
|
|
|
|
my %params_to_sign; |
|
294
|
|
|
|
|
|
|
foreach (qw( partNumber uploadId )) { |
|
295
|
|
|
|
|
|
|
$params_to_sign{$_} = $params{$_} if exists $params{$_}; |
|
296
|
|
|
|
|
|
|
} |
|
297
|
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
my @params_to_sign; |
|
299
|
|
|
|
|
|
|
foreach ( sort keys %params_to_sign ) { |
|
300
|
|
|
|
|
|
|
push @params_to_sign, defined $params{$_} ? "$_=$params{$_}" : $_; |
|
301
|
|
|
|
|
|
|
} |
|
302
|
|
|
|
|
|
|
|
|
303
|
|
|
|
|
|
|
$canon_resource .= "?" . join( "&", @params_to_sign ) if @params_to_sign; |
|
304
|
|
|
|
|
|
|
} |
|
305
|
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
my %x_amz_headers; |
|
307
|
|
|
|
|
|
|
$request->scan( sub { |
|
308
|
|
|
|
|
|
|
$x_amz_headers{lc $_[0]} = $_[1] if $_[0] =~ m/^X-Amz-/i; |
|
309
|
|
|
|
|
|
|
}); |
|
310
|
|
|
|
|
|
|
|
|
311
|
|
|
|
|
|
|
my $x_amz_headers = ""; |
|
312
|
|
|
|
|
|
|
$x_amz_headers .= "$_:$x_amz_headers{$_}\n" for sort keys %x_amz_headers; |
|
313
|
|
|
|
|
|
|
|
|
314
|
|
|
|
|
|
|
my $buffer = join( "\n", |
|
315
|
|
|
|
|
|
|
$request->method, |
|
316
|
|
|
|
|
|
|
$request->header( "Content-MD5" ) // "", |
|
317
|
|
|
|
|
|
|
$request->header( "Content-Type" ) // "", |
|
318
|
|
|
|
|
|
|
$request->header( "Date" ) // "", |
|
319
|
|
|
|
|
|
|
$x_amz_headers . $canon_resource ); |
|
320
|
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
my $s3 = $self->{s3}; |
|
322
|
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
my $hmac = Digest::HMAC_SHA1->new( $self->{secret_key} ); |
|
324
|
|
|
|
|
|
|
$hmac->add( $buffer ); |
|
325
|
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
my $access_key = $self->{access_key}; |
|
327
|
|
|
|
|
|
|
my $authkey = encode_base64( $hmac->digest, "" ); |
|
328
|
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
$request->header( Authorization => "AWS $access_key:$authkey" ); |
|
330
|
|
|
|
|
|
|
} |
|
331
|
|
|
|
|
|
|
|
|
332
|
|
|
|
|
|
|
# Turn non-2xx results into errors |
|
333
|
|
|
|
|
|
|
sub _do_request |
|
334
|
|
|
|
|
|
|
{ |
|
335
|
|
|
|
|
|
|
my $self = shift; |
|
336
|
|
|
|
|
|
|
my ( $request, %args ) = @_; |
|
337
|
|
|
|
|
|
|
|
|
338
|
|
|
|
|
|
|
$self->{http}->do_request( |
|
339
|
|
|
|
|
|
|
request => $request, |
|
340
|
|
|
|
|
|
|
SSL => ( $request->uri->scheme eq "https" ), |
|
341
|
|
|
|
|
|
|
%args |
|
342
|
|
|
|
|
|
|
)->then_with_f( sub { |
|
343
|
|
|
|
|
|
|
my ( $f, $resp ) = @_; |
|
344
|
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
my $code = $resp->code; |
|
346
|
|
|
|
|
|
|
if( $code !~ m/^2/ ) { |
|
347
|
|
|
|
|
|
|
my $message = $resp->message; |
|
348
|
|
|
|
|
|
|
$message =~ s/\r$//; # HTTP::Response leaves the \r on this |
|
349
|
|
|
|
|
|
|
|
|
350
|
|
|
|
|
|
|
return Future->new->fail( |
|
351
|
|
|
|
|
|
|
"$code $message", http => $resp, $request |
|
352
|
|
|
|
|
|
|
); |
|
353
|
|
|
|
|
|
|
} |
|
354
|
|
|
|
|
|
|
|
|
355
|
|
|
|
|
|
|
return $f; |
|
356
|
|
|
|
|
|
|
}); |
|
357
|
|
|
|
|
|
|
} |
|
358
|
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
# Convert response into an XML XPathContext tree |
|
360
|
|
|
|
|
|
|
sub _do_request_xpc |
|
361
|
|
|
|
|
|
|
{ |
|
362
|
|
|
|
|
|
|
my $self = shift; |
|
363
|
|
|
|
|
|
|
my ( $request, @args ) = @_; |
|
364
|
|
|
|
|
|
|
|
|
365
|
|
|
|
|
|
|
$self->_do_request( $request, @args )->then( sub { |
|
366
|
|
|
|
|
|
|
my $resp = shift; |
|
367
|
|
|
|
|
|
|
|
|
368
|
|
|
|
|
|
|
my $xpc = XML::LibXML::XPathContext->new( $libxml->parse_string( $resp->content ) ); |
|
369
|
|
|
|
|
|
|
$xpc->registerNs( s3 => "http://s3.amazonaws.com/doc/2006-03-01/" ); |
|
370
|
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
return Future->wrap( $xpc ); |
|
372
|
|
|
|
|
|
|
}); |
|
373
|
|
|
|
|
|
|
} |
|
374
|
|
|
|
|
|
|
|
|
375
|
|
|
|
|
|
|
sub _retry |
|
376
|
|
|
|
|
|
|
{ |
|
377
|
|
|
|
|
|
|
my $self = shift; |
|
378
|
|
|
|
|
|
|
my ( $method, @args ) = @_; |
|
379
|
|
|
|
|
|
|
|
|
380
|
|
|
|
|
|
|
my $delay = 0.5; |
|
381
|
|
|
|
|
|
|
|
|
382
|
|
|
|
|
|
|
my $retries = $self->{max_retries}; |
|
383
|
|
|
|
|
|
|
try_repeat { |
|
384
|
|
|
|
|
|
|
my ( $prev_f ) = @_; |
|
385
|
|
|
|
|
|
|
|
|
386
|
|
|
|
|
|
|
# Add a small delay after failure before retrying |
|
387
|
|
|
|
|
|
|
my $delay_f = |
|
388
|
|
|
|
|
|
|
$prev_f ? $self->loop->delay_future( after => ( $delay *= 2 ) ) |
|
389
|
|
|
|
|
|
|
: Future->new->done; |
|
390
|
|
|
|
|
|
|
|
|
391
|
|
|
|
|
|
|
$delay_f->then( sub { $self->$method( @args ) } ); |
|
392
|
|
|
|
|
|
|
} while => sub { |
|
393
|
|
|
|
|
|
|
my $f = shift; |
|
394
|
|
|
|
|
|
|
my ( $failure, $name, $response, $request ) = $f->failure or return 0; # success |
|
395
|
|
|
|
|
|
|
return 0 if defined $name and $name eq "http" and |
|
396
|
|
|
|
|
|
|
$response and $response->code =~ m/^4/; # don't retry HTTP 4xx |
|
397
|
|
|
|
|
|
|
return --$retries; |
|
398
|
|
|
|
|
|
|
}; |
|
399
|
|
|
|
|
|
|
} |
|
400
|
|
|
|
|
|
|
|
|
401
|
|
|
|
|
|
|
=head2 $s3->list_bucket( %args ) ==> ( $keys, $prefixes ) |
|
402
|
|
|
|
|
|
|
|
|
403
|
|
|
|
|
|
|
Requests a list of the keys in a bucket, optionally within some prefix. |
|
404
|
|
|
|
|
|
|
|
|
405
|
|
|
|
|
|
|
Takes the following named arguments: |
|
406
|
|
|
|
|
|
|
|
|
407
|
|
|
|
|
|
|
=over 8 |
|
408
|
|
|
|
|
|
|
|
|
409
|
|
|
|
|
|
|
=item bucket => STR |
|
410
|
|
|
|
|
|
|
|
|
411
|
|
|
|
|
|
|
The name of the S3 bucket to query |
|
412
|
|
|
|
|
|
|
|
|
413
|
|
|
|
|
|
|
=item prefix => STR |
|
414
|
|
|
|
|
|
|
|
|
415
|
|
|
|
|
|
|
=item delimiter => STR |
|
416
|
|
|
|
|
|
|
|
|
417
|
|
|
|
|
|
|
Optional. If supplied, the prefix and delimiter to use to divide up the key |
|
418
|
|
|
|
|
|
|
namespace. Keys will be divided on the C parameter, and only the |
|
419
|
|
|
|
|
|
|
key space beginning with the given prefix will be queried. |
|
420
|
|
|
|
|
|
|
|
|
421
|
|
|
|
|
|
|
=back |
|
422
|
|
|
|
|
|
|
|
|
423
|
|
|
|
|
|
|
The Future will return two ARRAY references. The first provides a list of the |
|
424
|
|
|
|
|
|
|
keys found within the given prefix, and the second will return a list of the |
|
425
|
|
|
|
|
|
|
common prefixes of further nested keys. |
|
426
|
|
|
|
|
|
|
|
|
427
|
|
|
|
|
|
|
Each key in the C<$keys> list is given in a HASH reference containing |
|
428
|
|
|
|
|
|
|
|
|
429
|
|
|
|
|
|
|
=over 8 |
|
430
|
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
=item key => STRING |
|
432
|
|
|
|
|
|
|
|
|
433
|
|
|
|
|
|
|
The key's name |
|
434
|
|
|
|
|
|
|
|
|
435
|
|
|
|
|
|
|
=item last_modified => STRING |
|
436
|
|
|
|
|
|
|
|
|
437
|
|
|
|
|
|
|
The last modification time of the key given in ISO 8601 format |
|
438
|
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
=item etag => STRING |
|
440
|
|
|
|
|
|
|
|
|
441
|
|
|
|
|
|
|
The entity tag of the key |
|
442
|
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
=item size => INT |
|
444
|
|
|
|
|
|
|
|
|
445
|
|
|
|
|
|
|
The size of the key's value, in bytes |
|
446
|
|
|
|
|
|
|
|
|
447
|
|
|
|
|
|
|
=item storage_class => STRING |
|
448
|
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
The S3 storage class of the key |
|
450
|
|
|
|
|
|
|
|
|
451
|
|
|
|
|
|
|
=back |
|
452
|
|
|
|
|
|
|
|
|
453
|
|
|
|
|
|
|
Each key in the C<$prefixes> list is given as a plain string. |
|
454
|
|
|
|
|
|
|
|
|
455
|
|
|
|
|
|
|
=cut |
|
456
|
|
|
|
|
|
|
|
|
457
|
|
|
|
|
|
|
sub _list_bucket |
|
458
|
|
|
|
|
|
|
{ |
|
459
|
|
|
|
|
|
|
my $self = shift; |
|
460
|
|
|
|
|
|
|
my %args = @_; |
|
461
|
|
|
|
|
|
|
|
|
462
|
|
|
|
|
|
|
my @keys; |
|
463
|
|
|
|
|
|
|
my @prefixes; |
|
464
|
|
|
|
|
|
|
|
|
465
|
|
|
|
|
|
|
my $f = repeat { |
|
466
|
|
|
|
|
|
|
my ( $prev_f ) = @_; |
|
467
|
|
|
|
|
|
|
|
|
468
|
|
|
|
|
|
|
my $marker = $prev_f ? $prev_f->get : undef; |
|
469
|
|
|
|
|
|
|
|
|
470
|
|
|
|
|
|
|
my $req = $self->_make_request( |
|
471
|
|
|
|
|
|
|
method => "GET", |
|
472
|
|
|
|
|
|
|
bucket => $args{bucket}, |
|
473
|
|
|
|
|
|
|
abs_path => "", |
|
474
|
|
|
|
|
|
|
query_params => { |
|
475
|
|
|
|
|
|
|
prefix => join( "", grep { defined } $self->{prefix}, $args{prefix} ), |
|
476
|
|
|
|
|
|
|
delimiter => $args{delimiter}, |
|
477
|
|
|
|
|
|
|
marker => $marker, |
|
478
|
|
|
|
|
|
|
max_keys => $self->{list_max_keys}, |
|
479
|
|
|
|
|
|
|
}, |
|
480
|
|
|
|
|
|
|
content => "", |
|
481
|
|
|
|
|
|
|
); |
|
482
|
|
|
|
|
|
|
|
|
483
|
|
|
|
|
|
|
$self->_do_request_xpc( $req, |
|
484
|
|
|
|
|
|
|
timeout => $args{timeout} // $self->{timeout}, |
|
485
|
|
|
|
|
|
|
stall_timeout => $args{stall_timeout} // $self->{stall_timeout}, |
|
486
|
|
|
|
|
|
|
)->then( sub { |
|
487
|
|
|
|
|
|
|
my $xpc = shift; |
|
488
|
|
|
|
|
|
|
|
|
489
|
|
|
|
|
|
|
my $last_key; |
|
490
|
|
|
|
|
|
|
foreach my $node ( $xpc->findnodes( ".//s3:Contents" ) ) { |
|
491
|
|
|
|
|
|
|
my $key = $xpc->findvalue( ".//s3:Key", $node ); |
|
492
|
|
|
|
|
|
|
$last_key = $key; |
|
493
|
|
|
|
|
|
|
|
|
494
|
|
|
|
|
|
|
$key =~ s/^\Q$self->{prefix}\E// if defined $self->{prefix}; |
|
495
|
|
|
|
|
|
|
push @keys, { |
|
496
|
|
|
|
|
|
|
key => $key, |
|
497
|
|
|
|
|
|
|
last_modified => $xpc->findvalue( ".//s3:LastModified", $node ), |
|
498
|
|
|
|
|
|
|
etag => $xpc->findvalue( ".//s3:ETag", $node ), |
|
499
|
|
|
|
|
|
|
size => $xpc->findvalue( ".//s3:Size", $node ), |
|
500
|
|
|
|
|
|
|
storage_class => $xpc->findvalue( ".//s3:StorageClass", $node ), |
|
501
|
|
|
|
|
|
|
}; |
|
502
|
|
|
|
|
|
|
} |
|
503
|
|
|
|
|
|
|
|
|
504
|
|
|
|
|
|
|
foreach my $node ( $xpc->findnodes( ".//s3:CommonPrefixes" ) ) { |
|
505
|
|
|
|
|
|
|
my $key = $xpc->findvalue( ".//s3:Prefix", $node ); |
|
506
|
|
|
|
|
|
|
|
|
507
|
|
|
|
|
|
|
$key =~ s/^\Q$self->{prefix}\E// if defined $self->{prefix}; |
|
508
|
|
|
|
|
|
|
push @prefixes, $key; |
|
509
|
|
|
|
|
|
|
} |
|
510
|
|
|
|
|
|
|
|
|
511
|
|
|
|
|
|
|
if( $xpc->findvalue( ".//s3:IsTruncated" ) eq "true" ) { |
|
512
|
|
|
|
|
|
|
return Future->wrap( $last_key ); |
|
513
|
|
|
|
|
|
|
} |
|
514
|
|
|
|
|
|
|
return Future->new->done; |
|
515
|
|
|
|
|
|
|
}); |
|
516
|
|
|
|
|
|
|
} while => sub { |
|
517
|
|
|
|
|
|
|
my $f = shift; |
|
518
|
|
|
|
|
|
|
!$f->failure and $f->get }; |
|
519
|
|
|
|
|
|
|
|
|
520
|
|
|
|
|
|
|
$f->then( sub { |
|
521
|
|
|
|
|
|
|
return Future->wrap( \@keys, \@prefixes ); |
|
522
|
|
|
|
|
|
|
}); |
|
523
|
|
|
|
|
|
|
} |
|
524
|
|
|
|
|
|
|
|
|
525
|
|
|
|
|
|
|
sub list_bucket |
|
526
|
|
|
|
|
|
|
{ |
|
527
|
|
|
|
|
|
|
my $self = shift; |
|
528
|
|
|
|
|
|
|
$self->_retry( "_list_bucket", @_ ); |
|
529
|
|
|
|
|
|
|
} |
|
530
|
|
|
|
|
|
|
|
|
531
|
|
|
|
|
|
|
=head2 $s3->get_object( %args ) ==> ( $value, $response, $meta ) |
|
532
|
|
|
|
|
|
|
|
|
533
|
|
|
|
|
|
|
Requests the value of a key from a bucket. |
|
534
|
|
|
|
|
|
|
|
|
535
|
|
|
|
|
|
|
Takes the following named arguments: |
|
536
|
|
|
|
|
|
|
|
|
537
|
|
|
|
|
|
|
=over 8 |
|
538
|
|
|
|
|
|
|
|
|
539
|
|
|
|
|
|
|
=item bucket => STR |
|
540
|
|
|
|
|
|
|
|
|
541
|
|
|
|
|
|
|
The name of the S3 bucket to query |
|
542
|
|
|
|
|
|
|
|
|
543
|
|
|
|
|
|
|
=item key => STR |
|
544
|
|
|
|
|
|
|
|
|
545
|
|
|
|
|
|
|
The name of the key to query |
|
546
|
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
=item on_chunk => CODE |
|
548
|
|
|
|
|
|
|
|
|
549
|
|
|
|
|
|
|
Optional. If supplied, this code will be invoked repeatedly on receipt of more |
|
550
|
|
|
|
|
|
|
bytes of the key's value. It will be passed the L object |
|
551
|
|
|
|
|
|
|
received in reply to the request, and a byte string containing more bytes of |
|
552
|
|
|
|
|
|
|
the value. Its return value is not important. |
|
553
|
|
|
|
|
|
|
|
|
554
|
|
|
|
|
|
|
$on_chunk->( $header, $bytes ) |
|
555
|
|
|
|
|
|
|
|
|
556
|
|
|
|
|
|
|
If this is supplied then the key's value will not be accumulated, and the |
|
557
|
|
|
|
|
|
|
final result of the Future will be an empty string. |
|
558
|
|
|
|
|
|
|
|
|
559
|
|
|
|
|
|
|
=item byte_range => STRING |
|
560
|
|
|
|
|
|
|
|
|
561
|
|
|
|
|
|
|
Optional. If supplied, is used to set the C request header with |
|
562
|
|
|
|
|
|
|
C as the units. This gives a range of bytes of the object to fetch, |
|
563
|
|
|
|
|
|
|
rather than fetching the entire content. The value must be as specified by |
|
564
|
|
|
|
|
|
|
HTTP/1.1; i.e. a comma-separated list of ranges, where each range specifies a |
|
565
|
|
|
|
|
|
|
start and optionally an inclusive stop byte index, separated by hypens. |
|
566
|
|
|
|
|
|
|
|
|
567
|
|
|
|
|
|
|
=item if_match => STRING |
|
568
|
|
|
|
|
|
|
|
|
569
|
|
|
|
|
|
|
Optional. If supplied, is used to set the C request header to the |
|
570
|
|
|
|
|
|
|
given string, which should be an entity etag. If the requested object no |
|
571
|
|
|
|
|
|
|
longer has this etag, the request will fail with an C failure whose |
|
572
|
|
|
|
|
|
|
response code is 412. |
|
573
|
|
|
|
|
|
|
|
|
574
|
|
|
|
|
|
|
=back |
|
575
|
|
|
|
|
|
|
|
|
576
|
|
|
|
|
|
|
The Future will return a byte string containing the key's value, the |
|
577
|
|
|
|
|
|
|
L that was received, and a hash reference containing any of |
|
578
|
|
|
|
|
|
|
the metadata fields, if found in the response. If an C code |
|
579
|
|
|
|
|
|
|
reference is passed, the C<$value> string will be empty. |
|
580
|
|
|
|
|
|
|
|
|
581
|
|
|
|
|
|
|
If the entire content of the object is requested (i.e. if C is not |
|
582
|
|
|
|
|
|
|
supplied) then stall timeout failures will be handled specially. If a stall |
|
583
|
|
|
|
|
|
|
timeout happens while receiving the content, the request will be retried using |
|
584
|
|
|
|
|
|
|
the C header to resume from progress so far. This will be repeated |
|
585
|
|
|
|
|
|
|
while every attempt still makes progress, and such resumes will not be counted |
|
586
|
|
|
|
|
|
|
as part of the normal retry count. The resume request also uses C to |
|
587
|
|
|
|
|
|
|
ensure it only resumes the resource with matching ETag. If a resume request |
|
588
|
|
|
|
|
|
|
fails for some reason (either because the ETag no longer matches or something |
|
589
|
|
|
|
|
|
|
else) then this error is ignored, and the original stall timeout failure is |
|
590
|
|
|
|
|
|
|
returned. |
|
591
|
|
|
|
|
|
|
|
|
592
|
|
|
|
|
|
|
=cut |
|
593
|
|
|
|
|
|
|
|
|
594
|
|
|
|
|
|
|
sub _head_then_get_object |
|
595
|
|
|
|
|
|
|
{ |
|
596
|
|
|
|
|
|
|
my $self = shift; |
|
597
|
|
|
|
|
|
|
my %args = @_; |
|
598
|
|
|
|
|
|
|
|
|
599
|
|
|
|
|
|
|
my $if_match = $args{if_match}; |
|
600
|
|
|
|
|
|
|
my $byte_range = $args{byte_range}; |
|
601
|
|
|
|
|
|
|
|
|
602
|
|
|
|
|
|
|
# TODO: This doesn't handle retries correctly |
|
603
|
|
|
|
|
|
|
# But that said neither does the rest of this module, wrt: on_chunk streaming |
|
604
|
|
|
|
|
|
|
|
|
605
|
|
|
|
|
|
|
my $on_chunk = delete $args{on_chunk}; |
|
606
|
|
|
|
|
|
|
|
|
607
|
|
|
|
|
|
|
my $header; |
|
608
|
|
|
|
|
|
|
my $head_future = $self->loop->new_future; |
|
609
|
|
|
|
|
|
|
my $value_future; |
|
610
|
|
|
|
|
|
|
my $value_len = 0; |
|
611
|
|
|
|
|
|
|
my $stall_failure_f; |
|
612
|
|
|
|
|
|
|
|
|
613
|
|
|
|
|
|
|
my $resume_on_stall = 1; |
|
614
|
|
|
|
|
|
|
|
|
615
|
|
|
|
|
|
|
# TODO: Right now I can't be bothered to write the logic required to update |
|
616
|
|
|
|
|
|
|
# the user-requested byte_range (which may in complex cases contain multiple |
|
617
|
|
|
|
|
|
|
# discontinuous ranges) after a stall to resume it. This probably could be |
|
618
|
|
|
|
|
|
|
# done at some stage. |
|
619
|
|
|
|
|
|
|
$resume_on_stall = 0 if defined $byte_range; |
|
620
|
|
|
|
|
|
|
|
|
621
|
|
|
|
|
|
|
( try_repeat { |
|
622
|
|
|
|
|
|
|
if( my $pos = $value_len ) { |
|
623
|
|
|
|
|
|
|
$byte_range = "$pos-"; |
|
624
|
|
|
|
|
|
|
} |
|
625
|
|
|
|
|
|
|
|
|
626
|
|
|
|
|
|
|
my $request = $self->_make_request( |
|
627
|
|
|
|
|
|
|
method => $args{method}, |
|
628
|
|
|
|
|
|
|
bucket => $args{bucket}, |
|
629
|
|
|
|
|
|
|
path => $args{key}, |
|
630
|
|
|
|
|
|
|
headers => { |
|
631
|
|
|
|
|
|
|
( defined $if_match ? ( "If-Match" => $if_match ) : () ), |
|
632
|
|
|
|
|
|
|
( defined $byte_range ? ( Range => "bytes=$byte_range" ) : () ), |
|
633
|
|
|
|
|
|
|
}, |
|
634
|
|
|
|
|
|
|
content => "", |
|
635
|
|
|
|
|
|
|
); |
|
636
|
|
|
|
|
|
|
|
|
637
|
|
|
|
|
|
|
$self->_do_request( $request, |
|
638
|
|
|
|
|
|
|
timeout => $args{timeout}, |
|
639
|
|
|
|
|
|
|
stall_timeout => $args{stall_timeout} // $self->{stall_timeout}, |
|
640
|
|
|
|
|
|
|
on_header => sub { |
|
641
|
|
|
|
|
|
|
my ( $this_header ) = @_; |
|
642
|
|
|
|
|
|
|
my $code = $this_header->code; |
|
643
|
|
|
|
|
|
|
|
|
644
|
|
|
|
|
|
|
if( $head_future->is_cancelled or $code !~ m/^2/ ) { |
|
645
|
|
|
|
|
|
|
# Just eat the body on cancellation or if it's not a 2xx |
|
646
|
|
|
|
|
|
|
# For failures this will cause ->on_fail to occur and fail the |
|
647
|
|
|
|
|
|
|
# $head_future |
|
648
|
|
|
|
|
|
|
return sub { |
|
649
|
|
|
|
|
|
|
return if @_; |
|
650
|
|
|
|
|
|
|
return $this_header; |
|
651
|
|
|
|
|
|
|
}; |
|
652
|
|
|
|
|
|
|
} |
|
653
|
|
|
|
|
|
|
|
|
654
|
|
|
|
|
|
|
my %meta; |
|
655
|
|
|
|
|
|
|
$this_header->scan( sub { |
|
656
|
|
|
|
|
|
|
$_[0] =~ m/^X-Amz-Meta-(.*)$/i and $meta{$1} = $_[1]; |
|
657
|
|
|
|
|
|
|
}); |
|
658
|
|
|
|
|
|
|
|
|
659
|
|
|
|
|
|
|
if( !$value_future ) { |
|
660
|
|
|
|
|
|
|
# First response |
|
661
|
|
|
|
|
|
|
$header = $this_header; |
|
662
|
|
|
|
|
|
|
# If we're going to retry this, ensure we only request this exact ETag |
|
663
|
|
|
|
|
|
|
$if_match ||= $header->header( "ETag" ); |
|
664
|
|
|
|
|
|
|
|
|
665
|
|
|
|
|
|
|
$value_future = $head_future->new; |
|
666
|
|
|
|
|
|
|
$head_future->done( $value_future, $header, \%meta ); |
|
667
|
|
|
|
|
|
|
} |
|
668
|
|
|
|
|
|
|
|
|
669
|
|
|
|
|
|
|
return sub { |
|
670
|
|
|
|
|
|
|
# Code here could be 200 (OK), 206 (Partial Content) or 204 (No Content) |
|
671
|
|
|
|
|
|
|
return $this_header if $code == 204; |
|
672
|
|
|
|
|
|
|
|
|
673
|
|
|
|
|
|
|
if( @_ ) { |
|
674
|
|
|
|
|
|
|
$value_len += length $_[0]; |
|
675
|
|
|
|
|
|
|
if( $on_chunk ) { |
|
676
|
|
|
|
|
|
|
$on_chunk->( $header, @_ ); |
|
677
|
|
|
|
|
|
|
} |
|
678
|
|
|
|
|
|
|
else { |
|
679
|
|
|
|
|
|
|
$header->add_content( $_[0] ); |
|
680
|
|
|
|
|
|
|
} |
|
681
|
|
|
|
|
|
|
return; |
|
682
|
|
|
|
|
|
|
} |
|
683
|
|
|
|
|
|
|
return $header; # with no body content |
|
684
|
|
|
|
|
|
|
}; |
|
685
|
|
|
|
|
|
|
} |
|
686
|
|
|
|
|
|
|
); |
|
687
|
|
|
|
|
|
|
} while => sub { |
|
688
|
|
|
|
|
|
|
my ( $prev_f ) = @_; |
|
689
|
|
|
|
|
|
|
# repeat while it keeps failing with a stall timeout |
|
690
|
|
|
|
|
|
|
return 0 if !$resume_on_stall or !$prev_f->failure; |
|
691
|
|
|
|
|
|
|
|
|
692
|
|
|
|
|
|
|
my $op = ( $prev_f->failure )[1] // ""; |
|
693
|
|
|
|
|
|
|
return 0 if $op ne "stall_timeout"; |
|
694
|
|
|
|
|
|
|
|
|
695
|
|
|
|
|
|
|
$stall_failure_f ||= $prev_f; |
|
696
|
|
|
|
|
|
|
return 1; |
|
697
|
|
|
|
|
|
|
} )->on_done( sub { |
|
698
|
|
|
|
|
|
|
my ( $response ) = @_; |
|
699
|
|
|
|
|
|
|
return if $head_future->is_cancelled; |
|
700
|
|
|
|
|
|
|
|
|
701
|
|
|
|
|
|
|
$value_future->done( $response->content, ( $head_future->get )[1,2] ); |
|
702
|
|
|
|
|
|
|
})->on_fail( sub { |
|
703
|
|
|
|
|
|
|
my $f = $value_future || $head_future; |
|
704
|
|
|
|
|
|
|
# If we have a $stall_failure_f it means we must have attempted a resume |
|
705
|
|
|
|
|
|
|
# after a stall timeout, then failed for a different reason. Ignore that |
|
706
|
|
|
|
|
|
|
# second reason and just pretend to the caller that we stalled. |
|
707
|
|
|
|
|
|
|
$f->fail( $stall_failure_f ? $stall_failure_f->failure : @_ ) if !$f->is_cancelled; |
|
708
|
|
|
|
|
|
|
}); |
|
709
|
|
|
|
|
|
|
|
|
710
|
|
|
|
|
|
|
return $head_future; |
|
711
|
|
|
|
|
|
|
} |
|
712
|
|
|
|
|
|
|
|
|
713
|
|
|
|
|
|
|
sub get_object |
|
714
|
|
|
|
|
|
|
{ |
|
715
|
|
|
|
|
|
|
my $self = shift; |
|
716
|
|
|
|
|
|
|
my %args = @_; |
|
717
|
|
|
|
|
|
|
|
|
718
|
|
|
|
|
|
|
$self->_retry( sub { |
|
719
|
|
|
|
|
|
|
$self->_head_then_get_object( %args, method => "GET" ) |
|
720
|
|
|
|
|
|
|
->then( sub { my ( $value_f ) = @_; $value_f }); # wait on the value |
|
721
|
|
|
|
|
|
|
}); |
|
722
|
|
|
|
|
|
|
} |
|
723
|
|
|
|
|
|
|
|
|
724
|
|
|
|
|
|
|
=head2 $s3->head_object( %args ) ==> ( $response, $meta ) |
|
725
|
|
|
|
|
|
|
|
|
726
|
|
|
|
|
|
|
Requests the value metadata of a key from a bucket. This is similar to the |
|
727
|
|
|
|
|
|
|
C method, but uses the C HTTP verb instead of C. |
|
728
|
|
|
|
|
|
|
|
|
729
|
|
|
|
|
|
|
Takes the same named arguments as C, but will ignore an |
|
730
|
|
|
|
|
|
|
C callback, if provided. |
|
731
|
|
|
|
|
|
|
|
|
732
|
|
|
|
|
|
|
The Future will return the L object and metadata hash |
|
733
|
|
|
|
|
|
|
reference, without the content string (as no content is returned to a C |
|
734
|
|
|
|
|
|
|
request). |
|
735
|
|
|
|
|
|
|
|
|
736
|
|
|
|
|
|
|
=cut |
|
737
|
|
|
|
|
|
|
|
|
738
|
|
|
|
|
|
|
sub head_object |
|
739
|
|
|
|
|
|
|
{ |
|
740
|
|
|
|
|
|
|
my $self = shift; |
|
741
|
|
|
|
|
|
|
my %args = @_; |
|
742
|
|
|
|
|
|
|
|
|
743
|
|
|
|
|
|
|
$self->_retry( sub { |
|
744
|
|
|
|
|
|
|
$self->_head_then_get_object( %args, method => "HEAD" ) |
|
745
|
|
|
|
|
|
|
->then( sub { my ( $value_f ) = @_; $value_f }) # wait on the empty body |
|
746
|
|
|
|
|
|
|
->then( sub { shift; Future->wrap( @_ ) }); # remove (empty) value |
|
747
|
|
|
|
|
|
|
}); |
|
748
|
|
|
|
|
|
|
} |
|
749
|
|
|
|
|
|
|
|
|
750
|
|
|
|
|
|
|
=head2 $s3->head_then_get_object( %args ) ==> ( $value_f, $response, $meta ) |
|
751
|
|
|
|
|
|
|
|
|
752
|
|
|
|
|
|
|
Performs a C operation similar to C, but allows access to the |
|
753
|
|
|
|
|
|
|
metadata header before the body content is complete. |
|
754
|
|
|
|
|
|
|
|
|
755
|
|
|
|
|
|
|
Takes the same named arguments as C. |
|
756
|
|
|
|
|
|
|
|
|
757
|
|
|
|
|
|
|
The returned Future completes as soon as the metadata header has been received |
|
758
|
|
|
|
|
|
|
and yields a second future (the body future), the L and a hash |
|
759
|
|
|
|
|
|
|
reference containing the metadata fields. The body future will eventually |
|
760
|
|
|
|
|
|
|
yield the actual body, along with another copy of the response and metadata |
|
761
|
|
|
|
|
|
|
hash reference. |
|
762
|
|
|
|
|
|
|
|
|
763
|
|
|
|
|
|
|
$value_f ==> $value, $response, $meta |
|
764
|
|
|
|
|
|
|
|
|
765
|
|
|
|
|
|
|
=cut |
|
766
|
|
|
|
|
|
|
|
|
767
|
|
|
|
|
|
|
sub head_then_get_object |
|
768
|
|
|
|
|
|
|
{ |
|
769
|
|
|
|
|
|
|
my $self = shift; |
|
770
|
|
|
|
|
|
|
$self->_retry( "_head_then_get_object", @_, method => "GET" ); |
|
771
|
|
|
|
|
|
|
} |
|
772
|
|
|
|
|
|
|
|
|
773
|
|
|
|
|
|
|
=head2 $s3->put_object( %args ) ==> ( $etag, $length ) |
|
774
|
|
|
|
|
|
|
|
|
775
|
|
|
|
|
|
|
Sets a new value for a key in the bucket. |
|
776
|
|
|
|
|
|
|
|
|
777
|
|
|
|
|
|
|
Takes the following named arguments: |
|
778
|
|
|
|
|
|
|
|
|
779
|
|
|
|
|
|
|
=over 8 |
|
780
|
|
|
|
|
|
|
|
|
781
|
|
|
|
|
|
|
=item bucket => STRING |
|
782
|
|
|
|
|
|
|
|
|
783
|
|
|
|
|
|
|
The name of the S3 bucket to put the value in |
|
784
|
|
|
|
|
|
|
|
|
785
|
|
|
|
|
|
|
=item key => STRING |
|
786
|
|
|
|
|
|
|
|
|
787
|
|
|
|
|
|
|
The name of the key to put the value in |
|
788
|
|
|
|
|
|
|
|
|
789
|
|
|
|
|
|
|
=item value => STRING |
|
790
|
|
|
|
|
|
|
|
|
791
|
|
|
|
|
|
|
=item value => Future giving STRING |
|
792
|
|
|
|
|
|
|
|
|
793
|
|
|
|
|
|
|
Optional. If provided, gives a byte string as the new value for the key or a |
|
794
|
|
|
|
|
|
|
L which will eventually yield such. |
|
795
|
|
|
|
|
|
|
|
|
796
|
|
|
|
|
|
|
=item value => CODE |
|
797
|
|
|
|
|
|
|
|
|
798
|
|
|
|
|
|
|
=item value_length => INT |
|
799
|
|
|
|
|
|
|
|
|
800
|
|
|
|
|
|
|
Alternative form of C, which is a C reference to a generator |
|
801
|
|
|
|
|
|
|
function. It will be called repeatedly to generate small chunks of content, |
|
802
|
|
|
|
|
|
|
being passed the position and length it should yield. |
|
803
|
|
|
|
|
|
|
|
|
804
|
|
|
|
|
|
|
$chunk = $value->( $pos, $len ) |
|
805
|
|
|
|
|
|
|
|
|
806
|
|
|
|
|
|
|
Typically this can be provided either by a C operation on a larger |
|
807
|
|
|
|
|
|
|
string buffer, or a C and C operation on a filehandle. |
|
808
|
|
|
|
|
|
|
|
|
809
|
|
|
|
|
|
|
In normal operation the function will just be called in a single sweep in |
|
810
|
|
|
|
|
|
|
contiguous regions up to the extent given by C. If however, the |
|
811
|
|
|
|
|
|
|
MD5sum check fails at the end of upload, it will be called again to retry the |
|
812
|
|
|
|
|
|
|
operation. The function must therefore be prepared to be invoked multiple |
|
813
|
|
|
|
|
|
|
times over its range. |
|
814
|
|
|
|
|
|
|
|
|
815
|
|
|
|
|
|
|
=item value => Future giving ( CODE, INT ) |
|
816
|
|
|
|
|
|
|
|
|
817
|
|
|
|
|
|
|
Alternative form of C, in which a C eventually yields the value |
|
818
|
|
|
|
|
|
|
generation C reference and length. The C reference is invoked as |
|
819
|
|
|
|
|
|
|
documented above. |
|
820
|
|
|
|
|
|
|
|
|
821
|
|
|
|
|
|
|
( $gen_value, $value_len ) = $value->get; |
|
822
|
|
|
|
|
|
|
|
|
823
|
|
|
|
|
|
|
$chunk = $gen_value->( $pos, $len ); |
|
824
|
|
|
|
|
|
|
|
|
825
|
|
|
|
|
|
|
=item gen_parts => CODE |
|
826
|
|
|
|
|
|
|
|
|
827
|
|
|
|
|
|
|
Alternative to C in the case of larger values, and implies the use of |
|
828
|
|
|
|
|
|
|
multipart upload. Called repeatedly to generate successive parts of the |
|
829
|
|
|
|
|
|
|
upload. Each time C is called it should return one of the forms of |
|
830
|
|
|
|
|
|
|
C given above; namely, a byte string, a C reference and size |
|
831
|
|
|
|
|
|
|
pair, or a C which will eventually yield either of these forms. |
|
832
|
|
|
|
|
|
|
|
|
833
|
|
|
|
|
|
|
( $value ) = $gen_parts->() |
|
834
|
|
|
|
|
|
|
|
|
835
|
|
|
|
|
|
|
( $gen_value, $value_length ) = $gen_parts->() |
|
836
|
|
|
|
|
|
|
|
|
837
|
|
|
|
|
|
|
( $value_f ) = $gen_parts->(); $value = $value_f->get |
|
838
|
|
|
|
|
|
|
( $gen_value, $value_length ) = $value_f->get |
|
839
|
|
|
|
|
|
|
|
|
840
|
|
|
|
|
|
|
Each case is analogous to the types that the C key can take. |
|
841
|
|
|
|
|
|
|
|
|
842
|
|
|
|
|
|
|
=item meta => HASH |
|
843
|
|
|
|
|
|
|
|
|
844
|
|
|
|
|
|
|
Optional. If provided, gives additional user metadata fields to set on the |
|
845
|
|
|
|
|
|
|
object, using the C fields. |
|
846
|
|
|
|
|
|
|
|
|
847
|
|
|
|
|
|
|
=item timeout => NUM |
|
848
|
|
|
|
|
|
|
|
|
849
|
|
|
|
|
|
|
Optional. For single-part uploads, this sets the C argument to use |
|
850
|
|
|
|
|
|
|
for the actual C request. For multi-part uploads, this argument is |
|
851
|
|
|
|
|
|
|
currently ignored. |
|
852
|
|
|
|
|
|
|
|
|
853
|
|
|
|
|
|
|
=item meta_timeout => NUM |
|
854
|
|
|
|
|
|
|
|
|
855
|
|
|
|
|
|
|
Optional. For multipart uploads, this sets the C argument to use for |
|
856
|
|
|
|
|
|
|
the initiate and complete requests, overriding a configured C. |
|
857
|
|
|
|
|
|
|
Ignored for single-part uploads. |
|
858
|
|
|
|
|
|
|
|
|
859
|
|
|
|
|
|
|
=item part_timeout => NUM |
|
860
|
|
|
|
|
|
|
|
|
861
|
|
|
|
|
|
|
Optional. For multipart uploads, this sets the C argument to use for |
|
862
|
|
|
|
|
|
|
the individual part C requests. Ignored for single-part uploads. |
|
863
|
|
|
|
|
|
|
|
|
864
|
|
|
|
|
|
|
=item on_write => CODE |
|
865
|
|
|
|
|
|
|
|
|
866
|
|
|
|
|
|
|
Optional. If provided, this code will be invoked after each successful |
|
867
|
|
|
|
|
|
|
C call on the underlying filehandle when writing actual file |
|
868
|
|
|
|
|
|
|
content, indicating that the data was at least written as far as the |
|
869
|
|
|
|
|
|
|
kernel. It will be passed the total byte length that has been written for this |
|
870
|
|
|
|
|
|
|
call to C. By the time the call has completed, this will be the |
|
871
|
|
|
|
|
|
|
total written length of the object. |
|
872
|
|
|
|
|
|
|
|
|
873
|
|
|
|
|
|
|
$on_write->( $bytes_written ) |
|
874
|
|
|
|
|
|
|
|
|
875
|
|
|
|
|
|
|
Note that because of retries it is possible this count will decrease, if a |
|
876
|
|
|
|
|
|
|
part has to be retried due to e.g. a failing MD5 checksum. |
|
877
|
|
|
|
|
|
|
|
|
878
|
|
|
|
|
|
|
=item concurrent => INT |
|
879
|
|
|
|
|
|
|
|
|
880
|
|
|
|
|
|
|
Optional. If present, gives the number of parts to upload concurrently. If |
|
881
|
|
|
|
|
|
|
absent, a default of 1 will apply (i.e. no concurrency). |
|
882
|
|
|
|
|
|
|
|
|
883
|
|
|
|
|
|
|
=back |
|
884
|
|
|
|
|
|
|
|
|
885
|
|
|
|
|
|
|
The Future will return a string containing the S3 ETag of the newly-set key, |
|
886
|
|
|
|
|
|
|
and the length of the value in bytes. |
|
887
|
|
|
|
|
|
|
|
|
888
|
|
|
|
|
|
|
For single-part uploads the ETag will be the MD5 sum in hex, surrounded by |
|
889
|
|
|
|
|
|
|
quote marks. For multi-part uploads this is a string in a different form, |
|
890
|
|
|
|
|
|
|
though details of its generation are not specified by S3. |
|
891
|
|
|
|
|
|
|
|
|
892
|
|
|
|
|
|
|
The returned MD5 sum from S3 during upload will be checked against an |
|
893
|
|
|
|
|
|
|
internally-generated MD5 sum of the content that was sent, and an error result |
|
894
|
|
|
|
|
|
|
will be returned if these do not match. |
|
895
|
|
|
|
|
|
|
|
|
896
|
|
|
|
|
|
|
=cut |
|
897
|
|
|
|
|
|
|
|
|
898
|
|
|
|
|
|
|
sub _md5sum_wrap |
|
899
|
|
|
|
|
|
|
{ |
|
900
|
|
|
|
|
|
|
my $content = shift; |
|
901
|
|
|
|
|
|
|
my @args = my ( $md5ctx, $posref, $content_length, $read_size ) = @_; |
|
902
|
|
|
|
|
|
|
|
|
903
|
|
|
|
|
|
|
if( !defined $content or !ref $content ) { |
|
904
|
|
|
|
|
|
|
my $len = $content_length - $$posref; |
|
905
|
|
|
|
|
|
|
if( defined $content ) { |
|
906
|
|
|
|
|
|
|
substr( $content, $len ) = "" if length $content > $len; |
|
907
|
|
|
|
|
|
|
} |
|
908
|
|
|
|
|
|
|
else { |
|
909
|
|
|
|
|
|
|
$content = "\0" x $len; |
|
910
|
|
|
|
|
|
|
} |
|
911
|
|
|
|
|
|
|
|
|
912
|
|
|
|
|
|
|
$md5ctx->add( $content ); |
|
913
|
|
|
|
|
|
|
$$posref += length $content; |
|
914
|
|
|
|
|
|
|
|
|
915
|
|
|
|
|
|
|
return $content; |
|
916
|
|
|
|
|
|
|
} |
|
917
|
|
|
|
|
|
|
elsif( ref $content eq "CODE" ) { |
|
918
|
|
|
|
|
|
|
return sub { |
|
919
|
|
|
|
|
|
|
return undef if $$posref >= $content_length; |
|
920
|
|
|
|
|
|
|
|
|
921
|
|
|
|
|
|
|
my $len = $content_length - $$posref; |
|
922
|
|
|
|
|
|
|
$len = $read_size if $len > $read_size; |
|
923
|
|
|
|
|
|
|
|
|
924
|
|
|
|
|
|
|
my $chunk = $content->( $$posref, $len ); |
|
925
|
|
|
|
|
|
|
return _md5sum_wrap( $chunk, @args ); |
|
926
|
|
|
|
|
|
|
} |
|
927
|
|
|
|
|
|
|
} |
|
928
|
|
|
|
|
|
|
elsif( blessed $content and $content->isa( "Future" ) ) { |
|
929
|
|
|
|
|
|
|
return $content->transform( |
|
930
|
|
|
|
|
|
|
done => sub { |
|
931
|
|
|
|
|
|
|
my ( $chunk ) = @_; |
|
932
|
|
|
|
|
|
|
return _md5sum_wrap( $chunk, @args ); |
|
933
|
|
|
|
|
|
|
}, |
|
934
|
|
|
|
|
|
|
); |
|
935
|
|
|
|
|
|
|
} |
|
936
|
|
|
|
|
|
|
else { |
|
937
|
|
|
|
|
|
|
die "TODO: md5sum wrap ref $content"; |
|
938
|
|
|
|
|
|
|
} |
|
939
|
|
|
|
|
|
|
} |
|
940
|
|
|
|
|
|
|
|
|
941
|
|
|
|
|
|
|
sub _put_object |
|
942
|
|
|
|
|
|
|
{ |
|
943
|
|
|
|
|
|
|
my $self = shift; |
|
944
|
|
|
|
|
|
|
my %args = @_; |
|
945
|
|
|
|
|
|
|
|
|
946
|
|
|
|
|
|
|
my $md5ctx = Digest::MD5->new; |
|
947
|
|
|
|
|
|
|
my $on_write = $args{on_write}; |
|
948
|
|
|
|
|
|
|
my $pos = 0; |
|
949
|
|
|
|
|
|
|
|
|
950
|
|
|
|
|
|
|
# Make $content definitely a Future |
|
951
|
|
|
|
|
|
|
Future->wrap( delete $args{content} )->then( sub { |
|
952
|
|
|
|
|
|
|
my ( $content ) = @_; |
|
953
|
|
|
|
|
|
|
my $content_length = @_ > 1 ? $_[1] : |
|
954
|
|
|
|
|
|
|
ref $content ? $args{content_length} : |
|
955
|
|
|
|
|
|
|
length $content; |
|
956
|
|
|
|
|
|
|
defined $content_length or die "TODO: referential content $content needs length"; |
|
957
|
|
|
|
|
|
|
|
|
958
|
|
|
|
|
|
|
my $request = $self->_make_request( |
|
959
|
|
|
|
|
|
|
%args, |
|
960
|
|
|
|
|
|
|
method => "PUT", |
|
961
|
|
|
|
|
|
|
content => "", # Doesn't matter, it'll be ignored |
|
962
|
|
|
|
|
|
|
); |
|
963
|
|
|
|
|
|
|
|
|
964
|
|
|
|
|
|
|
$request->content_length( $content_length ); |
|
965
|
|
|
|
|
|
|
$request->content( "" ); |
|
966
|
|
|
|
|
|
|
|
|
967
|
|
|
|
|
|
|
$self->_do_request( $request, |
|
968
|
|
|
|
|
|
|
timeout => $args{timeout}, |
|
969
|
|
|
|
|
|
|
stall_timeout => $args{stall_timeout} // $self->{stall_timeout}, |
|
970
|
|
|
|
|
|
|
expect_continue => 1, |
|
971
|
|
|
|
|
|
|
request_body => _md5sum_wrap( $content, $md5ctx, \$pos, $content_length, $self->{read_size} ), |
|
972
|
|
|
|
|
|
|
( $on_write ? ( on_body_write => $on_write ) : () ), |
|
973
|
|
|
|
|
|
|
); |
|
974
|
|
|
|
|
|
|
})->then( sub { |
|
975
|
|
|
|
|
|
|
my $resp = shift; |
|
976
|
|
|
|
|
|
|
|
|
977
|
|
|
|
|
|
|
defined( my $etag = $resp->header( "ETag" ) ) or |
|
978
|
|
|
|
|
|
|
return Future->new->die( "Response did not provide an ETag header" ); |
|
979
|
|
|
|
|
|
|
|
|
980
|
|
|
|
|
|
|
# Amazon S3 currently documents that the returned ETag header will be |
|
981
|
|
|
|
|
|
|
# the MD5 hash of the content, surrounded in quote marks. We'd better |
|
982
|
|
|
|
|
|
|
# hope this continues to be true... :/ |
|
983
|
|
|
|
|
|
|
my ( $got_md5 ) = lc($etag) =~ m/^"([0-9a-f]{32})"$/ or |
|
984
|
|
|
|
|
|
|
return Future->new->die( "Returned ETag ($etag) does not look like an MD5 sum", $resp ); |
|
985
|
|
|
|
|
|
|
|
|
986
|
|
|
|
|
|
|
my $expect_md5 = lc($md5ctx->hexdigest); |
|
987
|
|
|
|
|
|
|
|
|
988
|
|
|
|
|
|
|
if( $got_md5 ne $expect_md5 ) { |
|
989
|
|
|
|
|
|
|
return Future->new->die( "Returned MD5 hash ($got_md5) did not match expected ($expect_md5)", $resp ); |
|
990
|
|
|
|
|
|
|
} |
|
991
|
|
|
|
|
|
|
|
|
992
|
|
|
|
|
|
|
return Future->wrap( $etag, $pos ); |
|
993
|
|
|
|
|
|
|
}); |
|
994
|
|
|
|
|
|
|
} |
|
995
|
|
|
|
|
|
|
|
|
996
|
|
|
|
|
|
|
sub _initiate_multipart_upload |
|
997
|
|
|
|
|
|
|
{ |
|
998
|
|
|
|
|
|
|
my $self = shift; |
|
999
|
|
|
|
|
|
|
my %args = @_; |
|
1000
|
|
|
|
|
|
|
|
|
1001
|
|
|
|
|
|
|
my $req = $self->_make_request( |
|
1002
|
|
|
|
|
|
|
method => "POST", |
|
1003
|
|
|
|
|
|
|
bucket => $args{bucket}, |
|
1004
|
|
|
|
|
|
|
path => "$args{key}?uploads", |
|
1005
|
|
|
|
|
|
|
content => "", |
|
1006
|
|
|
|
|
|
|
meta => $args{meta}, |
|
1007
|
|
|
|
|
|
|
); |
|
1008
|
|
|
|
|
|
|
|
|
1009
|
|
|
|
|
|
|
$self->_do_request_xpc( $req, |
|
1010
|
|
|
|
|
|
|
timeout => $args{timeout}, |
|
1011
|
|
|
|
|
|
|
stall_timeout => $args{stall_timeout} // $self->{stall_timeout}, |
|
1012
|
|
|
|
|
|
|
)->then( sub { |
|
1013
|
|
|
|
|
|
|
my $xpc = shift; |
|
1014
|
|
|
|
|
|
|
|
|
1015
|
|
|
|
|
|
|
my $id = $xpc->findvalue( ".//s3:InitiateMultipartUploadResult/s3:UploadId" ); |
|
1016
|
|
|
|
|
|
|
return Future->wrap( $id ); |
|
1017
|
|
|
|
|
|
|
}); |
|
1018
|
|
|
|
|
|
|
} |
|
1019
|
|
|
|
|
|
|
|
|
1020
|
|
|
|
|
|
|
sub _complete_multipart_upload |
|
1021
|
|
|
|
|
|
|
{ |
|
1022
|
|
|
|
|
|
|
my $self = shift; |
|
1023
|
|
|
|
|
|
|
my %args = @_; |
|
1024
|
|
|
|
|
|
|
|
|
1025
|
|
|
|
|
|
|
my $req = $self->_make_request( |
|
1026
|
|
|
|
|
|
|
method => "POST", |
|
1027
|
|
|
|
|
|
|
bucket => $args{bucket}, |
|
1028
|
|
|
|
|
|
|
path => $args{key}, |
|
1029
|
|
|
|
|
|
|
content => $args{content}, |
|
1030
|
|
|
|
|
|
|
query_params => { |
|
1031
|
|
|
|
|
|
|
uploadId => $args{id}, |
|
1032
|
|
|
|
|
|
|
}, |
|
1033
|
|
|
|
|
|
|
headers => { |
|
1034
|
|
|
|
|
|
|
"Content-Type" => "application/xml", |
|
1035
|
|
|
|
|
|
|
"Content-MD5" => encode_base64( md5( $args{content} ), "" ), |
|
1036
|
|
|
|
|
|
|
}, |
|
1037
|
|
|
|
|
|
|
); |
|
1038
|
|
|
|
|
|
|
|
|
1039
|
|
|
|
|
|
|
$self->_do_request_xpc( $req, |
|
1040
|
|
|
|
|
|
|
timeout => $args{timeout}, |
|
1041
|
|
|
|
|
|
|
stall_timeout => $args{stall_timeout} // $self->{stall_timeout}, |
|
1042
|
|
|
|
|
|
|
)->then( sub { |
|
1043
|
|
|
|
|
|
|
my $xpc = shift; |
|
1044
|
|
|
|
|
|
|
|
|
1045
|
|
|
|
|
|
|
my $etag = $xpc->findvalue( ".//s3:CompleteMultipartUploadResult/s3:ETag" ); |
|
1046
|
|
|
|
|
|
|
return Future->wrap( $etag ); |
|
1047
|
|
|
|
|
|
|
}); |
|
1048
|
|
|
|
|
|
|
} |
|
1049
|
|
|
|
|
|
|
|
|
1050
|
|
|
|
|
|
|
sub _put_object_multipart |
|
1051
|
|
|
|
|
|
|
{ |
|
1052
|
|
|
|
|
|
|
my $self = shift; |
|
1053
|
|
|
|
|
|
|
my ( $gen_parts, %args ) = @_; |
|
1054
|
|
|
|
|
|
|
|
|
1055
|
|
|
|
|
|
|
my $on_write = $args{on_write}; |
|
1056
|
|
|
|
|
|
|
|
|
1057
|
|
|
|
|
|
|
my $id; |
|
1058
|
|
|
|
|
|
|
|
|
1059
|
|
|
|
|
|
|
my $written_committed = 0; # bytes written in committed parts |
|
1060
|
|
|
|
|
|
|
my %written_tentative; # {$part_num} => bytes written so far in this part |
|
1061
|
|
|
|
|
|
|
|
|
1062
|
|
|
|
|
|
|
$self->_retry( "_initiate_multipart_upload", |
|
1063
|
|
|
|
|
|
|
timeout => $args{meta_timeout} // $self->{timeout}, |
|
1064
|
|
|
|
|
|
|
%args |
|
1065
|
|
|
|
|
|
|
)->then( sub { |
|
1066
|
|
|
|
|
|
|
( $id ) = @_; |
|
1067
|
|
|
|
|
|
|
|
|
1068
|
|
|
|
|
|
|
my $part_num = 0; |
|
1069
|
|
|
|
|
|
|
( fmap1 { |
|
1070
|
|
|
|
|
|
|
my ( $part_num, $content, %moreargs ) = @{$_[0]}; |
|
1071
|
|
|
|
|
|
|
|
|
1072
|
|
|
|
|
|
|
$self->_retry( "_put_object", |
|
1073
|
|
|
|
|
|
|
bucket => $args{bucket}, |
|
1074
|
|
|
|
|
|
|
path => $args{key}, |
|
1075
|
|
|
|
|
|
|
content => $content, |
|
1076
|
|
|
|
|
|
|
query_params => { |
|
1077
|
|
|
|
|
|
|
partNumber => $part_num, |
|
1078
|
|
|
|
|
|
|
uploadId => $id, |
|
1079
|
|
|
|
|
|
|
}, |
|
1080
|
|
|
|
|
|
|
timeout => $args{part_timeout}, |
|
1081
|
|
|
|
|
|
|
( $on_write ? |
|
1082
|
|
|
|
|
|
|
( on_write => sub { |
|
1083
|
|
|
|
|
|
|
$written_tentative{$part_num} = $_[0]; |
|
1084
|
|
|
|
|
|
|
$on_write->( $written_committed + sum values %written_tentative ); |
|
1085
|
|
|
|
|
|
|
} ) : () ), |
|
1086
|
|
|
|
|
|
|
%moreargs, |
|
1087
|
|
|
|
|
|
|
)->then( sub { |
|
1088
|
|
|
|
|
|
|
my ( $etag, $len ) = @_; |
|
1089
|
|
|
|
|
|
|
$written_committed += $len; |
|
1090
|
|
|
|
|
|
|
delete $written_tentative{$part_num}; |
|
1091
|
|
|
|
|
|
|
return Future->wrap( [ $part_num, $etag ] ); |
|
1092
|
|
|
|
|
|
|
}); |
|
1093
|
|
|
|
|
|
|
} generate => sub { |
|
1094
|
|
|
|
|
|
|
my @content = $gen_parts->() or return; |
|
1095
|
|
|
|
|
|
|
$part_num++; |
|
1096
|
|
|
|
|
|
|
return [ $part_num, $content[0] ] if @content == 1; |
|
1097
|
|
|
|
|
|
|
return [ $part_num, $content[0], content_length => $content[1] ] if @content == 2 and ref $content[0] eq "CODE"; |
|
1098
|
|
|
|
|
|
|
# It's possible that $content[0] is a 100MiByte string. Best not to |
|
1099
|
|
|
|
|
|
|
# interpolate that into $@ or we'll risk OOM |
|
1100
|
|
|
|
|
|
|
die "Unsure what to do with gen_part result " . |
|
1101
|
|
|
|
|
|
|
join( ", ", map { ref $_ ? $_ : "<".length($_)." bytes>" } @content ); |
|
1102
|
|
|
|
|
|
|
}, concurrent => $args{concurrent} // $self->{put_concurrent} ); |
|
1103
|
|
|
|
|
|
|
})->then( sub { |
|
1104
|
|
|
|
|
|
|
my @etags = @_; |
|
1105
|
|
|
|
|
|
|
|
|
1106
|
|
|
|
|
|
|
my $doc = XML::LibXML::Document->new( "1.0", "UTF-8" ); |
|
1107
|
|
|
|
|
|
|
$doc->addChild( my $root = $doc->createElement( "CompleteMultipartUpload" ) ); |
|
1108
|
|
|
|
|
|
|
|
|
1109
|
|
|
|
|
|
|
#add content |
|
1110
|
|
|
|
|
|
|
foreach ( @etags ) { |
|
1111
|
|
|
|
|
|
|
my ( $part_num, $etag ) = @$_; |
|
1112
|
|
|
|
|
|
|
|
|
1113
|
|
|
|
|
|
|
$root->addChild( my $part = $doc->createElement('Part') ); |
|
1114
|
|
|
|
|
|
|
$part->appendTextChild( PartNumber => $part_num ); |
|
1115
|
|
|
|
|
|
|
$part->appendTextChild( ETag => $etag ); |
|
1116
|
|
|
|
|
|
|
} |
|
1117
|
|
|
|
|
|
|
|
|
1118
|
|
|
|
|
|
|
$self->_retry( "_complete_multipart_upload", |
|
1119
|
|
|
|
|
|
|
%args, |
|
1120
|
|
|
|
|
|
|
id => $id, |
|
1121
|
|
|
|
|
|
|
content => $doc->toString, |
|
1122
|
|
|
|
|
|
|
timeout => $args{meta_timeout} // $self->{timeout}, |
|
1123
|
|
|
|
|
|
|
)->then( sub { |
|
1124
|
|
|
|
|
|
|
my ( $etag ) = @_; |
|
1125
|
|
|
|
|
|
|
return Future->wrap( $etag, $written_committed ); |
|
1126
|
|
|
|
|
|
|
}); |
|
1127
|
|
|
|
|
|
|
}); |
|
1128
|
|
|
|
|
|
|
} |
|
1129
|
|
|
|
|
|
|
|
|
1130
|
|
|
|
|
|
|
sub put_object |
|
1131
|
|
|
|
|
|
|
{ |
|
1132
|
|
|
|
|
|
|
my $self = shift; |
|
1133
|
|
|
|
|
|
|
my %args = @_; |
|
1134
|
|
|
|
|
|
|
|
|
1135
|
|
|
|
|
|
|
my $gen_parts; |
|
1136
|
|
|
|
|
|
|
if( $gen_parts = delete $args{gen_parts} ) { |
|
1137
|
|
|
|
|
|
|
# OK |
|
1138
|
|
|
|
|
|
|
} |
|
1139
|
|
|
|
|
|
|
else { |
|
1140
|
|
|
|
|
|
|
my $content_length = $args{value_length} // length $args{value}; |
|
1141
|
|
|
|
|
|
|
|
|
1142
|
|
|
|
|
|
|
my $part_size = $self->{part_size}; |
|
1143
|
|
|
|
|
|
|
|
|
1144
|
|
|
|
|
|
|
if( $content_length > $part_size * 10_000 ) { |
|
1145
|
|
|
|
|
|
|
croak "Cannot upload content in more than 10,000 parts - consider using a larger part_size"; |
|
1146
|
|
|
|
|
|
|
} |
|
1147
|
|
|
|
|
|
|
elsif( $content_length > $part_size ) { |
|
1148
|
|
|
|
|
|
|
$gen_parts = sub { |
|
1149
|
|
|
|
|
|
|
return unless length $args{value}; |
|
1150
|
|
|
|
|
|
|
return substr( $args{value}, 0, $part_size, "" ); |
|
1151
|
|
|
|
|
|
|
}; |
|
1152
|
|
|
|
|
|
|
} |
|
1153
|
|
|
|
|
|
|
else { |
|
1154
|
|
|
|
|
|
|
my @parts = ( [ delete $args{value}, $content_length ] ); |
|
1155
|
|
|
|
|
|
|
$gen_parts = sub { return unless @parts; @{ shift @parts } }; |
|
1156
|
|
|
|
|
|
|
} |
|
1157
|
|
|
|
|
|
|
} |
|
1158
|
|
|
|
|
|
|
|
|
1159
|
|
|
|
|
|
|
my @parts; |
|
1160
|
|
|
|
|
|
|
push @parts, [ $gen_parts->() ]; |
|
1161
|
|
|
|
|
|
|
|
|
1162
|
|
|
|
|
|
|
# Ensure first part is a Future then unfuture it |
|
1163
|
|
|
|
|
|
|
Future->wrap( @{$parts[0]} )->then( sub { |
|
1164
|
|
|
|
|
|
|
@{$parts[0]} = @_ if @_; |
|
1165
|
|
|
|
|
|
|
|
|
1166
|
|
|
|
|
|
|
push @parts, [ $gen_parts->() ]; |
|
1167
|
|
|
|
|
|
|
|
|
1168
|
|
|
|
|
|
|
if( @{ $parts[1] } ) { |
|
1169
|
|
|
|
|
|
|
# There are at least two parts; we'd better use multipart upload |
|
1170
|
|
|
|
|
|
|
$self->_put_object_multipart( sub { @parts ? @{ shift @parts } : goto &$gen_parts }, %args ); |
|
1171
|
|
|
|
|
|
|
} |
|
1172
|
|
|
|
|
|
|
elsif( @{ $parts[0] } ) { |
|
1173
|
|
|
|
|
|
|
# There is exactly one part |
|
1174
|
|
|
|
|
|
|
my ( $content, $content_length ) = @{ shift @parts }; |
|
1175
|
|
|
|
|
|
|
$self->_retry( "_put_object", |
|
1176
|
|
|
|
|
|
|
bucket => $args{bucket}, |
|
1177
|
|
|
|
|
|
|
path => $args{key}, |
|
1178
|
|
|
|
|
|
|
content => $content, |
|
1179
|
|
|
|
|
|
|
content_length => $content_length, |
|
1180
|
|
|
|
|
|
|
meta => $args{meta}, |
|
1181
|
|
|
|
|
|
|
%args, |
|
1182
|
|
|
|
|
|
|
); |
|
1183
|
|
|
|
|
|
|
} |
|
1184
|
|
|
|
|
|
|
else { |
|
1185
|
|
|
|
|
|
|
# There are no parts at all - create an empty object |
|
1186
|
|
|
|
|
|
|
$self->_retry( "_put_object", |
|
1187
|
|
|
|
|
|
|
bucket => $args{bucket}, |
|
1188
|
|
|
|
|
|
|
path => $args{key}, |
|
1189
|
|
|
|
|
|
|
content => "", |
|
1190
|
|
|
|
|
|
|
meta => $args{meta}, |
|
1191
|
|
|
|
|
|
|
%args, |
|
1192
|
|
|
|
|
|
|
); |
|
1193
|
|
|
|
|
|
|
} |
|
1194
|
|
|
|
|
|
|
}); |
|
1195
|
|
|
|
|
|
|
} |
|
1196
|
|
|
|
|
|
|
|
|
1197
|
|
|
|
|
|
|
=head2 $s3->delete_object( %args ) ==> () |
|
1198
|
|
|
|
|
|
|
|
|
1199
|
|
|
|
|
|
|
Deletes a key from the bucket. |
|
1200
|
|
|
|
|
|
|
|
|
1201
|
|
|
|
|
|
|
Takes the following named arguments: |
|
1202
|
|
|
|
|
|
|
|
|
1203
|
|
|
|
|
|
|
=over 8 |
|
1204
|
|
|
|
|
|
|
|
|
1205
|
|
|
|
|
|
|
=item bucket => STRING |
|
1206
|
|
|
|
|
|
|
|
|
1207
|
|
|
|
|
|
|
The name of the S3 bucket to put the value in |
|
1208
|
|
|
|
|
|
|
|
|
1209
|
|
|
|
|
|
|
=item key => STRING |
|
1210
|
|
|
|
|
|
|
|
|
1211
|
|
|
|
|
|
|
The name of the key to put the value in |
|
1212
|
|
|
|
|
|
|
|
|
1213
|
|
|
|
|
|
|
=back |
|
1214
|
|
|
|
|
|
|
|
|
1215
|
|
|
|
|
|
|
The Future will return nothing. |
|
1216
|
|
|
|
|
|
|
|
|
1217
|
|
|
|
|
|
|
=cut |
|
1218
|
|
|
|
|
|
|
|
|
1219
|
|
|
|
|
|
|
sub _delete_object |
|
1220
|
|
|
|
|
|
|
{ |
|
1221
|
|
|
|
|
|
|
my $self = shift; |
|
1222
|
|
|
|
|
|
|
my %args = @_; |
|
1223
|
|
|
|
|
|
|
|
|
1224
|
|
|
|
|
|
|
my $request = $self->_make_request( |
|
1225
|
|
|
|
|
|
|
method => "DELETE", |
|
1226
|
|
|
|
|
|
|
bucket => $args{bucket}, |
|
1227
|
|
|
|
|
|
|
path => $args{key}, |
|
1228
|
|
|
|
|
|
|
content => "", |
|
1229
|
|
|
|
|
|
|
); |
|
1230
|
|
|
|
|
|
|
|
|
1231
|
|
|
|
|
|
|
$self->_do_request( $request, |
|
1232
|
|
|
|
|
|
|
timeout => $args{timeout} // $self->{timeout}, |
|
1233
|
|
|
|
|
|
|
stall_timeout => $args{stall_timeout} // $self->{stall_timeout}, |
|
1234
|
|
|
|
|
|
|
)->then( sub { |
|
1235
|
|
|
|
|
|
|
return Future->new->done; |
|
1236
|
|
|
|
|
|
|
}); |
|
1237
|
|
|
|
|
|
|
} |
|
1238
|
|
|
|
|
|
|
|
|
1239
|
|
|
|
|
|
|
sub delete_object |
|
1240
|
|
|
|
|
|
|
{ |
|
1241
|
|
|
|
|
|
|
my $self = shift; |
|
1242
|
|
|
|
|
|
|
$self->_retry( "_delete_object", @_ ); |
|
1243
|
|
|
|
|
|
|
} |
|
1244
|
|
|
|
|
|
|
|
|
1245
|
|
|
|
|
|
|
=head1 SPONSORS |
|
1246
|
|
|
|
|
|
|
|
|
1247
|
|
|
|
|
|
|
Parts of this code were paid for by |
|
1248
|
|
|
|
|
|
|
|
|
1249
|
|
|
|
|
|
|
=over 2 |
|
1250
|
|
|
|
|
|
|
|
|
1251
|
|
|
|
|
|
|
=item * |
|
1252
|
|
|
|
|
|
|
|
|
1253
|
|
|
|
|
|
|
SocialFlow L |
|
1254
|
|
|
|
|
|
|
|
|
1255
|
|
|
|
|
|
|
=item * |
|
1256
|
|
|
|
|
|
|
|
|
1257
|
|
|
|
|
|
|
Shadowcat Systems L |
|
1258
|
|
|
|
|
|
|
|
|
1259
|
|
|
|
|
|
|
=back |
|
1260
|
|
|
|
|
|
|
|
|
1261
|
|
|
|
|
|
|
=head1 AUTHOR |
|
1262
|
|
|
|
|
|
|
|
|
1263
|
|
|
|
|
|
|
Paul Evans |
|
1264
|
|
|
|
|
|
|
|
|
1265
|
|
|
|
|
|
|
=cut |
|
1266
|
|
|
|
|
|
|
|
|
1267
|
|
|
|
|
|
|
0x55AA; |