File Coverage

blib/lib/WebService/Rackspace/CloudFiles.pm
Criterion Covered Total %
statement 67 91 73.6
branch 6 22 27.2
condition 3 12 25.0
subroutine 15 18 83.3
pod 4 4 100.0
total 95 147 64.6


line stmt bran cond sub pod time code
1             package WebService::Rackspace::CloudFiles;
2 3     3   627546 use Moo;
  3         28431  
  3         14  
3 3     3   6103 use MooX::StrictConstructor;
  3         43178  
  3         15  
4 3     3   72423 use Types::Standard qw(Bool Str Num Int HashRef InstanceOf ClassName);
  3         250253  
  3         29  
5 3     3   6063 use DateTime::Format::HTTP;
  3         1600887  
  3         149  
6 3     3   1810 use WebService::Rackspace::CloudFiles::Container;
  3         11  
  3         110  
7 3     3   1422 use WebService::Rackspace::CloudFiles::Object;
  3         22  
  3         106  
8 3     3   26 use WebService::Rackspace::CloudFiles::Object::Iterator;
  3         7  
  3         74  
9 3     3   1353 use WebService::Rackspace::CloudFiles::ConnCache;
  3         10  
  3         88  
10 3     3   1558 use LWP::UserAgent::Determined;
  3         1635  
  3         91  
11 3     3   1447 use URI::QueryParam;
  3         2409  
  3         100  
12 3     3   24 use JSON::Any;
  3         7  
  3         29  
13 3     3   460 use Carp qw(confess);
  3         9  
  3         4550  
