File Coverage

blib/lib/Cloudinary.pm
Criterion Covered Total %
statement 63 66 95.4
branch 28 34 82.3
condition 16 28 57.1
subroutine 11 11 100.0
pod 3 3 100.0
total 121 142 85.2


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