File Coverage

lib/Cloudinary.pm
Criterion Covered Total %
statement 60 70 85.7
branch 23 36 63.8
condition 16 26 61.5
subroutine 11 11 100.0
pod 3 3 100.0
total 113 146 77.4


line stmt bran cond sub pod time code
1             package Cloudinary;
2              
3             =head1 NAME
4              
5             Cloudinary - Talk with cloudinary.com
6              
7             =head1 VERSION
8              
9             0.14
10              
11             =head1 DESCRIPTION
12              
13             This module lets you interface to L.
14              
15             =head1 SYNOPSIS
16              
17             =head2 Standalone
18              
19             my $delay = Mojo::IOLoop->delay;
20             my $cloudinary = Cloudinary->new(
21             cloud_name => '...',
22             api_key => '...',
23             api_secret => '...',
24             );
25              
26             $delay->begin;
27             $cloudinary->upload(
28             {
29             file => {
30             file => $path_to_file,
31             },
32             },
33             sub {
34             my($cloudinary, $res) = @_;
35             # ...
36             $delay->end;
37             },
38             });
39              
40             # let's you do multiple upload() in parallel
41             # just call $delay->begin once pr upload()
42             # and $delay->end in each callback given to upload()
43             $delay->wait;
44              
45             =head2 With mojolicious
46              
47             See L.
48              
49             =head2 Options
50              
51             As from 0.04 all methods support the short and long option, meaning
52             the examples below work the same:
53              
54             $self->url_for('billclinton.jpg' => { w => 50 });
55             $self->url_for('billclinton.jpg' => { width => 50 });
56              
57             =head2 url_for() examples
58              
59             $cloudinary->url_for('billclinton.jpg', { type => 'facebook' });
60             $cloudinary->url_for('billclinton.jpg', { type => 'twitter_name', h => 70, w => 100 });
61             $cloudinary->url_for('18913373.jpg', { type => 'twitter_name' });
62             $cloudinary->url_for('my-uploaded-image.jpg', { h => 50, w => 50 });
63             $cloudinary->url_for('myrawid', { resource_type => 'raw' });
64              
65             =head2 Aliases
66              
67             This module provides alias for the Cloudinary transformations:
68              
69             a = angle
70             b = background
71             c = crop
72             d = default_image
73             e = effect
74             f = fetch_format
75             g = gravity
76             h = height
77             l = overlay
78             p = prefix
79             q = quality
80             r = radius
81             t = named_transformation
82             w = width
83             x = x
84             y = y
85              
86             =cut
87              
88 4     4   871882 use Mojo::Base -base;
  4         11  
  4         35  
89 4     4   1385 use File::Basename;
  4         9  
  4         321  
90 4     4   1032 use Mojo::UserAgent;
  4         173209  
  4         35  
91 4     4   165 use Mojo::Util qw( sha1_sum url_escape );
  4         9  
  4         247  
92 4     4   44 use Scalar::Util 'weaken';
  4         7  
  4         12603  
