Amazon-S3-Lite
view release on metacpan or search on metacpan
lib/Amazon/S3/Lite.pm view on Meta::CPAN
return $url;
}
########################################################################
# URI-encode an S3 key, preserving '/' separators
########################################################################
sub _encode_key {
########################################################################
my ($key) = @_;
return join '/', map { uri_escape_utf8( $_, '^A-Za-z0-9\-._~' ) }
split m{/}, $key, -1;
}
########################################################################
sub _request {
########################################################################
my ( $self, $method, $url, $headers, $content, $extra, $region ) = @_;
$region //= $self->region;
$headers //= {};
$content //= q{};
$extra //= {};
my $content_is_coderef = ref $content eq 'CODE';
# sign â returns merged headers ready for HTTP::Tiny
my $signed = $self->_signer($region)->sign(
method => $method,
url => $url,
headers => $headers,
payload => $content_is_coderef ? q{} : $content,
);
# HTTP::Tiny sets Host itself â remove to avoid duplicate header error
delete $signed->{host};
$self->logger->debug("$method $url");
my $options = { headers => $signed };
if ( length $content || $content_is_coderef ) {
$options->{content} = $content;
}
if ( $extra->{data_callback} ) {
$options->{data_callback} = $extra->{data_callback};
}
my $response = $self->ua->request( $method, $url, $options );
$self->logger->debug( sprintf 'Response: %s %s', $response->{status}, $response->{reason} );
return $response;
}
########################################################################
# head_object( $bucket, $key )
#
# Fetches metadata for an object without retrieving the body.
# Returns undef if the key does not exist (404).
# Returns a hashref with content_type, content_length, etag,
# last_modified, and metadata (x-amz-meta-* headers).
########################################################################
sub head_object {
########################################################################
my ( $self, $bucket, $key ) = @_;
croak 'bucket is required' if !defined $bucket || !length $bucket;
croak 'key is required' if !defined $key || !length $key;
my $url = $self->_endpoint( $bucket, $key );
my $response = $self->_request( 'HEAD', $url );
return undef ## no critic (Subroutines::ProhibitExplicitReturnUndef)
if _is_not_found($response);
$self->_croak_on_error( $response, 'head_object' );
return $self->_extract_object_metadata( $response->{headers} );
}
########################################################################
# Extract the standard object metadata hashref from a response headers
# hash. Used by both head_object and get_object.
########################################################################
sub _extract_object_metadata {
########################################################################
my ( $self, $headers ) = @_;
my $etag = $headers->{etag};
$etag =~ s/\A"|"\z//gxsm if defined $etag;
# Collect x-amz-meta-* headers, stripping the prefix from the key
my %metadata;
for my $name ( keys %{$headers} ) {
if ( $name =~ /^x-amz-meta-(.+)$/xsm ) {
$metadata{$1} = $headers->{$name};
}
}
return {
content_type => $headers->{'content-type'},
content_length => $headers->{'content-length'} + 0,
etag => $etag,
last_modified => $headers->{'last-modified'},
metadata => \%metadata,
};
}
########################################################################
# get_object( $bucket, $key, %options )
#
# Fetches an object from S3. Options:
# range => 'bytes=0-1023' partial fetch
# filename => '/tmp/foo' stream body to disk; omits content key
#
# Returns undef on 404.
# Returns a hashref with content_type, content_length, etag,
# last_modified, metadata, and content (unless filename is used).
########################################################################
sub get_object {
########################################################################
my ( $self, $bucket, $key, %options ) = @_;
croak 'bucket is required' if !defined $bucket || !length $bucket;
croak 'key is required' if !defined $key || !length $key;
my $url = $self->_endpoint( $bucket, $key );
my %headers;
$headers{Range} = $options{range} if defined $options{range};
my $filename = $options{filename};
my $extra = {};
if ( defined $filename ) {
# Open the destination file before making the request so we catch
# permission errors early, before network round-trip
open my $fh, '>', $filename
or croak "cannot open '$filename' for writing: $!";
$extra->{data_callback} = sub {
my ($data) = @_;
print {$fh} $data
or croak "write to '$filename' failed: $!";
};
my $response = $self->_request( 'GET', $url, \%headers, q{}, $extra );
close $fh
or croak "close of '$filename' failed: $!";
return undef ## no critic (Subroutines::ProhibitExplicitReturnUndef)
if _is_not_found($response);
$self->_croak_on_error( $response, 'get_object' );
# Return metadata only â content is on disk
return $self->_extract_object_metadata( $response->{headers} );
}
# In-memory path
my $response = $self->_request( 'GET', $url, \%headers );
return undef ## no critic (Subroutines::ProhibitExplicitReturnUndef)
if _is_not_found($response);
$self->_croak_on_error( $response, 'get_object' );
my $result = $self->_extract_object_metadata( $response->{headers} );
$result->{content} = $response->{content};
return $result;
}
########################################################################
# delete_object( $bucket, $key, %options )
lib/Amazon/S3/Lite.pm view on Meta::CPAN
'MaxKeys' => sub { $max_keys = $_[1]->text + 0 },
'IsTruncated' => sub { $is_truncated = $_[1]->text eq 'true' ? 1 : 0 },
'NextContinuationToken' => sub { $next_token = $_[1]->text },
'Contents' => sub {
my ( $t, $node ) = @_;
my $etag = $node->first_child_text('ETag') // q{};
$etag =~ s/\A"|"\z//gxsm;
push @objects,
{
key => $node->first_child_text('Key'),
size => $node->first_child_text('Size') + 0,
last_modified => $node->first_child_text('LastModified'),
etag => $etag,
storage_class => $node->first_child_text('StorageClass'),
};
$t->purge; # free memory as we go - important for large listings
},
'CommonPrefixes' => sub {
my ( $t, $node ) = @_;
push @common_prefixes, $node->first_child_text('Prefix');
},
}
)->parse($xml);
return {
bucket => $bucket,
prefix => $prefix,
key_count => $key_count,
max_keys => $max_keys,
is_truncated => $is_truncated,
next_continuation_token => $next_token,
objects => \@objects,
common_prefixes => \@common_prefixes,
};
}
########################################################################
# list_all_objects_v2( $bucket, %options )
#
# Convenience wrapper that auto-paginates list_objects_v2 and returns
# a flat list of all matching object hashrefs.
# delimiter is ignored â use list_objects_v2 directly for that.
########################################################################
sub list_all_objects_v2 {
########################################################################
my ( $self, $bucket, %options ) = @_;
# delimiter is meaningless here â silently remove it
delete $options{delimiter};
my @all_objects;
my $continuation_token;
while ($TRUE) {
if ( defined $continuation_token ) {
$options{continuation_token} = $continuation_token;
}
my $result = $self->list_objects_v2( $bucket, %options );
last if !$result; # 404 / empty bucket
push @all_objects, @{ $result->{objects} };
last if !$result->{is_truncated};
$continuation_token = $result->{next_continuation_token};
}
return @all_objects;
}
########################################################################
sub put_bucket_notification_configuration {
########################################################################
my ( $self, $bucket, %options ) = @_;
my $xml = $self->_create_notification_configuration( $bucket, %options );
my $url = $self->_endpoint($bucket) . q{?notification=};
my %headers = (
'Content-Type' => 'application/xml',
'Content-Length' => length $xml,
'Content-MD5' => encode_base64( md5($xml), q{} ),
);
my $response = $self->_request( 'PUT', $url, \%headers, $xml );
$self->_croak_on_error( $response, 'put_bucket_notification_configuration' );
return $TRUE;
}
########################################################################
sub remove_bucket_notification_configuration {
########################################################################
my ( $self, $bucket ) = @_;
croak 'bucket is required'
if !defined $bucket || !length $bucket;
my $xml = <<'END_XML';
<NotificationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"/>
END_XML
my $url = $self->_endpoint($bucket) . q{?notification=};
my %headers = (
'Content-Type' => 'application/xml',
'Content-Length' => length $xml,
'Content-MD5' => encode_base64( md5($xml), q{} ),
);
my $response = $self->_request( 'PUT', $url, \%headers, $xml );
$self->_croak_on_error( $response, 'remove_bucket_notification_configuration' );
return $TRUE;
}
lib/Amazon/S3/Lite.pm view on Meta::CPAN
sub _fetch_templates {
########################################################################
my ($self) = @_;
return \%TEMPLATES
if %TEMPLATES;
local $RS = undef;
my $data = <DATA>;
$data =~ s/\A(.*?)^=pod.*\z/$1/xsm;
my $t = q{};
my $template_name = q{};
foreach my $line ( split /\n/xsm, $data ) {
if ( $line =~ /^:(.*)$/xsm ) {
if ( $template_name && $t ) {
$TEMPLATES{$template_name} = $t;
}
$t = q{};
$template_name = $1;
next;
}
$t .= "$line\n";
}
$TEMPLATES{$template_name} = $t;
return \%TEMPLATES;
}
########################################################################
sub _resolve {
########################################################################
my ( $self, $template, %data ) = @_;
my $output = $template;
foreach my $p ( pairs %data ) {
my ( $k, $v ) = @{$p};
$output =~ s/[@]\Q$k\E[@]/$v/xsmg;
}
return $output;
}
########################################################################
# Error checking helpers
########################################################################
sub _is_success {
########################################################################
return $_[0]->{status} =~ /\A2\d{2}\z/;
}
########################################################################
sub _is_not_found {
########################################################################
return $_[0]->{status} == 404;
}
########################################################################
sub _croak_on_error {
########################################################################
my ( $self, $response, $context ) = @_;
return if _is_success($response);
my ( $status, $reason ) = @{$response}{qw(status reason)};
# Attempt to extract S3 error message from XML body
my $detail = q{};
if ( $response->{content} && $response->{content} =~ /<\?xml/xsm ) {
my ($code) = $response->{content} =~ m{<Code>([^<]+)</Code>}xsm;
my ($msg) = $response->{content} =~ m{<Message>([^<]+)</Message>}xsm;
if ( $code || $msg ) {
$detail = " - $code: $msg";
}
}
croak sprintf '%s failed: HTTP %s %s%s', $context, $status, $reason, $detail;
}
1;
## no critic (RequirePodSections)
__DATA__
:filters
<Filter>
<S3Key>
@filter_rules@
</S3Key>
</Filter>
:filter-rule
<FilterRule>
<Name>@filter_name@</Name>
<Value>@filter@</Value>
</FilterRule>
:event
<Event>@event@</Event>
:lambda-event
<NotificationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CloudFunctionConfiguration>
<Id>@id@</Id>
<CloudFunction>@lambda_arn@</CloudFunction>
@events@
@filters@
</CloudFunctionConfiguration>
</NotificationConfiguration>
:sqs-event
<NotificationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<QueueConfiguration>
<Id>@id@</Id>
<Queue>@queue_arn@</Queue>
@events@
@filters@
lib/Amazon/S3/Lite.pm view on Meta::CPAN
An object providing the standard log methods:
$logger->trace(...)
$logger->debug(...)
$logger->info(...)
$logger->warn(...)
$logger->error(...)
If not supplied, the module looks for L<Log::Log4perl>. If available,
it calls C<Log::Log4perl::easy_init> with the configure log level (or
WARN) and logs to STDERR. If Log::Log4perl is not installed, a
minimal internal logger.
=item host
Override the S3 endpoint host. Defaults to C<s3.amazonaws.com>.
Useful for S3-compatible services (MinIO, Ceph, LocalStack).
=item secure
Use HTTPS. Default is 1 (true). Set to 0 only for testing against
local S3-compatible endpoints.
=item timeout
HTTP request timeout in seconds. Default is 30.
=back
=head2 Credential resolution order
When no C<credentials> object is passed, credentials are resolved in
this order:
=over 4
=item 1.
Constructor arguments C<aws_access_key_id> and C<aws_secret_access_key>.
=item 2.
Environment variables C<AWS_ACCESS_KEY_ID>, C<AWS_SECRET_ACCESS_KEY>,
and optionally C<AWS_SESSION_TOKEN>.
=item 3.
L<Amazon::Credentials>, if installed. This covers IAM instance roles,
Lambda execution roles, ECS task roles, and C<~/.aws/credentials>
profiles.
=item 4.
If none of the above yield credentials, the constructor croaks.
=back
=head1 METHODS
All methods croak on unrecoverable errors (network failure, HTTP 5xx).
HTTP 404 is not an exception - methods that can meaningfully return
C<undef> for a missing resource do so.
=head2 list_objects_v2
my $result = $s3->list_objects_v2($bucket, %options);
Lists objects in C<$bucket> using the S3 ListObjectsV2 API.
Options:
=over 4
=item prefix
Limit results to keys beginning with this string.
=item delimiter
Group keys sharing a common prefix up to this delimiter. Grouped
prefixes are returned in C<common_prefixes>.
=item max_keys
Maximum number of objects to return per call (1-1000, default 1000).
=item continuation_token
Resume a truncated listing from a prior call's
C<next_continuation_token>.
=item start_after
Return only keys lexicographically after this value.
=back
Returns a hashref:
{
bucket => 'my-bucket',
prefix => 'logs/',
is_truncated => 0,
next_continuation_token => undef, # set when is_truncated is true
key_count => 42,
objects => [
{
key => 'logs/2024-01-01.gz',
size => 102400,
last_modified => '2024-01-01T00:00:00.000Z',
etag => 'abc123',
storage_class => 'STANDARD',
},
...
],
common_prefixes => [], # populated when delimiter is set
}
=head2 list_all_objects_v2
my @objects = $s3->list_all_objects_v2($bucket, %options);
Convenience wrapper around L</list_objects_v2> that automatically
follows continuation tokens and returns a flat list of all matching
object hashrefs in a single call.
Accepts the same options as C<list_objects_v2> except
C<continuation_token> (which is managed internally) and C<delimiter>
(which is silently ignored - see below).
my @logs = $s3->list_all_objects_v2('my-bucket', prefix => 'logs/');
foreach my $obj (@logs) {
printf "%s %d bytes\n", $obj->{key}, $obj->{size};
}
Be mindful of memory when listing buckets with large numbers of
objects. For very large listings, use L</list_objects_v2> directly
and process each page as it arrives.
C<delimiter> and C<common_prefixes> are not supported by this method.
The purpose of C<list_all_objects_v2> is a complete flat listing of
all matching keys. Hierarchical directory-style traversal using
C<delimiter> is inherently page-by-page and should use
L</list_objects_v2> directly.
Returns a (possibly empty) list of object hashrefs, each with the same
fields as the elements of C<objects> in the C<list_objects_v2>
response.
=item log_level
Log level for the internal logger. Accepted values: C<trace>, C<debug>,
C<info>, C<warn>, C<error>, C<fatal>. Default is C<warn>. Only consulted
when no C<logger> object is supplied and Log::Log4perl is not available
or not yet initialized.
=head2 get_object
my $obj = $s3->get_object($bucket, $key);
my $obj = $s3->get_object($bucket, $key, %options);
Fetches the object at C<$key> in C<$bucket>.
Returns C<undef> if the key does not exist (HTTP 404).
Returns a hashref on success:
{
content => '...', # raw bytes; absent when filename is used
content_type => 'application/json',
content_length => 1024,
etag => 'abc123',
last_modified => 'Tue, 01 Jan 2024 00:00:00 GMT',
metadata => { # x-amz-meta-* headers, lowercased
source => 'lambda',
},
}
Options:
=over 4
=item range
An HTTP Range header value, e.g. C<bytes=0-1023>, for partial fetches.
=item filename
Path to a local file where the object body should be written. When
supplied, the response body is streamed directly to disk via
HTTP::Tiny's C<:content_file> mechanism and C<content> is omitted from
the returned hashref. The file is created or overwritten.
my $meta = $s3->get_object('my-bucket', 'data/dump.csv',
filename => '/tmp/dump.csv',
);
# $meta->{content} is absent; file is on disk
This is the recommended approach for large objects in Lambda where
holding the full body in memory is undesirable.
=back
=head2 head_object
my $meta = $s3->head_object($bucket, $key);
Fetches metadata for C<$key> without retrieving the object body.
Useful for existence checks and reading C<x-amz-meta-*> headers
cheaply.
Returns C<undef> if the key does not exist (HTTP 404).
Returns a hashref on success with the same fields as C<get_object>
except C<content>, which is always absent.
=head2 put_object
$s3->put_object($bucket, $key, $data, %options);
Stores C<$data> at C<$key> in C<$bucket>. C<$data> may be:
=over 4
=item * A scalar string (the object body verbatim)
=item * A reference to a scalar (avoids copying large strings)
=item * An open filehandle or L<IO::File> object (body is read to EOF)
=back
When passing a filehandle, C<content_length> becomes required unless
HTTP::Tiny can determine the size from the handle (i.e. the handle is
backed by a real file). For in-memory handles (C<IO::Scalar>, etc.)
you must supply C<content_length> explicitly, or the method will
croak.
# Scalar
$s3->put_object('my-bucket', 'hello.txt', 'Hello, world!',
content_type => 'text/plain',
);
# Filehandle
open my $fh, '<', '/tmp/data.csv' or die $!;
$s3->put_object('my-bucket', 'data.csv', $fh,
content_type => 'text/csv',
);
Options:
=over 4
=item content_type
MIME type for the object. Defaults to C<application/octet-stream>.
=item content_length
Required when C<$data> is an in-memory filehandle. Optional (and
ignored) for scalar data, where length is computed automatically.
=item metadata
Hashref of user-defined metadata. Keys should be bare names - the
C<x-amz-meta-> prefix is added automatically.
metadata => { source => 'lambda', job_id => '42' }
=item acl
Canned ACL string, e.g. C<private> (default), C<public-read>.
lib/Amazon/S3/Lite.pm view on Meta::CPAN
Returns an arrayref of configuration hashrefs, each containing:
=over 4
=item id
The configuration entry identifier.
=item lambda_arn
The Lambda function ARN. Present for Lambda notification entries;
C<undef> for SQS entries.
=item queue_arn
The SQS queue ARN. Present for SQS notification entries;
C<undef> for Lambda entries.
=item events
Arrayref of event type strings.
=item filters
Arrayref of hashrefs, each with C<name> (C<prefix> or C<suffix>)
and C<value>.
=back
Returns an empty arrayref if no notification configuration is set.
Croaks on failure.
=head2 remove_bucket_notification_configuration
$s3->remove_bucket_notification_configuration($bucket);
Removes all notification configurations from C<$bucket> by sending an
empty C<NotificationConfiguration> document to S3. After this call S3
will no longer deliver any events for the bucket.
Returns true on success. Croaks on failure.
=head1 ERROR HANDLING
Methods croak on:
=over 4
=item * Network-level failures (connection refused, timeout, DNS failure)
=item * HTTP 5xx responses from S3
=item * Unexpected HTTP 3xx responses that could not be resolved
=back
Methods return C<undef> on:
=over 4
=item * HTTP 404 (key or bucket not found), where the return type allows it
=back
All other HTTP error codes (400, 403, 409, etc.) cause a croak with a
message containing the HTTP status line and the S3 error body where
available.
=head1 DEPENDENCIES
=over 4
=item * L<HTTP::Tiny> (core since Perl 5.14)
=item * L<Amazon::Signature4::Lite>
=item * L<XML::Twig> (for parsing list and copy responses)
=item * L<Digest::MD5> (core, for Content-MD5 headers)
=item * L<MIME::Base64> (core)
=item * L<URI::Escape>
=item * L<Carp> (core)
=back
Optional:
=over 4
=item * L<Amazon::Credentials> - automatic credential discovery from IAM
roles, ECS task roles, ~/.aws/credentials, and environment.
=item * L<Log::Log4perl> - structured logging; if present, used in
preference to the built-in minimal logger.
=back
=head1 LAMBDA USAGE NOTES
In a Lambda container, credentials come from the execution role via
the ECS credential provider endpoint (indicated by
C<AWS_CONTAINER_CREDENTIALS_RELATIVE_URI> in the environment).
L<Amazon::Credentials> handles this automatically when installed and
is the recommended approach. If you prefer not to take that
dependency, the Lambda runtime also populates C<AWS_ACCESS_KEY_ID>,
C<AWS_SECRET_ACCESS_KEY>, and C<AWS_SESSION_TOKEN> directly, which
this module picks up automatically from the environment.
B<Region note:> The C<list_buckets> method is a global S3 operation
and is always signed against C<us-east-1>, regardless of the region
supplied to the constructor. This is an S3 requirement, not a
limitation of this module, and is handled transparently - your
object's region is not changed.
B<Cold start:> Because this module depends only on L<HTTP::Tiny> (Perl
core), L<XML::Twig>, L<AWS::Signature4>, and L<URI::Escape>, it adds
minimal overhead to Lambda container image builds compared to
LWP-based S3 clients.
( run in 0.761 second using v1.01-cache-2.11-cpan-788537b7465 )