NAME

Amazon::S3::Lite - A lightweight Amazon S3 client for common operations

SYNOPSIS

use Amazon::S3::Lite;

# Credentials from environment or IAM role automatically
my $s3 = Amazon::S3::Lite->new({ region => 'us-east-1' });

# Explicit credentials
my $s3 = Amazon::S3::Lite->new({
  region                => 'us-east-1',
  aws_access_key_id     => $key,
  aws_secret_access_key => $secret,
  token                 => $session_token,  # optional, for STS/Lambda roles
});

# Pass any credentials object with standard getters
my $s3 = Amazon::S3::Lite->new({
  region      => 'us-east-1',
  credentials => $creds_obj,
});

# List objects in a bucket
my $result = $s3->list_objects_v2('my-bucket', prefix => 'logs/');

foreach my $obj ( @{ $result->{objects} } ) {
  printf "%s  %d bytes\n", $obj->{key}, $obj->{size};
}

# Paginate
while ( $result->{is_truncated} ) {
  $result = $s3->list_objects_v2('my-bucket',
    prefix             => 'logs/',
    continuation_token => $result->{next_continuation_token},
  );
  # ... process $result->{objects}
}

# Get an object
my $obj = $s3->get_object('my-bucket', 'path/to/key.json');
print $obj->{content};

# Head an object (existence check / metadata only)
my $meta = $s3->head_object('my-bucket', 'path/to/key.json');
if ($meta) {
  print $meta->{content_length};
}

# Put an object
$s3->put_object('my-bucket', 'path/to/key.json', $json_string,
  content_type => 'application/json',
  metadata     => { source => 'lambda' },
);

# Copy an object
$s3->copy_object(
  src_bucket => 'my-bucket', src_key => 'orig/file.json',
  dst_bucket => 'my-bucket', dst_key => 'archive/file.json',
);

# Delete an object
$s3->delete_object('my-bucket', 'path/to/key.json');

# List all buckets
my $result = $s3->list_buckets;
for my $bucket ( @{ $result->{buckets} } ) {
  print $bucket->{name}, "\n";
}

DESCRIPTION

Amazon::S3::Lite is a minimal Amazon S3 client covering the operations most commonly needed in AWS Lambda functions and lightweight scripts: listing buckets, listing objects, reading, writing, copying, and deleting.

It is built on HTTP::Tiny (core since Perl 5.14) and AWS::Signature4, with no dependency on LWP or any part of the libwww-perl ecosystem. The dependency list is intentionally small, making it well-suited for Lambda container images where minimizing cold-start time and image size matters.

It is not a replacement for Amazon::S3 or Net::Amazon::S3, which 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.

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 HTTP::Response objects so callers handle status codes and errors themselves. Amazon::S3::Lite differs in three ways: it has no dependency on LWP (Amazon::S3::Thin defaults to 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, Amazon::S3::Thin is a fine choice.

CONSTRUCTOR

new

my $s3 = Amazon::S3::Lite->new(\%options);

Returns a new Amazon::S3::Lite object. Options:

Credential resolution order

When no credentials object is passed, credentials are resolved in this order:

  1. Constructor arguments aws_access_key_id and aws_secret_access_key.
  2. Environment variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and optionally AWS_SESSION_TOKEN.
  3. Amazon::Credentials, if installed. This covers IAM instance roles, Lambda execution roles, ECS task roles, and ~/.aws/credentials profiles.
  4. If none of the above yield credentials, the constructor croaks.

METHODS

All methods croak on unrecoverable errors (network failure, HTTP 5xx). HTTP 404 is not an exception - methods that can meaningfully return undef for a missing resource do so.

list_objects_v2

my $result = $s3->list_objects_v2($bucket, %options);

Lists objects in $bucket using the S3 ListObjectsV2 API.

Options:

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
}

list_all_objects_v2

my @objects = $s3->list_all_objects_v2($bucket, %options);

Convenience wrapper around "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 list_objects_v2 except continuation_token (which is managed internally) and 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 "list_objects_v2" directly and process each page as it arrives.

