Amazon-S3-Lite
view release on metacpan or search on metacpan
lib/Amazon/S3/Lite.pm view on Meta::CPAN
use Amazon::Signature4::Lite;
use Amazon::S3::Lite::Credentials;
use Amazon::S3::Lite::Logger;
use Carp qw(croak);
use Data::Dumper;
use Digest::MD5 qw(md5_base64 md5);
use English qw(-no_match_vars);
use HTTP::Tiny;
use List::Util qw(pairs);
use MIME::Base64 qw(encode_base64);
use Scalar::Util qw(blessed openhandle);
use URI::Escape qw(uri_escape_utf8);
use XML::Twig;
use Readonly;
Readonly our $TRUE => 1;
Readonly our $FALSE => 0;
our $VERSION = '1.1.5';
########################################################################
sub new {
########################################################################
my ( $class, $args ) = @_;
$args //= {};
croak 'new() requires a hashref'
if ref $args ne 'HASH';
croak 'region is required'
if !$args->{region};
my $self = bless {}, $class;
$self->{region} = $args->{region};
$self->{host} = $args->{host} // 's3.amazonaws.com';
$self->{secure} = $args->{secure} // 1;
$self->{timeout} = $args->{timeout} // 30;
$self->_init_logger( $args->{logger} );
$self->_init_credentials($args);
$self->_init_ua;
return $self;
}
########################################################################
# Logger setup
# Priority: caller-supplied object -> Log::Log4perl (if available) ->
# minimal STDERR logger
########################################################################
sub _init_logger {
########################################################################
my ( $self, $logger ) = @_;
if ($logger) {
# Validate it quacks like a logger
for my $method (qw(trace debug info warn error)) {
croak "logger object must implement '$method'"
if !$logger->can($method);
}
$self->{logger} = $logger;
return;
}
if ( eval { require Log::Log4perl; 1 } ) {
if ( !Log::Log4perl->initialized ) {
Log::Log4perl->easy_init($Log::Log4perl::WARN);
}
$self->{logger} = Log::Log4perl->get_logger(__PACKAGE__);
return;
}
# Fall back to minimal STDERR logger
$self->{logger} = Amazon::S3::Lite::Logger->new;
return;
}
########################################################################
# Credential resolution
# Priority: explicit credentials object -> constructor args ->
# environment variables -> Amazon::Credentials (if available)
########################################################################
sub _init_credentials {
########################################################################
my ( $self, $args ) = @_;
# 1. Caller-supplied credentials object (duck-typed)
if ( my $creds = $args->{credentials} ) {
croak "credential object is not blessed.\n"
if !blessed $creds;
foreach (qw(aws_access_key_id aws_secret_access_key token)) {
my $sub = $creds->can($_) // $creds->can("get_$_");
croak "credentials object must implement $_ or get_$_\n"
if !$sub;
}
$self->{credentials} = $creds;
return;
}
# 2. Explicit constructor args
if ( $args->{aws_access_key_id} && $args->{aws_secret_access_key} ) {
$self->{credentials} = Amazon::S3::Lite::Credentials->new(
aws_access_key_id => $args->{aws_access_key_id},
aws_secret_access_key => $args->{aws_secret_access_key},
token => $args->{token},
);
return;
}
# 3. Environment variables
if ( $ENV{AWS_ACCESS_KEY_ID} && $ENV{AWS_SECRET_ACCESS_KEY} ) {
$self->{credentials} = Amazon::S3::Lite::Credentials->new(
lib/Amazon/S3/Lite.pm view on Meta::CPAN
region => $region // $self->region,
service => 's3',
);
}
########################################################################
# Build the endpoint URL for a bucket/key
########################################################################
sub _endpoint {
########################################################################
my ( $self, $bucket, $key ) = @_;
my $scheme = $self->{secure} ? 'https' : 'http';
my $host = $self->host;
# Path-style URL: https://s3.amazonaws.com/bucket/key
# (virtual-hosted style omitted for simplicity; path-style works
# everywhere and avoids SSL cert issues with dotted bucket names)
my $url = "$scheme://$host";
$url .= "/$bucket" if defined $bucket && length $bucket;
$url .= '/' . _encode_key($key) if defined $key && length $key;
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 )
lib/Amazon/S3/Lite.pm view on Meta::CPAN
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 get_bucket_notification_configuration {
########################################################################
my ( $self, $bucket ) = @_;
croak 'bucket is required'
if !defined $bucket || !length $bucket;
my $url = $self->_endpoint($bucket) . q{?notification=};
my $response = $self->_request( 'GET', $url );
$self->_croak_on_error( $response, 'get_bucket_notification_configuration' );
my $rsp = $self->_parse_notification_configuration( $response->{content} );
$self->logger->debug(
Dumper(
[ response => $response,
parsed_response => $rsp
]
)
);
return $rsp;
}
########################################################################
sub _parse_notification_configuration {
########################################################################
my ( $self, $xml ) = @_;
my @configs;
my $handler = sub {
my ( $t, $node ) = @_;
my @events = map { $_->text } $node->children('Event');
my @filter_rules;
if ( my $filter = $node->first_child('Filter') ) {
if ( my $s3key = $filter->first_child('S3Key') ) {
for my $rule ( $s3key->children('FilterRule') ) {
push @filter_rules,
{
name => $rule->first_child_text('Name'),
value => $rule->first_child_text('Value'),
};
}
}
}
push @configs,
{
id => $node->first_child_text('Id'),
lambda_arn => $node->first_child_text('CloudFunction'),
queue_arn => $node->first_child_text('Queue'),
topic_arn => $node->first_child_text('Topic'),
events => \@events,
filters => \@filter_rules,
};
$t->purge;
};
XML::Twig->new(
twig_handlers => {
CloudFunctionConfiguration => $handler,
QueueConfiguration => $handler,
TopicConfiguration => $handler,
}
)->parse($xml);
return \@configs;
}
lib/Amazon/S3/Lite.pm view on Meta::CPAN
support the full S3 API surface including multipart upload, bucket
management, ACLs, versioning, and presigned URLs. If you need those
features, use one of those distributions instead.
L<Amazon::S3::Thin> is another excellent lightweight S3 client with a
similar philosophy and a longer track record. It is more complete than
this module - supporting presigned URLs, bulk delete, and
virtual-hosted-style requests - and returns raw L<HTTP::Response>
objects so callers handle status codes and errors
themselves. C<Amazon::S3::Lite> differs in three ways: it has no
dependency on LWP (C<Amazon::S3::Thin> defaults to L<LWP::UserAgent>),
it returns parsed hashrefs rather than raw response objects, and it
has first-class support for Lambda IAM role credential rotation. If
you need the broader feature set or prefer direct HTTP access,
C<Amazon::S3::Thin> is a fine choice.
=head1 CONSTRUCTOR
=head2 new
my $s3 = Amazon::S3::Lite->new(\%options);
Returns a new C<Amazon::S3::Lite> object. Options:
=over 4
=item region (required)
The AWS region for your bucket, e.g. C<us-east-1>.
=item aws_access_key_id / aws_secret_access_key
Static credentials. C<token> may also be supplied for STS temporary
credentials (as used by Lambda execution roles).
These are only consulted if no C<credentials> object is provided.
=item token
Optional STS session token, used alongside static credentials for
temporary credential sets.
=item credentials
An object providing credential getters. The object must respond to:
$creds->aws_access_key_id
$creds->aws_secret_access_key
$creds->token # may return undef
Any object that satisfies this interface is accepted -
L<Amazon::Credentials>, L<Paws::Credential::*>, or your own. The
getters are called at request time, so objects that refresh expiring
credentials transparently are supported.
=item logger
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 level WARN and logs to
STDERR. If Log::Log4perl is not installed, a minimal internal logger
is used that prints WARN and above to STDERR.
=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
( run in 0.467 second using v1.01-cache-2.11-cpan-5511b514fd6 )