Amazon-S3-Thin

 view release on metacpan or  search on metacpan

lib/Amazon/S3/Thin.pm  view on Meta::CPAN

        $self->{virtual_host} = shift;
    } else {
        return $self->{virtual_host};
    }
}

sub _send {
    my ($self, $request) = @_;
    warn "[Request]\n" , $request->as_string if $self->{debug};
    my $response = $self->ua->request($request);
    warn "[Response]\n" , $response->as_string if $self->{debug};
    return $response;
}

# API calls

sub get_object {
    my ($self, $bucket, $key, $headers) = @_;
    my $request = $self->_compose_request('GET', $self->_resource($bucket, $key), $headers);
    return $self->_send($request);
}

sub head_object {
    my ($self, $bucket, $key) = @_;
    my $request = $self->_compose_request('HEAD', $self->_resource($bucket, $key));
    return $self->_send($request);
}

sub delete_object {
    my ($self, $bucket, $key) = @_;
    my $request = $self->_compose_request('DELETE', $self->_resource($bucket, $key));
    return $self->_send($request);
}

sub copy_object {
    my ($self, $src_bucket, $src_key, $dst_bucket, $dst_key, $headers) = @_;
    $headers ||= {};
    $headers->{'x-amz-copy-source'} = $src_bucket . "/" . $src_key;
    my $request = $self->_compose_request('PUT', $self->_resource($dst_bucket, $dst_key), $headers);
    my $res = $self->_send($request);

    # XXX: Since the COPY request might return error response in 200 OK, we'll rewrite the status code to 500 for convenience
    # ref http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectCOPY.html
    # ref https://github.com/boto/botocore/blob/4e9b4419ec018716ab1a3fe1587fbdc3cfef200e/botocore/handlers.py#L77-L120
    if ($self->_looks_like_special_case_error($res)) {
        $res->code(500);
    }
    return $res;
}

sub _looks_like_special_case_error {
    my ($self, $res) = @_;
    return $res->code == 200 && (length $res->content == 0 || $res->content =~ /<Error>/);
}

sub put_object {
    my ($self, $bucket, $key, $content, $headers) = @_;
    croak 'must specify key' unless $key && length $key;

    if ($headers->{acl_short}) {
        $self->_validate_acl_short($headers->{acl_short});
        $headers->{'x-amz-acl'} = $headers->{acl_short};
        delete $headers->{acl_short};
    }

    if (ref($content) eq 'SCALAR') {
        $headers->{'Content-Length'} ||= -s $$content;
        $content = _content_sub($$content);
    }
    else {
        $headers->{'Content-Length'} ||= length $content;
    }

    if (ref($content)) {
        # TODO
        # I do not understand what it is :(
        #
        # return $self->_send_request_expect_nothing_probed('PUT',
        #    $self->_resource($bucket, $key), $headers, $content);
        #
        die "unable to handle reference";
    }
    else {
        my $request = $self->_compose_request('PUT', $self->_resource($bucket, $key), $headers, $content);
        return $self->_send($request);
    }
}

sub list_objects {
    my ($self, $bucket, $opt) = @_;
    croak 'must specify bucket' unless $bucket;
    $opt ||= {};

    my $query_string;
    if (%$opt) {
        $query_string = join('&',
                 map { $_ . "=" . Amazon::S3::Thin::Resource->urlencode($opt->{$_}) } sort keys %$opt);
    }

    my $resource = $self->_resource($bucket, undef, $query_string);
    my $request = $self->_compose_request('GET', $resource);
    my $response = $self->_send($request);
    return $response;
}

sub delete_multiple_objects {
    my ($self, $bucket, @keys) = @_;

    my $content = _build_xml_for_delete(@keys);
    # XXX: specify an empty string with `delete` query for calculating signature correctly in AWS::Signature4
    my $resource = $self->_resource($bucket, undef, 'delete=');
    my $request = $self->_compose_request(
        'POST',
        $resource,
        {
            'Content-MD5'    => Digest::MD5::md5_base64($content) . '==',
            'Content-Length' => length $content,
        },
        $content
    );
    my $response = $self->_send($request);

lib/Amazon/S3/Thin.pm  view on Meta::CPAN

    $content .= '</Delete>';

    return $content;
}

