File Coverage

blib/lib/Flickr/Upload.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package Flickr::Upload;
2              
3 6     6   84938 use strict;
  6         9  
  6         135  
4 6     6   19 use warnings;
  6         6  
  6         122  
5              
6 6     6   3415 use LWP::UserAgent;
  6         189830  
  6         154  
7 6     6   2546 use HTTP::Request::Common;
  6         9277  
  6         319  
8 6     6   2631 use Net::OAuth;
  6         2683  
  6         136  
9 6     6   24 use URI::Escape;
  6         7  
  6         277  
10 6     6   2599 use Flickr::API;
  0            
  0            
11             use XML::Simple qw(:strict);
12             use Digest::MD5 qw(md5_hex);
13             use Encode qw(encode_utf8);
14             use Carp;
15              
16             our $VERSION = '1.6';
17              
18             our @ISA = qw(Flickr::API);
19              
20             =head1 NAME
21              
22             Flickr::Upload - Upload images to C
23              
24             =head1 SYNOPSIS
25              
26             use Flickr::Upload;
27              
28             my $ua = Flickr::Upload->new(
29             {
30             'key' => '90909354',
31             'secret' => '37465825'
32             });
33             $ua->upload(
34             'photo' => '/tmp/image.jpg',
35             'auth_token' => $auth_token,
36             'tags' => 'me myself eye',
37             'is_public' => 1,
38             'is_friend' => 1,
39             'is_family' => 1
40             ) or die "Failed to upload /tmp/image.jpg";
41              
42             =head1 DESCRIPTION
43              
44             Upload an image to L.
45              
46             =head1 METHODS
47              
48             =head2 new
49              
50             =over
51              
52             =item Using Flickr Authentication
53              
54             my $ua = Flickr::Upload->new(
55             {
56             'key' => '90909354',
57             'secret' => '37465825'
58             });
59              
60             =item Using OAuth Authentication
61              
62             my $ua = Flickr::Upload->new(
63             {
64             'consumer_key' => 'your_api_key',
65             'consumer_secret' => 'your_app_secret',
66             });
67              
68             =item Retrieve saved configuration (possibly including OAuth access token)
69              
70             my $config_file = "$ENV{HOME}/saved-flickr.st";
71             my $ua = Flickr::Upload->import_storable_config($config_file);
72              
73             =back
74              
75             Instantiates a L instance, using either the Flickr
76             Authentication or the OAuth Authentication. The C or
77             C argument is your API key and the C or
78             C argument is the API secret associated with it. To
79             get an API key and secret, go to
80             L.
81              
82             The resulting L instance is a subclass of L
83             and can be used for any other Flickr API calls. As such,
84             L is also a subclass of L.
85              
86             =head2 upload
87              
88             my $photoid = $ua->upload(
89             'photo' => '/tmp/image.jpg',
90             'auth_token' => $auth_token,
91             'tags' => 'me myself eye',
92             'is_public' => 1,
93             'is_friend' => 1,
94             'is_family' => 1
95             'async' => 0,
96             );
97              
98             Taking a L instance C<$ua> as an argument, this is
99             basically a direct interface to the Flickr Photo Upload API. Required
100             parameters are C and, when using Flickr Authentication,
101             C. Note that the C must have been issued
102             against the API key and secret used to instantiate the uploader.
103              
104             When using OAuth, C is not required, and the
105             L instance must instead contain a valid L
106             access token which can be added by calling the L
107             C method.
108              
109             Returns the resulting identifier of the uploaded photo on success,
110             C on failure. According to the API documentation, after an upload the
111             user should be directed to the page
112             L.
113              
114             If the C option is non-zero, the photo will be uploaded
115             asynchronously and a successful upload returns a ticket identifier. See
116             L. The caller can then
117             periodically poll for a photo id using the C method. Note
118             that photo and ticket identifiers aren't necessarily numeric.
119              
120             =cut
121              
122             sub upload {
123             my $self = shift;
124             die '$self is not a Flickr::Upload' unless $self->isa('Flickr::Upload');
125             my %args = @_;
126              
127             # these are the only things _required_ by the uploader.
128             die "Can't read photo '$args{'photo'}'" unless $args{'photo'} and (ref $args{'photo'} eq "ARRAY" or -f $args{'photo'});
129             die "Missing 'auth_token'" unless $self->is_oauth or defined $args{'auth_token'};
130              
131             # create a request object and execute it
132             my $req = $self->make_upload_request( %args );
133             return undef unless defined $req;
134              
135             return $self->upload_request( $req );
136             }
137              
138             =head2 check_upload
139              
140             my %status2txt = (0 => 'not complete', 1 => 'completed', 2 => 'failed');
141             my @rc = $ua->check_upload( @ticketids );
142             for( @rc ) {
143             print "Ticket $_->{id} has $status2txt{$_->{complete}}\n";
144             print "\tPhoto id is $_->{photoid}\n" if exists $_->{photoid};
145             }
146              
147             This function will check the status of one or more asynchronous uploads. A
148             list of ticket identifiers are provided (C<@ticketids>) and each is
149             checked. This is basically just a wrapper around the Flickr API
150             C method.
151              
152             On success, a list of hash references is returned. Each
153             hash contains a C (the ticket id), C and, if
154             completed, C members. C may also be returned.
155             Status codes (for C) are as documented at
156             L and, actually, the
157             returned fields are identical to those listed in the C tag of the
158             response. The returned list isn't guaranteed to be in any particular order.
159              
160             This function polls a web server, so avoid calling it too frequently.
161              
162             =cut
163              
164             sub check_upload {
165             my $self = shift;
166             die '$self is not a Flickr::API' unless $self->isa('Flickr::API');
167              
168             return () unless @_; # no tickets
169              
170             my $res = $self->execute_method( 'flickr.photos.upload.checkTickets',
171             { 'tickets' => ((@_ == 1) ? $_[0] : join(',', @_)) } );
172             return () unless defined $res and $res->{success};
173              
174             # FIXME: better error feedback
175              
176             my @rc;
177             return undef unless defined $res->{tree} and exists $res->{tree}->{'children'};
178             for my $n ( @{$res->{tree}->{'children'}} ) {
179             next unless defined $n and exists $n->{'name'} and $n->{'children'};
180             next unless $n->{'name'} eq "uploader";
181              
182             for my $m (@{$n->{'children'}} ) {
183             next unless exists $m->{'name'}
184             and $m->{'name'} eq 'ticket'
185             and exists $m->{'attributes'};
186              
187             # okay, this is maybe a little lazy...
188             push @rc, $m->{'attributes'};
189             }
190             }
191              
192             return @rc;
193             }
194              
195             =head2 make_upload_request
196              
197             my $req = $uploader->make_upload_request(
198             'auth_token' => '82374523',
199             'tags' => 'me myself eye',
200             'is_public' => 1,
201             'is_friend' => 1,
202             'is_family' => 1
203             );
204             $req->header( 'X-Greetz' => 'hi cal' );
205             my $resp = $ua->request( $req );
206              
207             Creates an L object loaded with all the flick upload
208             parameters. This will also sign the request, which means you won't be able to
209             mess any further with the upload request parameters.
210              
211             Takes all the same parameters as L, except that the photo argument
212             isn't required. This in intended so that the caller can include it by
213             messing directly with the HTTP content (via C<$DYNAMIC_FILE_UPLOAD> or
214             the L class, among other things). See C directory from
215             the source distribution for examples.
216              
217             Returns a standard L POST object. The caller can manually
218             do the upload or just call the L function.
219              
220             =cut
221              
222             sub make_upload_request {
223             my $self = shift;
224             die '$self is not a Flickr::Upload' unless $self->isa('Flickr::Upload');
225             my %args = @_;
226              
227             # _required_ by the uploader.
228             unless ($self->is_oauth) {
229             die "Missing 'auth_token' argument" unless $args{'auth_token'};
230             } else {
231             croak "OAuth access token needed" unless defined $self->{oauth}->{token};
232             }
233              
234             my $uri = $args{'uri'} || 'https://api.flickr.com/services/upload/';
235              
236             # passed in separately, so remove from the hash
237             delete $args{uri};
238              
239             # Flickr::API includes this with normal requests, but we're building a custom
240             # message.
241             $args{'api_key'} = $self->{'api_key'} unless $self->is_oauth;
242              
243             # photo is _not_ included in the sig
244             my $photo = $args{photo};
245             delete $args{photo};
246              
247             unless( $self->is_oauth ) {
248             $args{'api_sig'} = $self->_sign_args(\%args);
249             } else {
250             my %oauth = (
251             'nonce' => $self->_make_nonce(),
252             'consumer_key' => $self->{oauth}->{consumer_key},
253             'consumer_secret' => $self->{oauth}->{consumer_secret},
254             'timestamp' => time,
255             'signature_method' => $self->{oauth}->{signature_method},
256             'version' => $self->{oauth}->{version},
257             'token' => $self->{oauth}->{token},
258             'token_secret' => $self->{oauth}->{token_secret},
259             );
260             $oauth{extra_params} = \%args;
261             $oauth{request_method} = 'POST';
262             $oauth{request_url} = $uri;
263             $Net::OAuth::PROTOCOL_VERSION = Net::OAuth::PROTOCOL_VERSION_1_0A;
264             my $req = Net::OAuth->request( "protected resource" )->new( %oauth );
265             $req->sign();
266             my $tmp_body = $req->to_post_body();
267             %args = ();
268             foreach (split '&', $tmp_body) {
269             my ($name, $val) = split '=', $_, 2;
270             $args{$name} = URI::Escape::uri_unescape( $val );
271             }
272             }
273              
274             # unlikely that the caller would set up the photo as an array,
275             # but...
276             if( defined $photo ) {
277             $photo = [ $photo ] if ref $photo ne "ARRAY";
278             $args{photo} = $photo;
279             }
280              
281             my $req = POST $uri, 'Content_Type' => 'form-data', 'Content' => \%args;
282              
283             return $req;
284             }
285              
286             =head2 upload_request
287              
288             my $photoid = upload_request( $ua, $request );
289              
290             Taking (at least) L and L objects as
291             arguments, this executes the request and processes the result as a
292             flickr upload. It's assumed that the request looks a lot like something
293             created with L. Note that the request must be signed
294             according to the Flickr API authentication rules.
295              
296             Returns the resulting identifier of the uploaded photo (or ticket for
297             asynchronous uploads) on success, C on failure. According to the
298             API documentation, after an upload the user should be directed to the
299             page L.
300              
301             =cut
302              
303             sub upload_request {
304             my $self = shift;
305             die "$self is not a LWP::UserAgent" unless $self->isa('LWP::UserAgent');
306             my $req = shift;
307             die "expecting a HTTP::Request" unless $req->isa('HTTP::Request');
308              
309             # Try 3 times to upload data. Without this flickr_upload is bound
310             # to die on large uploads due to some miscellaneous network
311             # issues. Timeouts on flickr or something else.
312             my ($res, $xml);
313             my $tries = 3;
314             for my $try (1 .. $tries) {
315             # Try to upload
316             $res = $self->request( $req );
317             return () unless defined $res;
318              
319             if ($res->is_success) {
320             $xml = XMLin($res->decoded_content, KeyAttr=>[], ForceArray=>0);
321             return () unless defined $xml;
322             last;
323             } else {
324             my $what_next = ($try == $tries ? "giving up" : "trying again");
325             my $status = $res->status_line;
326              
327             print STDERR "Failed uploading attempt attempt $try/$tries, $what_next. Message from server was: '$status'\n";
328             next;
329             }
330             }
331              
332             my $photoid = $xml->{photoid};
333             my $ticketid = $xml->{ticketid};
334             unless( defined $photoid or defined $ticketid ) {
335             print STDERR "upload failed:\n", $res->decoded_content(), "\n";
336             return undef;
337             }
338              
339             return (defined $photoid) ? $photoid : $ticketid;
340             }
341              
342             =head2 file_length_in_encoded_chunk
343              
344             $HTTP::Request::Common::DYNAMIC_FILE_UPLOAD = 1;
345             my $photo = 'image.jpeg';
346             my $photo_size = (stat($photo))[7];
347             my $req = $ua->make_upload_request( ... );
348             my $gen = $req->content();
349             die unless ref($gen) eq "CODE";
350              
351             my $state;
352             my $size;
353              
354             $req->content(
355             sub {
356             my $chunk = &$gen();
357              
358             $size += Flickr::Upload::file_length_in_encoded_chunk(\$chunk, \$state, $photo_size);
359              
360             warn "$size bytes have now been uploaded";
361              
362             return $chunk;
363             }
364             );
365              
366             $rc = $ua->upload_request( $req );
367              
368             This subroutine is tells you how much of a chunk in a series of
369             variable size multipart HTTP chunks contains a single file being
370             uploaded given a reference to the current chunk, a reference to a
371             state variable that lives between calls, and the size of the file
372             being uploaded.
373              
374             It can be used used along with L's
375             $HTTP::Request::Common::DYNAMIC_FILE_UPLOAD facility to implement
376             upload progress bars or other upload monitors, see L
377             for a practical example and F for tests.
378              
379             =cut
380              
381             sub file_length_in_encoded_chunk
382             {
383             my ($chunk, $s, $img_size) = @_;
384              
385             $$s = {} unless ref $$s eq 'HASH';
386              
387             # If we've run past the end of the image there's nothing to do but
388             # report no image content in this sector.
389             return 0 if $$s->{done};
390              
391             unless ($$s->{in}) {
392             # Since we haven't found the image yet append this chunk to
393             # our internal data store, we do this because we have to do a
394             # regex match on m[Content-Type...] which might be split
395             # across multiple chunks
396             $$s->{data} .= defined $$chunk ? $$chunk : '';
397              
398             if ($$s->{data} =~ m[Content-Type: .*?\r\n\r\n]g) {
399             # We've found the image inside the stream, record this,
400             # delete ->{data} since we don't need it, and see how much
401             # of the image this particular chunk gives us.
402             $$s->{in} = 1;
403             my $size = length substr($$s->{data}, pos($$s->{data}), -1);
404             delete $$s->{data};
405              
406             $$s->{size} = $size;
407              
408             if ($$s->{size} >= $img_size) {
409             # The image could be so small that we've already run
410             # through it in chunk it starts in, mark as done and
411             # return the total image size
412              
413             $$s->{done} = 1;
414             return $img_size;
415             } else {
416             return $$s->{size};
417             }
418             } else {
419             # Are we inside the image yet? No!
420             return 0;
421             }
422             } else {
423             my $size = length $$chunk;
424              
425             if (($$s->{size} + $size) >= $img_size) {
426             # This chunk finishes the image
427              
428             $$s->{done} = 1;
429              
430             # Return what we had left
431             return $img_size - $$s->{size};
432             } else {
433             # This chunk isn't the last one
434              
435             $$s->{size} += $size;
436              
437             return $size;
438             }
439             }
440             }
441              
442             =head2 photosets_create
443              
444             Calls Flickr's "flickr.photosets.create" method,
445             to create a new Set.
446              
447             The set will use the PrimaryPhotoID as the thumbnail photo.
448              
449             returns: UNDEF on failure, PhotosetID on success.
450              
451             my $photoset_id = $ua->photosets_create( title => 'title',
452             description => 'description',
453             primary_photo_id => ID,
454             auth_token => AUTH_TOKEN );
455              
456             $ua->photosets_addphoto ( photoset_id => $photoset_id,
457             photo_id => ID );
458              
459             =cut
460             sub photosets_create {
461             my $self = shift;
462             die '$self is not a Flickr::API' unless $self->isa('Flickr::API');
463              
464             my %args = @_;
465             carp "Missing 'auth_token' parameter for photosets_create()"
466             unless exists $args{'auth_token'};
467             my $auth_token = $args{'auth_token'};
468             carp "Missing 'title' parameter for photosets_create()"
469             unless exists $args{'title'} && length($args{'title'})>0;
470             my $title = $args{'title'};
471             carp "Missing 'primary_photo_id' parameter for photosets_create()"
472             unless exists $args{'primary_photo_id'};
473             my $primary_photo_id = $args{'primary_photo_id'};
474             carp "Invalid primary_photo_id ($primary_photo_id) value (expecting numeric ID)" unless $primary_photo_id =~ /^[0-9]+$/;
475             my $description = ( exists $args{'description'} ) ? $args{'description'} : "" ;
476              
477             my $res = $self->execute_method( 'flickr.photosets.create',
478             { 'title' => $title,
479             'description' => $description,
480             'primary_photo_id' => $primary_photo_id,
481             'auth_token' => $auth_token,
482             } ) ;
483             #TODO: Add detailed error messages
484             return undef unless defined $res and $res->{success};
485              
486             my $hash = XMLin($res->decoded_content(), KeyAttr=>[], ForceArray=>0);
487             my $photoset_id = $hash->{photoset}->{id};
488             if ( ! defined $photoset_id ) {
489             warn "Failed to extract photoset ID from response:\n" .
490             $res->decoded_content() . "\n\n";
491             return undef;
492             }
493             return $photoset_id ;
494             }
495              
496             =head2 photosets_addphoto
497              
498             Calls Flickr's "flickr.photosets.addPhoto" method,
499             to add a (existing) photo to an existing set.
500              
501             returns: UNDEF on failure, TRUE on success.
502              
503             my $photoset_id = $ua->photosets_create( title => 'title',
504             description => 'description',
505             primary_photo_id => ID,
506             auth_token => AUTH_TOKEN );
507              
508             $ua->photosets_addphoto ( photoset_id => $photoset_id,
509             photo_id => ID );
510              
511             =cut
512             sub photosets_addphoto {
513             my $self = shift;
514             die '$self is not a Flickr::API' unless $self->isa('Flickr::API');
515              
516             my %args = @_;
517             carp "Missing 'auth_token' parameter for photosets_addphoto()"
518             unless exists $args{'auth_token'};
519             my $auth_token = $args{'auth_token'};
520             carp "Missing 'photoset_id' parameter for photosets_addphoto()"
521             unless exists $args{'photoset_id'};
522             my $photoset_id = $args{'photoset_id'};
523             carp "Missing 'photo_id' parameter for photosets_addphoto()"
524             unless exists $args{'photo_id'};
525             my $photo_id = $args{'photo_id'};
526              
527             my $res = $self->execute_method( 'flickr.photosets.addPhoto',
528             { 'photoset_id' => $photoset_id,
529             'photo_id' => $photo_id,
530             'auth_token' => $auth_token,
531             } ) ;
532             #TODO: Add detailed error messages
533             return undef unless defined $res;
534              
535             return $res->{success};
536             }
537              
538             # Private method adapted from Flickr::API
539             # See: https://www.flickr.com/services/api/auth.howto.web.html
540             sub _sign_args {
541             my $self = shift;
542             my $args = shift;
543              
544             my $sig = $self->{api_secret};
545              
546             for(sort { $a cmp $b } keys %$args) {
547             $sig .= $_ . (defined($args->{$_}) ? $args->{$_} : "");
548             }
549              
550             return md5_hex($self->{unicode} ? encode_utf8($sig) : $sig);
551             }
552              
553             1;
554             __END__