138 lines
3.8 KiB
Perl
138 lines
3.8 KiB
Perl
|
|
#!/usr/bin/perl
|
||
|
|
#
|
||
|
|
# DW::BlobStore::S3
|
||
|
|
#
|
||
|
|
# Library for storing blobs in S3.
|
||
|
|
#
|
||
|
|
# Authors:
|
||
|
|
# Mark Smith <mark@dreamwidth.org>
|
||
|
|
#
|
||
|
|
# Copyright (c) 2016-2017 by Dreamwidth Studios, LLC.
|
||
|
|
#
|
||
|
|
# This program is free software; you may redistribute it and/or modify it under
|
||
|
|
# the same terms as Perl itself. For a copy of the license, please reference
|
||
|
|
# 'perldoc perlartistic' or 'perldoc perlgpl'.
|
||
|
|
#
|
||
|
|
|
||
|
|
package DW::BlobStore::S3;
|
||
|
|
|
||
|
|
use strict;
|
||
|
|
use v5.10;
|
||
|
|
use Log::Log4perl;
|
||
|
|
my $log = Log::Log4perl->get_logger(__PACKAGE__);
|
||
|
|
|
||
|
|
use Digest::MD5 qw/ md5_hex /;
|
||
|
|
use Paws;
|
||
|
|
|
||
|
|
sub type { 's3' }
|
||
|
|
|
||
|
|
sub init {
|
||
|
|
my ( $class, %args ) = @_;
|
||
|
|
|
||
|
|
foreach my $required (qw/ access_key secret_key region prefix bucket /) {
|
||
|
|
$log->logcroak( 'S3 configuration must include config: ', $required )
|
||
|
|
unless exists $args{$required};
|
||
|
|
}
|
||
|
|
|
||
|
|
$log->logcroak('Prefix does not match required regex: [a-zA-Z0-9_-]+$.')
|
||
|
|
if defined $args{prefix} && $args{prefix} !~ /^[a-zA-Z0-9_-]+$/;
|
||
|
|
|
||
|
|
my $paws = Paws->new(
|
||
|
|
config => {
|
||
|
|
region => $args{region},
|
||
|
|
},
|
||
|
|
) or $log->logcroak('Failed to initialize Paws object.');
|
||
|
|
my $s3 = $paws->service('S3')
|
||
|
|
or $log->logcroak('Failed to initialize Paws::S3 object.');
|
||
|
|
|
||
|
|
$log->debug("Initializing blobstore for S3");
|
||
|
|
my $self = {
|
||
|
|
s3 => $s3,
|
||
|
|
bucket => $args{bucket},
|
||
|
|
prefix => $args{prefix}
|
||
|
|
};
|
||
|
|
return bless $self, $class;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub get_location_for_key {
|
||
|
|
my ( $self, $namespace, $key ) = @_;
|
||
|
|
|
||
|
|
# Hash the key, we create two layers of directory structure so the files
|
||
|
|
# spread across 256^2 directories
|
||
|
|
my $hash = md5_hex($key);
|
||
|
|
|
||
|
|
# Create the fully qualified path including optional configured prefix
|
||
|
|
my $fqfn =
|
||
|
|
( defined $self->{prefix} ? $self->{prefix} . '/' : '' )
|
||
|
|
. $namespace . '/'
|
||
|
|
. substr( $hash, 0, 2 ) . '/'
|
||
|
|
. substr( $hash, 2, 2 ) . '/'
|
||
|
|
. $hash;
|
||
|
|
$log->debug("($namespace, $key) => $fqfn");
|
||
|
|
return $fqfn;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub store {
|
||
|
|
my ( $self, $namespace, $key, $blobref ) = @_;
|
||
|
|
$log->logcroak('Unable to store empty file.')
|
||
|
|
unless defined $$blobref && length $$blobref;
|
||
|
|
my $fqfn = $self->get_location_for_key( $namespace, $key );
|
||
|
|
|
||
|
|
my $res = eval {
|
||
|
|
$self->{s3}->PutObject(
|
||
|
|
Bucket => $self->{bucket},
|
||
|
|
Key => $fqfn,
|
||
|
|
Body => $$blobref,
|
||
|
|
);
|
||
|
|
};
|
||
|
|
if ( $@ && $@->isa('Paws::Exception') ) {
|
||
|
|
$log->error( "Failed to store to ( $namespace, $key ): " . $@->message );
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
$log->debug( "Wrote ", length $$blobref, " bytes to: $fqfn" );
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub exists {
|
||
|
|
my ( $self, $namespace, $key ) = @_;
|
||
|
|
my $fqfn = $self->get_location_for_key( $namespace, $key );
|
||
|
|
|
||
|
|
my $res = eval { $self->{s3}->HeadObject( Bucket => $self->{bucket}, Key => $fqfn, ) };
|
||
|
|
if ( $@ && $@->isa('Paws::Exception') ) {
|
||
|
|
$log->error( "Failed to check exists on ( $namespace, $key ): ", $@->message );
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
$log->debug( 'Found path exists in S3: ', $fqfn );
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub retrieve {
|
||
|
|
my ( $self, $namespace, $key ) = @_;
|
||
|
|
my $fqfn = $self->get_location_for_key( $namespace, $key );
|
||
|
|
|
||
|
|
my $res = eval { $self->{s3}->GetObject( Bucket => $self->{bucket}, Key => $fqfn, ) };
|
||
|
|
if ( $@ && $@->isa('Paws::Exception') ) {
|
||
|
|
$log->error( "Failed to retrieve from ( $namespace, $key ): " . $@->message );
|
||
|
|
return undef;
|
||
|
|
}
|
||
|
|
return \$res->Body;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub delete {
|
||
|
|
my ( $self, $namespace, $key ) = @_;
|
||
|
|
my $fqfn = $self->get_location_for_key( $namespace, $key );
|
||
|
|
|
||
|
|
my $res = eval { $self->{s3}->DeleteObject( Bucket => $self->{bucket}, Key => $fqfn, ) };
|
||
|
|
if ( $@ && $@->isa('Paws::Exception') ) {
|
||
|
|
$log->error( "Failed to delete ( $namespace, $key ): ", $@->message );
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
$log->debug( 'Deleted path from S3: ', $fqfn );
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
1;
|