14             our $VERSION = '2.03';
15              
16             my $DEBUG = 0;
17              
18             has 'user' => ( is => 'ro', isa => Str, required => 1 );
19             has 'key' => ( is => 'ro', isa => Str, required => 1 );
20             has 'location'=> ( is => 'ro', isa => Str, required => 0, default => 'usa');
21             has 'timeout' => ( is => 'ro', isa => Num, required => 0, default => 30 );
22             has 'retries' => ( is => 'ro', isa => Str, required => 0, default => '1,2,4,8,16,32' );
23              
24             has 'connection_cache_class' => (
25             is => 'ro',
26             isa => ClassName,
27             default => 'WebService::Rackspace::CloudFiles::ConnCache'
28             );
29              
30             has 'iterator_callback_class' => (
31             is => 'ro',
32             isa => ClassName,
33             default => 'WebService::Rackspace::CloudFiles::Object::Iterator'
34             );
35              
36             has locations => (
37             traits => [ 'Hash' ],
38             isa => HashRef,
39             is => 'ro',
40             default => sub {
41             return {
42             uk => 'https://lon.auth.api.rackspacecloud.com/v1.0',
43             usa => 'https://auth.api.rackspacecloud.com/v1.0',
44             },
45             },
46             handles => {
47             location_names => 'keys',
48             },
49             );
50              
51             has location_url => (
52             is => 'ro',
53             isa => Str,
54             lazy => 1,
55             required => 0,
56             default => sub {
57             my $self = shift;
58              
59             return $self->locations->{$self->location} ||
60             confess "location $self->{location} unknown: valid locations are " .
61             join ', ', $self->location_names ;
62             },
63             );
64              
65             has 'ua' => (
66             is => 'ro',
67             isa => InstanceOf['LWP::UserAgent'],
68             required => 0,
69             lazy => 1,
70             builder => '_build_ua',
71             );
72              
73             has storage_url => (
74             is => 'rw',
75             isa => Str,
76             required => 0,
77             lazy => 1,
78             default => sub {
79             my $self = shift;
80             $self->_authenticate;
81             $self->storage_url;
82             },
83             );
84              
85             has cdn_management_url => (
86             is => 'rw',
87             isa => Str,
88             required => 0,
89             lazy => 1,
90             default => sub {
91             my $self = shift;
92             $self->_authenticate;
93             $self->cdn_management_url;
94             },
95             );
96              
97             has token => (
98             is => 'rw',
99             isa => Str,
100             required => 0,
101             lazy => 1,
102             default => sub {
103             my $self = shift;
104             $self->_authenticate;
105             $self->token;
106             },
107             );
108              
109             has is_authenticated => (
110             is => 'rw',
111             isa => Bool,
112             required => 0,
113             default => 0,
114             );
115              
116             sub _build_ua {
117 0     0   0 my $self = shift;
118              
119 0         0 my $ua = LWP::UserAgent::Determined->new(
120             keep_alive => 10,
121             requests_redirectable => [qw(GET HEAD DELETE PUT)],
122             );
123 0         0 $ua->timing($self->retries);
124 0         0 $ua->conn_cache(
125             $self->connection_cache_class->new(
126             total_capacity => 10,
127             max_keep_alive_requests => 990,
128             )
129             );
130 0         0 my $http_codes_hr = $ua->codes_to_determinate();
131 0         0 $http_codes_hr->{422} = 1; # used by cloudfiles for upload data corruption
132 0         0 $ua->timeout( $self->timeout );
133 0         0 $ua->env_proxy;
134              
135 0         0 return $ua;
136             }
137              
138             sub _authenticate {
139 3     3   266 my $self = shift;
140              
141 3         65 my $request = HTTP::Request->new(
142             'GET',
143             $self->location_url,
144             [ 'X-Auth-User' => $self->user,
145             'X-Auth-Key' => $self->key,
146             ]
147             );
148 3         17480 $self->is_authenticated(1); # needed to prevent infinite recursion on auth requests
149 3         175 my $response = $self->_request($request);
150 3         75 $self->is_authenticated(0);
151              
152 3 50       85 confess 'Unauthorized' if $response->code == 401;
153 3 50       56 confess 'Unknown error' if !$response->is_success;
154              
155 3   33     44 my $storage_url = $response->header('X-Storage-Url')
156             || confess 'Missing storage url';
157 3   33     152 my $token = $response->header('X-Auth-Token')
158             || confess 'Missing auth token';
159 3   33     127 my $cdn_management_url = $response->header('X-CDN-Management-Url')
160             || confess 'Missing CDN management url';
161              
162 3         196 $self->storage_url($storage_url);
163 3         170 $self->token($token);
164 3         159 $self->cdn_management_url($cdn_management_url);
165              
166 3         155 $self->is_authenticated(1);
167             }
168              
169             before _request => sub {
170             my $self = shift;
171              
172             $self->_authenticate unless $self->is_authenticated;
173             };
174              
175             sub _request {
176             my ( $self, $request, $filename ) = @_;
177             warn $request->as_string if $DEBUG;
178             my $response = $self->ua->request( $request, $filename );
179             warn $response->as_string if $DEBUG;
180             if ( $response->code == 401 && $request->header('X-Auth-Token') ) {
181              
182             # http://trac.cyberduck.ch/ticket/2876
183             # Be warned that the token will expire over time (possibly as short
184             # as an hour). The application should trap a 401 (Unauthorized)
185             # response on a given request (to either storage or cdn system)
186             # and then re-authenticate to obtain an updated token.
187             $self->is_authenticated(0);
188             $self->_authenticate;
189             $request->header( 'X-Auth-Token', $self->token );
190             warn $request->as_string if $DEBUG;
191             $response = $self->ua->request( $request, $filename );
192             warn $response->as_string if $DEBUG;
193             }
194             return $response;
195             }
196              
197             sub containers {
198 1     1 1 206 my $self = shift;
199 1         25 my $request = HTTP::Request->new( 'GET',
200             $self->storage_url . '?format=json',
201             [ 'X-Auth-Token' => $self->token ] );
202 1         290 my $response = $self->_request($request);
203 1 50       4 return if $response->code == 204;
204 1 50       14 confess 'Unknown error' if !$response->is_success;
205 1         8 my @containers;
206              
207 1         3 foreach my $container_data ( @{JSON::Any->from_json($response->content)} ) {
  1         4  
208 1         56 push @containers, $self->container(%{$container_data});
  1         6  
209             }
210 1         6408 return @containers;
211             }
212              
213             sub total_bytes_used {
214 0     0 1 0 my $self = shift;
215 0         0 my $request = HTTP::Request->new( 'HEAD', $self->storage_url,
216             [ 'X-Auth-Token' => $self->token ] );
217 0         0 my $response = $self->_request($request);
218 0 0       0 confess 'Unknown error' unless $response->is_success;
219 0         0 my $total_bytes_used = $response->header('X-Account-Bytes-Used');
220 0 0       0 $total_bytes_used = 0 if $total_bytes_used eq 'None';
221 0         0 return $total_bytes_used;
222             }
223              
224             sub container {
225 1     1 1 4 my ( $self, %conf ) = @_;
226 1 50       5 confess 'Missing name' unless $conf{name};
227 1         5 $conf{cloudfiles} = $self;
228 1         4 for (keys %conf){
229 2 50       98 if (ref $conf{$_} eq ref JSON::Any->true){
230 0 0       0 $conf{$_} = ($conf{$_} ? 'true' : 'false');
231             }
232             }
233              
234 1         30 return WebService::Rackspace::CloudFiles::Container->new(%conf);
235             }
236              
237             sub create_container {
238 0     0 1   my ( $self, %conf ) = @_;
239 0           my $name = $conf{name};
240 0 0         confess 'Missing name' unless $name;
241              
242 0           my $request = HTTP::Request->new(
243             'PUT',
244             $self->storage_url . '/' . $name,
245             [ 'X-Auth-Token' => $self->token ]
246             );
247 0           my $response = $self->_request($request);
248 0 0 0       confess 'Unknown error'
249             if $response->code != 201 && $response->code != 202;
250 0           return WebService::Rackspace::CloudFiles::Container->new(
251             cloudfiles => $self,
252             name => $name,
253             );
254             }
255              
256             __PACKAGE__->meta->make_immutable;
257              
258             1;
259              
260             __END__
261              
262             =head1 NAME
263              
264             WebService::Rackspace::CloudFiles - Interface to Rackspace CloudFiles service
265              
266             =head1 SYNOPSIS
267              
268             use WebService::Rackspace::CloudFiles;
269             use Perl6::Say;
270              
271             my $cloudfiles = WebService::Rackspace::CloudFiles->new(
272             user => 'myusername',
273             key => 'mysecretkey',
274             );
275              
276             # list all containers
277             my @containers = $cloudfiles->containers;
278             foreach my $container (@containers) {
279             say 'have container ' . $container->name;
280             }
281              
282             # create a new container
283             my $container = $cloudfiles->create_container(name => 'testing');
284              
285             # use an existing container
286             my $existing_container = $cloudfiles->container(name => 'testing');
287              
288             my $total_bytes_used = $cloudfiles->total_bytes_used;
289             say "used $total_bytes_used";
290              
291             my $object_count = $container->object_count;
292             say "$object_count objects";
293              
294             my $bytes_used = $container->bytes_used;
295             say "$bytes_used bytes";
296              
297             # returns a Data::Stream::Bulk object
298             # as it may have to make multiple HTTP requests
299             my @objects = $container->objects->all;
300             foreach my $object (@objects) {
301             say 'have object ' . $object->name;
302             # also size, etag, content_type, last_modified
303             }
304             my @objects2 = $container->objects(prefix => 'dir/')->all;
305              
306             # To create a new object
307             my $xxx = $container->object( name => 'XXX' );
308             $xxx->put('this is the value');
309              
310             # To set metadata of an object:
311             $xxx->object_metadata({
312             description => 'this is a description',
313             useful_number => 17
314             });
315            
316             # To create a new object with the contents of a local file
317             my $yyy = $container->object( name => 'YYY', content_type => 'text/plain' );
318             $yyy->put_filename('README');
319              
320             # To fetch an object:
321             my $xxx2 = $container->object( name => 'XXX' );
322             my $value = $xxx2->get;
323             say 'has name ' . $xxx2->name;
324             say 'has md5 ' . $xxx2->etag;
325             say 'has size ' . $xxx2->size;
326             say 'has content type ' . $xxx2->content_type;
327             say 'has last_modified ' . $xxx2->last_modified;
328              
329             # To fetch metadata of an object:
330             say 'metadata description ' . $xxx2->object_metadata->{'description'};
331             say 'metadata useful_number ' . $xxx2->object_metadata->{'useful_number'};
332            
333             # To download an object to a local file
334             $yyy->get_filename('README.downloaded');
335              
336             $object->delete;
337              
338             $container->delete;
339              
340             =head1 DESCRIPTION
341              
342             This module was forked from L<Net::Mosso::CloudFiles> which was written by Leon
343             Brocard <acme@astray.com>. However, due to Mosso changing its name to Rackspace
344             it felt right to fork the module to a new namespace. Upgrading from
345             L<Net::Mosso::CloudFiles> 0.44 should only require you to rename all Net::Mosso
346             entries to WebService::Rackspace.
347              
348             This module provides a simple interface to the Rackspace Cloud Files
349             service. "Cloud Files is reliable, scalable and affordable web-based
350             storage for backing up and archiving all your static content".
351             Find out more at L<http://www.rackspacecloud.com/cloud_hosting_products/files>.
352              
353             To use this module you will need to sign up to Rackspace Cloud Files
354             and provide a "user" and "key". If you use this module, you will
355             incurr costs as specified by Rackspace. Please check the costs. If
356             you use this module with your user and key you will be responsible
357             for these costs.
358              
359             I highly recommend reading all about Cloud Files, but in a nutshell
360             data is stored in objects. Objects are referenced by names and objects
361             are stored in containers.
362              
363             =head1 METHODS
364              
365             =head2 new
366              
367             The constructor logs you into Cloud Files:
368              
369             my $cloudfiles = WebService::Rackspace::CloudFiles->new(
370             user => 'myusername',
371             key => 'mysecretkey',
372             );
373              
374             A location for the Cloud Files can now be specified. Valid locations are currently I<usa> and I<uk>, the default location is I<usa>
375              
376             my $cloudfiles = WebService::Rackspace::CloudFiles->new(
377             user => 'myusername',
378             key => 'mysecretkey',
379             location => 'uk',
380             );
381              
382             If you wish to use a custom location url instead, I<location_url> can be used
383             to override the usual sites:
384              
385             my $cloudfiles = WebService::Rackspace::CloudFiles->new(
386             user => 'myusername',
387             key => 'mysecretkey',
388             location_url => 'https://my.cloudfile.me/v1.0',
389             );
390              
391             =head2 containers
392              
393             List all the containers and return them as L<WebService::Rackspace::CloudFiles::Container> objects:
394              
395             my @containers = $cloudfiles->containers;
396              
397             =head2 create_container
398              
399             Create a new container and return it as a L<WebService::Rackspace::CloudFiles::Container> object:
400              
401             my $container = $cloudfiles->create_container(name => 'testing');
402              
403             =head2 container
404              
405             Use an existing container and return it as a L<WebService::Rackspace::CloudFiles::Container> object:
406              
407             my $existing_container = $cloudfiles->container(name => 'testing');
408              
409             =head2 total_bytes_used
410              
411             Returns the total amount of bytes used in your Cloud Files account:
412              
413             my $total_bytes_used = $cloudfiles->total_bytes_used;
414              
415             =head2 connection_cache_class
416              
417             =head2 iterator_callback_class
418              
419             =head2 key
420              
421             =head2 location
422              
423             =head2 locations
424              
425             =head2 retries
426              
427             =head2 timeout
428              
429             =head2 user
430              
431             =head1 TESTING
432              
433             Testing CloudFiles is a tricky thing. Rackspace charges you a bit of
434             money each time you use their service. And yes, testing counts as using.
435             Because of this, this module's test suite skips testing unless
436             you set the following three environment variables, along the lines of:
437              
438             CLOUDFILES_EXPENSIVE_TESTS=1 CLOUDFILES_USER=username CLOUDFILES_KEY=15bf43... perl t/simple.t
439              
440             =head1 SEE ALSO
441              
442             L<WebService::Rackspace::CloudFiles::Container>, L<WebService::Rackspace::CloudFiles::Object>.
443              
444             =head1 AUTHORS
445              
446             Christiaan Kras <ckras@cpan.org>.
447             L<Net::Mosso::CloudFiles> by Leon Brocard <acme@astray.com>.
448              
449             =head1 COPYRIGHT
450              
451             Copyright (C) 2010-2011, Christiaan Kras
452             Copyright (C) 2008-9, Leon Brocard
453              
454             =head1 LICENSE
455              
456             This module is free software; you can redistribute it or modify it
457             under the same terms as Perl itself.