DarkPAN-Indexer
view release on metacpan or search on metacpan
lib/DarkPAN/Indexer/Storage/S3.pm view on Meta::CPAN
package DarkPAN::Indexer::Storage::S3;
# S3 storage engine for DarkPAN::Indexer.
#
# Consumes the DarkPAN::Indexer::Storage role (the contract). Wraps the
# four S3 primitives the old roles called directly:
# list_objects_v2 -> list_distributions
# get_object -> fetch_object
# head_object -> has_object
# put_object -> save_object
#
use strict;
use warnings;
use Amazon::Credentials;
use Amazon::S3::Lite;
use Amazon::S3::Lite::Lock;
use Data::Dumper;
use Carp qw(croak);
use English qw(-no_match_vars);
use Role::Tiny::With;
with 'DarkPAN::Indexer::Storage'; # requires: list_distributions fetch_object
# save_object has_object lock base_url
caller or exit __PACKAGE__->main();
########################################################################
sub new {
########################################################################
my ( $class, $config ) = @_;
# prefer storage keys over AWS
my %storage = ( %{ $config->{AWS} // {} }, %{ $config->{storage} // {} } );
$storage{region} //= $ENV{AWS_REGION} // 'us-east-1';
$config->{storage} = \%storage;
croak "S3 storage: bucket is required\n"
if !defined $storage{bucket} || $storage{bucket} eq q{};
my $self = bless {
config => $config,
bucket => $storage{bucket},
prefix => $storage{prefix},
region => $storage{region},
profile => $storage{profile},
base_url => $config->{url}, # public (CloudFront) base
}, $class;
return $self;
}
########################################################################
sub _s3 {
########################################################################
my ($self) = @_;
return $self->{s3} //= Amazon::S3::Lite->new(
{ region => $self->{region},
credentials => Amazon::Credentials->new( profile => $self->{profile} ),
}
);
}
########################################################################
sub base_url { return $_[0]->{base_url} }
########################################################################
########################################################################
# list_distributions() -> list of keys (relative to the bucket) that
# look like distribution tarballs under the configured prefix. Paginates
# via continuation token. The .tar.gz filter lives here because "what
# counts as a distribution" is the one bit of domain knowledge this
# seam legitimately owns.
########################################################################
sub list_distributions {
########################################################################
my ($self) = @_;
my $s3 = $self->_s3;
my @keys;
my $token;
while (1) {
my %options = ( defined $self->{prefix} ? ( prefix => $self->{prefix} ) : () );
if ( defined $token ) {
$options{continuation_token} = $token;
}
my $result = $s3->list_objects_v2( $self->{bucket}, %options );
for my $object ( @{ $result->{objects} || [] } ) {
my $key = $object->{key};
next if !defined $key;
next if $key !~ m{[.]tar[.]gz\z}xsm;
push @keys, $key;
}
last
if !$result->{is_truncated};
$token = $result->{next_continuation_token};
last if !$token;
}
return @keys;
}
########################################################################
# fetch_object($key) -> raw bytes, or undef if genuinely absent.
# A real I/O failure throws (fail-fast contract).
########################################################################
sub fetch_object {
my ( $self, $key ) = @_;
my $object = $self->_s3->get_object( $self->{bucket}, $key );
return
if !defined $object; # not found -> undef
( run in 1.466 second using v1.01-cache-2.11-cpan-3f962061493 )