93              
94             our $VERSION = '0.14';
95             our(%SHORTER, %LONGER);
96             my @SIGNATURE_KEYS = qw( callback eager format public_id tags timestamp transformation type );
97              
98             {
99             %LONGER = (
100             a => 'angle',
101             b => 'background',
102             c => 'crop',
103             d => 'default_image',
104             e => 'effect',
105             f => 'fetch_format',
106             g => 'gravity',
107             h => 'height',
108             l => 'overlay',
109             p => 'prefix',
110             q => 'quality',
111             r => 'radius',
112             t => 'named_transformation',
113             w => 'width',
114             x => 'x',
115             y => 'y',
116             );
117             %SHORTER = reverse %LONGER;
118             }
119              
120             =head1 ATTRIBUTES
121              
122             =head2 cloud_name
123              
124             Your cloud name from L
125              
126             =head2 api_key
127              
128             Your API key from L
129              
130             =head2 api_secret
131              
132             Your API secret from L
133              
134             =head2 private_cdn
135              
136             Your private CDN url from L.
137              
138             =cut
139              
140             has cloud_name => sub { die 'cloud_name is required in constructor' };
141             has api_key => sub { die 'api_key is required in constructor' };
142             has api_secret => sub { die 'api_secret is required in constructor' };
143             has private_cdn => sub { die 'private_cdn is required in constructor' };
144             has _api_url => 'http://api.cloudinary.com/v1_1';
145             has _public_cdn => 'http://res.cloudinary.com';
146             has _ua => sub {
147             my $ua = Mojo::UserAgent->new;
148              
149             $ua->on(
150             start => sub {
151             my($ua, $tx) = @_;
152              
153             for my $part (@{ $tx->req->content->parts }) {
154             my $content_type = $part->headers->content_type || '';
155             $part->headers->remove('Content-Type') if $content_type eq 'text/plain';
156             }
157             }
158             );
159              
160             return $ua;
161             };
162              
163             =head1 METHODS
164              
165             =head2 upload
166              
167             $self->upload(
168             {
169             file => $binary_str|$url, # required
170             timestamp => $epoch, # time()
171             public_id => $str, # optional
172             format => $str, # optional
173             resource_type => $str, # image or raw. defaults to "image"
174             tags => ['foo', 'bar'], # optional
175             },
176             sub {
177             my($cloudinary, $res) = @_;
178             # ...
179             },
180             );
181              
182             Will upload a file to L using the parameters given
183             L, L and L. C<$res> in the callback
184             will be the json response from cloudinary:
185              
186             {
187             url => $str,
188             secure_url => $str,
189             public_id => $str,
190             version => $str,
191             width => $int, # only for images
192             height => $int, # only for images
193             }
194              
195             C<$res> on error can be either C if there was an issue
196             connecting/communicating with cloudinary or a an error data structure:
197              
198             {
199             error => { message: $str },
200             }
201              
202             The C can be:
203              
204             =over 4
205              
206             =item * A hash
207              
208             { file => 'path/to/image' }
209              
210             =item * A L object.
211              
212             =item * A L object.
213              
214             =item * A URL
215              
216             =back
217              
218             C in callbacks will be the JSON response from L
219             as a hash ref. It may also be C if something went wrong with the
220             actual HTTP POST.
221              
222             See also L and
223             L.
224              
225             =cut
226              
227             sub upload {
228 5     5 1 10883 my($self, $args, $cb) = @_;
229              
230             # TODO: transformation, eager
231 5 100       32 $args = { file => $args } if ref $args ne 'HASH';
232 5   50     42 $args->{resource_type} ||= 'image';
233 5   66     32 $args->{timestamp} ||= time;
234              
235 5 100       42 die "Usage: \$self->upload({ file => ... })" unless defined $args->{file};
236              
237 4 50       85 if(ref $args->{tags} eq 'ARRAY') {
238 0         0 $args->{tags} = join ',', @{ $args->{tags} };
  0         0  
239             }
240 4 100       43 if(UNIVERSAL::isa($args->{file}, 'Mojo::Asset')) {
    50          
241 2   33     60 $args->{file} = { file => $args->{file}, filename => $args->{filename} || basename($args->{file}->path), };
242             }
243             elsif (UNIVERSAL::isa($args->{file}, 'Mojo::Upload')) {
244 0         0 $args->{file} = { file => $args->{file}->asset, filename => $args->{file}->filename, };
245             }
246              
247             $self->_call_api(
248 7         52 upload => $args,
249             {
250             timestamp => time,
251 4         141 (map { ($_, $args->{$_}) } grep { defined $args->{$_} } @SIGNATURE_KEYS), file => $args->{file},
  32         69  
252             },
253             $cb,
254             );
255             }
256              
257             =head2 destroy
258              
259             $self->destroy(
260             {
261             public_id => $public_id,
262             resource_type => $str, # image or raw. defaults to "image"
263             },
264             sub {
265             my($cloudinary, $res) = @_;
266             # ...
267             }
268             });
269              
270             Will delete an image from cloudinary, identified by C<$public_id>.
271             The callback will be called when the image got deleted or if an error occur.
272              
273             On error, look for:
274              
275             {
276             error => { message: $str },
277             }
278              
279             See also L.
280              
281             =cut
282              
283             sub destroy {
284 3     3 1 10129 my($self, $args, $cb) = @_;
285              
286 3 100       27 $args = { public_id => $args } unless ref $args eq 'HASH';
287              
288 3 100       44 die "Usage: \$self->destroy({ public_id => ... })" unless defined $args->{public_id};
289              
290 2   50     18 $args->{resource_type} ||= 'image';
291              
292 2   66     36 $self->_call_api(
      50        
293             destroy => $args,
294             {
295             public_id => $args->{public_id},
296             timestamp => $args->{timestamp} || time,
297             type => $args->{type} || 'upload',
298             },
299             $cb,
300             );
301             }
302              
303             sub _call_api {
304 6     6   18 my($self, $action, $args, $post, $cb) = @_;
305 6         169 my $url = join '/', $self->_api_url, $self->cloud_name, $args->{resource_type}, $action;
306 6         328 my $headers = { 'Content-Type' => 'multipart/form-data' };
307              
308 6         159 $post->{api_key} = $self->api_key;
309 6         63 $post->{signature} = $self->_api_sign_request($post);
310              
311 6         114 Scalar::Util::weaken($self);
312             $self->_ua->post(
313             $url, $headers,
314             form => $post,
315             sub {
316 6     6   220296 my($ua, $tx) = @_;
317              
318 6 100       42 if($cb) {
    50          
319 2   50     46 $self->$cb($tx->res->json || { error => $tx->error || 'Unknown error' });
320             }
321             elsif($tx->success) {
322 0 0       0 if($args->{on_success}) {
    0          
323 0         0 warn "[Cloudinary] 'on_success' will be deprecated!";
324 0         0 $args->{on_success}->($tx->res->json);
325             }
326             elsif($args->{delay}) {
327 0         0 warn "[Cloudinary] 'delay' will be deprecated!";
328 0         0 $args->{delay}->($self, $tx->res->json, $tx);
329             }
330             }
331             else {
332 4 50       503 if($args->{on_error}) {
    0          
333 4         1630 warn "[Cloudinary] 'on_error' will be deprecated!";
334 4         128 $args->{on_error}->($tx->res->json);
335             }
336             elsif ($args->{delay}) {
337 0         0 warn "[Cloudinary] 'delay' will be deprecated!";
338 0         0 $args->{delay}->($self, undef, $tx);
339             }
340             }
341             }
342 6         158 );
343             }
344              
345             sub _api_sign_request {
346 7     7   1043 my($self, $args) = @_;
347 7         13 my @query;
348              
349 7         151 for my $k (@SIGNATURE_KEYS) {
350 56 100       298 push @query, "$k=" . url_escape $args->{$k} if defined $args->{$k};
351             }
352              
353 7         234 $query[-1] .= $self->api_secret;
354              
355 7         100 sha1_sum join '&', @query;
356             }
357              
358             =head2 url_for
359              
360             $url_obj = $self->url_for("$public_id.$format", \%args);
361              
362             This method will return a public URL to the image at L.
363             It will use L or the public CDN and L to construct
364             the URL. The return value is a L object.
365              
366             Example C<%args>:
367              
368             {
369             w => 100, # width of image
370             h => 150, # height of image
371             resource_type => $str, # image or raw. defaults to "image"
372             type => $str, # upload, facebook. defaults to "upload"
373             secure => $bool, # use private_cdn or public cdn
374             }
375              
376             See also L
377             and L.
378              
379             =cut
380              
381             sub url_for {
382 6     6 1 16942 my $self = shift;
383 6 50       34 my $public_id = shift or die 'Usage: $self->url_for($public_id, ...)';
384 6   100     31 my $args = shift || {};
385 6 100       51 my $format = $public_id =~ s/\.(\w+)// ? $1 : 'jpg';
386 6 50       197 my $url = Mojo::URL->new(delete $args->{secure} ? $self->private_cdn : $self->_public_cdn);
387              
388 30         146 $url->path(
389             join '/',
390 6   66     46 grep {length} $self->cloud_name,
391             $args->{resource_type} || 'image',
392             $args->{type} || 'upload',
393             join(',',
394 7 50       201 map { ($SHORTER{$_} || $_) . '_' . $args->{$_} }
395 6   50     3672 grep { $_ ne 'resource_type' and $_ ne 'type' } sort keys %$args),
      100        
396             "$public_id.$format",
397             );
398              
399 6         3233 return $url;
400             }
401              
402             =head1 COPYRIGHT & LICENSE
403              
404             This library is free software. You can redistribute it and/or
405             modify it under the same terms as Perl itself.
406              
407             =head1 AUTHOR
408              
409             Jan Henning Thorsen - jhthorsen@cpan.org
410              
411             =cut
412              
413             1;