package SOAP::Amazon::S3; use warnings; use strict; use Time::Piece; use Digest::HMAC_SHA1; use MIME::Base64 qw(encode_base64 decode_base64); use XML::MyXML 0.098061 qw(tidy_xml simple_to_xml xml_to_object); use SOAP::MySOAP 0.023; use Carp; use Data::Dumper; =head1 NAME SOAP::Amazon::S3 - A module for interfacing with Amazon S3 through SOAP =head1 VERSION Version 0.0401 =cut our $VERSION = '0.0401'; =head1 SYNOPSIS An object-oriented interface to handle your Amazon S3 storage. (Still experimental, although functional) use SOAP::Amazon::S3; my $s3 = SOAP::Amazon::S3->new( $access_key_id, $secret_access_key, { Debug => 1, RaiseError => 1 } ); my @buckets = $s3->listbuckets; my $bucket = $s3->createbucket('mybucketname'); my $bucket = $s3->bucket('myoldbucket'); # won't create a new bucket print $bucket->name; $bucket->delete; my @objects = $bucket->list; my $object = $bucket->putobject( $obj_key, $obj_data, { 'Content-Type' => 'text/plain' } ); my $object = $bucket->object( $old_obj_key ); # won't put a new object in the bucket print $object->name; $object->delete; $object->acl('public'); $object->acl('private'); print $object->acl(); # will print 'public' or 'private' $data = $object->getdata; =head1 FUNCTIONS =head2 SOAP::Amazon::S3->new( $access_key_id, $secret_key_id, { Debug => 0_or_1, RaiseError => 0_or_1 } ); Creates a new S3 requester object. The {} parameters are optional and default to 0. Debug will output all SOAP communications on screen. RaiseError will make your program die if it receives an error reply from Amazon S3, and output the error message on screen. If RaiseError is off, then $s3->{'error'} will still be set to true when an S3 error occurs. =cut sub new { my $class = shift; my $access_key_id = shift; my $secret_access_key = shift; my $params = shift || {}; my $self = { access_key_id => $access_key_id, secret_access_key => $secret_access_key, soaper => SOAP::MySOAP->new('https://s3.amazonaws.com/soap'), %$params, }; $self->{'soaper'}->{'ua'}->default_header( Accept => 'text/xml' ); bless $self, $class; } sub _send { my $self = shift; my $command = shift; my @params = @_; my $t = gmtime; my $timestamp = $t->datetime.".000Z"; my $canonical = "AmazonS3$command$timestamp"; my $hmac = Digest::HMAC_SHA1->new($self->{'secret_access_key'}); $hmac->add($canonical); my $signature = encode_base64($hmac->digest, ''); push @params, ( 'AWSAccessKeyId' => $self->{'access_key_id'} ); push @params, ( 'Timestamp' => $timestamp ); push @params, ( 'Signature' => $signature ); my $xml_body = simple_to_xml([ "tns:$command xsi:nil=\"true\"" => \@params ]); # # my $xml = < EOB $xml .= $xml_body."\n"; $xml .= < EOB $xml = tidy_xml($xml); my $result = $self->{'soaper'}->request($xml); if ($self->{'Debug'}) { print $self->{'soaper'}->{'request'}->as_string; print "\n\n\n"; print $self->{'soaper'}->{'response'}->headers->as_string; print "\n"; print &XML::MyXML::tidy_xml($self->{'soaper'}->{'response'}->content); print "\n"; } my $obj = &xml_to_object($self->{'soaper'}{'response'}->content); $self->{'error'} = $obj->path("Body/Fault"); if ($self->{'RaiseError'} and $self->{'error'}) { print "\nAmazon returned Fault:\n"; print Dumper($self->{'error'}->simplify); $obj->delete(); confess; } my $ret = $self->{'soaper'}->{'response'}->content; $obj->delete(); return $ret if defined wantarray; } =head1 OBJECT METHODS =head2 $s3->listbuckets Returns the list of buckets in SOAP::Amazon::S3::Bucket form =cut sub listbuckets { my $self = shift; my $xml = $self->ListAllMyBuckets; my $obj = &xml_to_object($xml); my @buckets = map {$_->simplify} $obj->path("Body/ListAllMyBucketsResponse/ListAllMyBucketsResponse/Buckets/Bucket"); $obj->delete(); foreach my $bucket (@buckets) { $bucket->{'_s3'} = $self; $bucket->{'Name'} = $bucket->{'Bucket'}{'Name'}; bless $bucket, 'SOAP::Amazon::S3::Bucket'; } return @buckets; } =head2 $s3->createbucket( $bucket_name ) Creates a bucket named $bucket_name in your S3 space and returns the appropriate ...::S3::Bucket type object for further use =cut sub createbucket { my $self = shift; my $name = shift; my $xml = $self->CreateBucket( Bucket => $name ); bless { _s3 => $self, Name => $name }, 'SOAP::Amazon::S3::Bucket' unless $self->{'error'}; } =head2 $s3->bucket( $bucket_name ) Returns an ...::S3::Bucket type object, corresponding to an already existing bucket in your S3 space, named $bucket_name =cut sub bucket { my $self = shift; my $name = shift; bless { _s3 => $self, Name => $name }, 'SOAP::Amazon::S3::Bucket'; } our $AUTOLOAD; sub AUTOLOAD { my $self = shift; my @params = @_; my $command = $AUTOLOAD; $command =~ s/^.*\:\://; return $self->_send($command, @_); } sub DESTROY { } package SOAP::Amazon::S3::Bucket; =head2 $bucket->delete Deletes the bucket if empty. If not empty, Amazon S3 returns an error (viewable in $s3->{'error'}) =cut sub delete { my $self = shift; $self->{'_s3'}->DeleteBucket( Bucket => $self->{'Name'} ); } =head2 $bucket->list Returns the list of objects in the bucket, in the form of ..::S3::Object type objects =cut sub list { my $self = shift; my $s3 = $self->{'_s3'}; my $xml = $s3->ListBucket(Bucket => $self->{'Name'}); my $obj = &XML::MyXML::xml_to_object($xml); my @objects = map {values %{$_->simplify}} $obj->path('Body/ListBucketResponse/ListBucketResponse/Contents'); $obj->delete(); foreach my $object (@objects) { $object->{'_s3'} = $s3; $object->{'Bucket'} = $self->{'Name'}; $object->{'_bucket'} = $self; bless $object, 'SOAP::Amazon::S3::Object'; } return @objects; } =head2 $bucket->name Returns the name of the bucket =cut sub name { my $self = shift; return $self->{'Name'}; } =head2 $bucket->putobject( $obj_key, $obj_data, { 'Content-Type' => $mime_type, metadata => {color=>'red', feel=>'soft'} } ) Creates an object in the S3 bucket, named $obj_key. The {} section is optional, and may contain the Content-Type (defaults to 'text/plain') and a metadata hashref. Returns an ...::S3::Object type object pointing to the object just created, if successful. =cut sub putobject { my $self = shift; my $key = shift; my $data = shift; my $options = shift || {}; my $metadata = $options->{'metadata'} || {}; $metadata->{'Content-Type'} ||= $options->{'Content-Type'} || 'text/plain'; $metadata = [ map {(MetaData => [Name => $_, Value => $metadata->{$_}])} keys %$metadata ]; my $s3 = $self->{'_s3'}; $s3->PutObjectInline( Bucket => $self->name, Key => $key, @$metadata, Data => MIME::Base64::encode_base64($data), ContentLength => length($data) ); bless { _s3 => $s3, Bucket => $self->name, _bucket => $self, Key => $key }, 'SOAP::Amazon::S3::Object' unless $s3->{'error'}; } =head2 $bucket->object( $old_obj_key ) Returns an ...::S3::Object type object, corresponding to an already created object in the S3 bucket, named $old_obj_key =cut sub object { my $self = shift; my $key = shift; my $s3 = $self->{'_s3'}; bless { _s3 => $s3, Bucket => $self->name, _bucket => $self, Key => $key }, 'SOAP::Amazon::S3::Object'; } package SOAP::Amazon::S3::Object; =head2 $object->name Returns the Key attribute of an object =cut sub name { my $self = shift; return $self->{'Key'}; } =head2 $object->delete Deletes the object =cut sub delete { my $self = shift; my $s3 = $self->{'_s3'}; my $bucket = $self->{'_bucket'}; $s3->DeleteObject( Bucket => $bucket->name, Key => $self->name ); } =head2 $object->acl( 'public' or 'private' or nothing ) Gets or sets the object's ACL, making it public (and viewable through the web) or private just to you. If no parameter is entered, returns either 'public' or 'private'. =cut sub acl { my $self = shift; my $what = shift; my $s3 = $self->{'_s3'}; my $bucket = $self->{'_bucket'}; if ($what) { if (lc($what) eq 'public') { $s3->SetObjectAccessControlPolicy( Bucket => $bucket->name, Key => $self->name, AccessControlList => [ Grant => [ 'Grantee xsi:type="Group"' => [ URI => 'http://acs.amazonaws.com/groups/global/AllUsers' ], Permission => 'READ' ] ] ); } elsif (lc($what) eq 'private') { $s3->SetObjectAccessControlPolicy( Bucket => $bucket->name, Key => $self->name, AccessControlList => [ ] ); } else { Carp::confess "Invalid policy: '$what' - valid policies are 'public' and 'private'"; } } else { my $resp = $s3->GetObjectAccessControlPolicy( Bucket => $bucket->name, Key => $self->name ); my $xml = &XML::MyXML::xml_to_object($resp); my @grants = $xml->path('Body/GetObjectAccessControlPolicyResponse/GetObjectAccessControlPolicyResponse/AccessControlList/Grant'); foreach my $grant (@grants) { my $uri = $grant->path('Grantee/URI'); if ($uri and $uri->value eq 'http://acs.amazonaws.com/groups/global/AllUsers') { $xml->delete(); return 'public'; } } $xml->delete(); return 'private'; } } =head2 $object->getdata Returns the data of the object, after fetching it from S3 =cut sub getdata { my $self = shift; my $s3 = $self->{'_s3'}; my $bucket = $self->{'_bucket'}; my $resp = $s3->GetObject( Bucket => $bucket->name, Key => $self->name, GetMetadata => 'false', GetData => 'true', InlineData => 'true' ); return if $s3->{'error'}; my $obj = &XML::MyXML::xml_to_object($resp); my $data = $obj->path('Body/GetObjectResponse/GetObjectResponse/Data'); if ($data) { $data = $data->value; } else { $obj->delete(); return; } $obj->delete(); return MIME::Base64::decode_base64($data); } =head2 $object->url Return the URL of the object =cut sub url { my $self = shift; return "http://s3.amazonaws.com/".$self->{'_bucket'}->name()."/".$self->name(); } =head1 AUTHOR Alexander Karelas, C<< >> =head1 BUGS Please report any bugs or feature requests to C, or through the web interface at L. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. =head1 SUPPORT You can find documentation for this module with the perldoc command. perldoc SOAP::Amazon::S3 You can also look for information at: =over 4 =item * AnnoCPAN: Annotated CPAN documentation L =item * CPAN Ratings L =item * RT: CPAN's request tracker L =item * Search CPAN L =item * Module's RSS feed L =back =head1 ACKNOWLEDGEMENTS =head1 COPYRIGHT & LICENSE Copyright 2006-2007 Alexander Karelas, all rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut 1; # End of SOAP::Amazon::S3