delimiter and common_prefixes are not supported by this method. The purpose of list_all_objects_v2 is a complete flat listing of all matching keys. Hierarchical directory-style traversal using delimiter is inherently page-by-page and should use "list_objects_v2" directly.

Returns a (possibly empty) list of object hashrefs, each with the same fields as the elements of objects in the list_objects_v2 response.

get_object

my $obj = $s3->get_object($bucket, $key);
my $obj = $s3->get_object($bucket, $key, %options);

Fetches the object at $key in $bucket.

Returns 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:

head_object

my $meta = $s3->head_object($bucket, $key);

Fetches metadata for $key without retrieving the object body. Useful for existence checks and reading x-amz-meta-* headers cheaply.

Returns undef if the key does not exist (HTTP 404).

Returns a hashref on success with the same fields as get_object except content, which is always absent.

put_object

$s3->put_object($bucket, $key, $data, %options);

Stores $data at $key in $bucket. $data may be:

When passing a filehandle, 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 (IO::Scalar, etc.) you must supply 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:

Returns the ETag of the stored object on success. Croaks on failure.

copy_object

$s3->copy_object(
  src_bucket => 'src-bucket',
  src_key    => 'original/key.json',
  dst_bucket => 'dst-bucket',
  dst_key    => 'copy/key.json',
);

Copies an object within or between buckets without transferring data through the client. The copy is performed entirely server-side by S3.

Returns a hashref on success:

{
  etag          => 'abc123',
  last_modified => '2024-01-01T00:00:00.000Z',
}

Croaks on failure.

delete_object

$s3->delete_object($bucket, $key);
$s3->delete_object($bucket, $key, version_id => $vid);

Deletes the object at $key in $bucket.

If version_id is provided, that specific version is deleted.

Returns true on success. Note that S3 returns HTTP 204 for both successful deletes and deletes of non-existent keys, so this method does not distinguish between the two - it succeeds silently in either case.

list_buckets

my $result = $s3->list_buckets;

Lists all S3 buckets owned by the authenticated account.

Returns a hashref:

{
  owner_id   => 'abc123...',
  owner_name => 'myaccount',
  buckets    => [
    { name => 'my-bucket',    creation_date => '2024-01-01T00:00:00.000Z' },
    { name => 'other-bucket', creation_date => '2024-06-01T00:00:00.000Z' },
    ...
  ],
}

Note that this operation is always signed against us-east-1 regardless of the region the object was constructed with. See "LAMBDA USAGE NOTES".

ERROR HANDLING

Methods croak on:

Methods return undef on:

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.

DEPENDENCIES

Optional:

LAMBDA USAGE NOTES

In a Lambda container, credentials come from the execution role via the ECS credential provider endpoint (indicated by AWS_CONTAINER_CREDENTIALS_RELATIVE_URI in the environment). 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 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN directly, which this module picks up automatically from the environment.

Region note: The list_buckets method is a global S3 operation and is always signed against 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.

Cold start: Because this module depends only on HTTP::Tiny (Perl core), XML::Twig, AWS::Signature4, and URI::Escape, it adds minimal overhead to Lambda container image builds compared to LWP-based S3 clients.

TESTING

When testing against LocalStack, be aware that LocalStack is more lenient than real S3 regarding SigV4 requirements. In particular, LocalStack may accept requests where the x-amz-content-sha256 header is missing or where session token handling is incorrect. Tests that pass against LocalStack should always be verified against real S3 before release.

SEE ALSO

Amazon::S3 - the full-featured S3 client this module draws from

Amazon::S3::Thin - another excellent lightweight S3 client with a similar philosophy, broader feature coverage, and a longer track record. Uses LWP by default and returns raw HTTP::Response objects. See "DESCRIPTION" for a detailed comparison.

Net::Amazon::S3 - a Moose-based full-featured alternative

Amazon::Signature4::Lite - the signing module used internally

Amazon::Credentials - credential provider with IAM role and profile support

AUTHOR

Rob Lauer rlauer@treasurersbriefcase.com

LICENSE

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.