#!perl use warnings; use strict; use lib 'lib'; use Digest::MD5::File qw(file_md5_hex); use LWP::Simple; use File::stat; use Test::More; use Test::Exception; use File::Temp qw/ :seekable /; unless ( $ENV{'AMAZON_S3_EXPENSIVE_TESTS'} ) { plan skip_all => 'Testing this module for real costs money.'; } else { plan tests => 53; } use_ok('Net::Amazon::S3'); my $aws_access_key_id = $ENV{'AWS_ACCESS_KEY_ID'}; my $aws_secret_access_key = $ENV{'AWS_ACCESS_KEY_SECRET'}; my $s3 = Net::Amazon::S3->new( aws_access_key_id => $aws_access_key_id, aws_secret_access_key => $aws_secret_access_key, retry => 1, ); my $readme_size = stat('README.md')->size; my $readme_md5hex = file_md5_hex('README.md'); my $client = Net::Amazon::S3::Client->new( s3 => $s3 ); my @buckets = $client->buckets; my $bucket_name = 'net-amazon-s3-test-' . lc($aws_access_key_id) . '-'. time; my $bucket = $client->create_bucket( name => $bucket_name, acl_short => 'public-read', location_constraint => 'EU', ); eval { is( $bucket->name, $bucket_name, 'newly created bucket has correct name' ); like( $bucket->acl, qr{<AccessControlPolicy xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Owner><ID>[a-z0-9]{64}</ID><DisplayName>.+?</DisplayName></Owner><AccessControlList><Grant><Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="CanonicalUser"><ID>[a-z0-9]{64}</ID><DisplayName>.+?</DisplayName></Grantee><Permission>FULL_CONTROL</Permission></Grant><Grant><Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Group"><URI>http://acs.amazonaws.com/groups/global/AllUsers</URI></Grantee><Permission>READ</Permission></Grant></AccessControlList></AccessControlPolicy>}, 'newly created bucket is public-readable' ); is( $bucket->location_constraint, 'eu-west-1', 'newly created bucket is in the EU' ); my $stream = $bucket->list; until ( $stream->is_done ) { foreach my $object ( $stream->items ) { $object->delete; } } my $count = 0; $stream = $bucket->list; until ( $stream->is_done ) { foreach my $object ( $stream->items ) { $count++; } } is( $count, 0, 'newly created bucket has no objects' ); my $object = $bucket->object( key => 'this is the key' ); ok( !$object->exists, 'object does not exist yet' ); $object->put('this is the value'); ok( $object->exists, 'object now exists yet' ); my @objects; @objects = (); $stream = $bucket->list( { prefix => 'this is the key' } ); until ( $stream->is_done ) { foreach my $object ( $stream->items ) { push @objects, $object; } } is( @objects, 1, 'bucket list with prefix finds key' ); @objects = (); $stream = $bucket->list( { prefix => 'this is not the key' } ); until ( $stream->is_done ) { foreach my $object ( $stream->items ) { push @objects, $object; } } is( @objects, 0, 'bucket list with different prefix does not find key' ); @objects = (); $stream = $bucket->list; until ( $stream->is_done ) { foreach my $object ( $stream->items ) { push @objects, $object; } } is( @objects, 1, 'bucket list finds newly created key' ); is( $objects[0]->key, 'this is the key', 'newly created object has the right key' ); is( $objects[0]->etag, '94325a12f8db22ffb6934cc5f22f6698', 'newly created object has the right etag' ); is( $objects[0]->size, '17', 'newly created object has the right size' ); is( $object->get, 'this is the value', 'newly created object has the right value' ); is( $bucket->object( key => 'this is the key' )->get, 'this is the value', 'newly created object fetched by name has the right value' ); is( get( $object->uri ), undef, 'newly created object cannot be fetched by uri' ); $object->expires('2037-01-01'); is( get( $object->query_string_authentication_uri() ), 'this is the value', 'newly created object can be fetch by authentication uri' ); my $signed_url = $object->query_string_authentication_uri({ 'response-content-disposition' => 'attachment; filename=abc.doc' }); like( $signed_url, qr/response-content-disposition/, 'cuttom response headers included in the signed uri' ); is( get( $signed_url ), 'this is the value', 'newly created object can be fetch by authentication uri with custom headers' ); $object->delete; # upload a public object $object = $bucket->object( key => 'this is the public key', acl_short => 'public-read', content_type => 'text/plain', content_encoding => 'identity', expires => '2001-02-03', ); $object->put('this is the public value'); is( get( $object->uri ), 'this is the public value', 'newly created public object is publically accessible' ); is( ( head( $object->uri ) )[0], 'text/plain', 'newly created public object has the right content type' ); is( ( head( $object->uri ) )[3], $object->expires->epoch, 'newly created public object has the right expires' ); $object->delete; # delete a non-existant object $object = $bucket->object( key => 'not here' ); throws_ok { $object->get } qr/NoSuchKey/, 'getting non-existant object throws exception'; # upload a file with put_filename $object = $bucket->object( key => 'the readme' ); $object->put_filename('README.md'); @objects = (); $stream = $bucket->list; until ( $stream->is_done ) { foreach my $object ( $stream->items ) { push @objects, $object; } } is( @objects, 1, 'have newly uploaded object' ); is( $objects[0]->key, 'the readme', 'newly uploaded object has the right key' ); is( $objects[0]->etag, $readme_md5hex, 'newly uploaded object has the right etag' ); is( $objects[0]->size, $readme_size, 'newly created object has the right size' ); ok( $objects[0]->last_modified, 'newly created object has a last modified' ); $object->delete; # upload a public object with put_filename $object = $bucket->object( key => 'the public readme', acl_short => 'public-read' ); $object->put_filename('README.md'); is( length( get( $object->uri ) ), $readme_size, 'newly uploaded public object has the right size' ); $object->delete; # upload a file with put_filename with known md5hex size and AES256 encryption $object = $bucket->object( key => 'the new readme', etag => $readme_md5hex, size => $readme_size, encryption => 'AES256' ); $object->put_filename('README.md'); @objects = (); $stream = $bucket->list; until ( $stream->is_done ) { foreach my $object ( $stream->items ) { push @objects, $object; } } is( @objects, 1, 'have newly uploaded object' ); is( $objects[0]->key, 'the new readme', 'newly uploaded object has the right key' ); is( $objects[0]->etag, $readme_md5hex, 'newly uploaded object has the right etag' ); is( $objects[0]->size, $readme_size, 'newly created object has the right size' ); ok( $objects[0]->last_modified, 'newly created object has a last modified' ); # download an object with get_filename my $tmp_fh = File::Temp->new(); $object->get_filename($tmp_fh->filename); is( stat($tmp_fh->filename)->size, $readme_size, 'download has right size' ); is( file_md5_hex($tmp_fh->filename), $readme_md5hex, 'download has right etag' ); $object->delete; # upload a public object with put_filename with known md5hex and size $object = $bucket->object( key => 'the new public readme', etag => $readme_md5hex, size => $readme_size, acl_short => 'public-read' ); $object->put_filename( 'README.md', $readme_md5hex, $readme_size ); is( length( get( $object->uri ) ), $readme_size, 'newly uploaded public object has the right size' ); $object->delete; { # upload an object using multipart upload and then abort it $object = $bucket->object( key => 'new multipart file soon to be aborted', acl_short => 'public-read' ); my $upload_id; ok( $upload_id = $object->initiate_multipart_upload, "can initiate a new multipart upload -- $upload_id" ); #put part my $put_part_response; ok( $put_part_response = $object->put_part( part_number => 1, upload_id => $upload_id, value => 'x' x ( 5 * 1024 * 1024 ) ), 'Got a successful response for PUT part' ); ok( $put_part_response->header('ETag'), 'etag ok' ); ok( my $abort_response = $object->abort_multipart_upload( upload_id => $upload_id ), 'Got a successful response for DELETE multipart upload' ); ok( !$object->exists, "object has now been deleted" ); } # upload an object using multipart upload $object = $bucket->object( key => 'new multipart file', acl_short => 'public-read' ); my $upload_id; ok($upload_id = $object->initiate_multipart_upload, "can initiate a new multipart upload"); #put part my $put_part_response; ok( $put_part_response = $object->put_part(part_number => 1, upload_id => $upload_id, value => 'x' x (5 * 1024 * 1024)), 'Got a successful response for PUT part' ); my @etags; push @etags, $put_part_response->header('ETag'); ok( $put_part_response = $object->put_part(part_number => 2, upload_id => $upload_id, value => 'z' x (1024 * 1024)), 'Got a successful response for 2nd PUT part' ); push @etags, $put_part_response->header('ETag'); # TODO list part? - We've got this, but how to expose it nicely? #complete multipart upload my $complete_upload_response; ok( $complete_upload_response = $object->complete_multipart_upload( upload_id => $upload_id, part_numbers => [1,2], etags => \@etags), "successful response for complete multipart upload" ); #get the file and check that it looks like we expect ok($object->exists, "object has now been created"); $tmp_fh = File::Temp->new(); $object->get_filename($tmp_fh->filename); is( stat($tmp_fh->filename)->size, 6 * 1024 * 1024, "downloaded file has a size equivalent to the sum of it's parts"); $tmp_fh->seek((5 * 1024 * 1024) - 1, SEEK_SET); #jump to 5MB position my $test_bytes; read($tmp_fh, $test_bytes, 2); is($test_bytes, "xz", "The second chunk of the file begins in the correct place"); #test listing a multipart object $stream = $bucket->list({prefix => 'new multipart file'}); lives_ok {my @items = $stream->items} 'Listing a multipart file does not throw an exeption'; $object->delete; #test multi-object delete #make 3 identical objects @objects =(); for my $i (1..3) { my $bulk_object = $bucket->object( key => "bulk-readme-$i", etag => $readme_md5hex, size => $readme_size ); $bulk_object->put_filename('README.md'); push @objects, $bulk_object; } #now delete 2 of those objects ok($bucket->delete_multi_object(@objects[0..1]), "executed multi delete operation"); ok( !grep($_->exists, @objects[0..1]), "target objects no longer exist"); ok( $objects[2]->exists, "object not included in multi-object delete still exists" ); $objects[2]->delete; }; $bucket->delete;