# Operations on Buckets

sub put_bucket {
    my ($self, $bucket, $headers) = @_;
    # 
    # https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
    my $region = $self->{region};
    my $content ;
    if ($region eq "us-east-1") {
        $content = "";
    } else {
        my $location_constraint = "<LocationConstraint>$region</LocationConstraint>";
        $content = <<"EOT";
<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">$location_constraint</CreateBucketConfiguration>
EOT
    }

    my $request = $self->_compose_request('PUT', $self->_resource($bucket), $headers, $content);
    return $self->_send($request);
}

sub delete_bucket {
    my ($self, $bucket) = @_;
    my $request = $self->_compose_request('DELETE', $self->_resource($bucket));
    return $self->_send($request);
}

sub generate_presigned_post {
    my ($self, $bucket, $key, $fields, $conditions, $expires_in) = @_;

    croak 'must specify bucket' unless defined $bucket;
    croak 'must specify key' unless defined $key;

    if ($self->{signature_version} == 4) {
        my $resource = $self->_resource($bucket);
        my $protocol = $self->secure ? 'https' : 'http';

        return {
            ($self->virtual_host
                ? (url => $resource->to_virtual_hosted_style_url($protocol))
                : (url => $resource->to_path_style_url($protocol, $self->{region}))),
            fields => $self->{signer}->_generate_presigned_post(
                $bucket, $key, $fields, $conditions, $expires_in
            ),
        };
    } else {
        croak 'generate_presigned_post is only supported on signature v4';
    }
}

sub _resource {
    my ($self, $bucket, $key, $query_string) = @_;
    return Amazon::S3::Thin::Resource->new($bucket, $key, $query_string);
}

sub _validate_acl_short {
    my ($self, $policy_name) = @_;

    if (!grep({$policy_name eq $_}
            qw(private public-read public-read-write authenticated-read)))
    {
        croak "$policy_name is not a supported canned access policy";
    }
}

# make the HTTP::Request object
sub _compose_request {
    my ($self, $method, $resource, $headers, $content, $metadata) = @_;
    croak 'must specify method' unless $method;
    croak 'must specify resource'   unless defined $resource;
    if (ref $resource ne 'Amazon::S3::Thin::Resource') {
        croak 'resource must be an instance of Amazon::S3::Thin::Resource';
    }
    $headers ||= {};
    $metadata ||= {};

    # generates an HTTP::Headers objects given one hash that represents http
    # headers to set and another hash that represents an object's metadata.
    my $http_headers = HTTP::Headers->new;
    while (my ($k, $v) = each %$headers) {
        $http_headers->header($k => $v);
    }
    while (my ($k, $v) = each %$metadata) {
        $http_headers->header("$METADATA_PREFIX$k" => $v);
    }

    my $protocol = $self->secure ? 'https' : 'http';

    my $url;

    if ($self->{signature_version} == 4) {
        if ($self->virtual_host) {
            $url = $resource->to_virtual_hosted_style_url($protocol);
        } else {
            $url = $resource->to_path_style_url($protocol, $self->{region});
        }
    } else {
        $url = $resource->to_url_without_region($protocol, $MAIN_HOST);
    }

    my $request = HTTP::Request->new($method, $url, $http_headers, $content);
    # sign the request using the signer, unless already signed
    if (!$request->header('Authorization')) {
        $self->{signer}->sign($request);
    }
    return $request;
}

1;

__END__

=head1 NAME

Amazon::S3::Thin - A thin, lightweight, low-level Amazon S3 client



( run in 1.443 second using v1.01-cache-2.11-cpan-140bd7fdf52 )