File Coverage

blib/lib/Net/Dropbox/API.pm
Criterion Covered Total %
statement 30 151 19.8
branch 0 44 0.0
condition 0 21 0.0
subroutine 10 26 38.4
pod 13 13 100.0
total 53 255 20.7


line stmt bran cond sub pod time code
1             package Net::Dropbox::API;
2              
3 1     1   25067 use common::sense;
  1         11  
  1         6  
4 1     1   62 use File::Basename qw(basename);
  1         3  
  1         151  
5 1     1   1082 use JSON;
  1         22216  
  1         4  
6 1     1   1180 use Mouse;
  1         33724  
  1         6  
7 1     1   1272 use Net::OAuth;
  1         726  
  1         29  
8 1     1   3475 use LWP::UserAgent;
  1         69914  
  1         38  
9 1     1   13 use URI;
  1         3  
  1         63  
10 1     1   2497 use HTTP::Request::Common;
  1         3797  
  1         93  
11 1     1   1419 use Data::Random qw(rand_chars);
  1         5280  
  1         106  
12 1     1   6183 use Encode;
  1         21172  
  1         3370  
13              
14             =head1 NAME
15              
16             Net::Dropbox::API - A dropbox API interface
17              
18             =head1 VERSION
19              
20             Version 1.9.8
21              
22             =cut
23              
24             our $VERSION = '1.9';
25              
26              
27             =head1 SYNOPSIS
28              
29             The Dropbox API is a OAuth based API. I try to abstract as much away as
30             possible so you should not need to know too much about it.
31             This is how it works:
32              
33             use Net::Dropbox::API;
34              
35             my $box = Net::Dropbox::API->new({key => 'KEY', secret => 'SECRET'});
36             my $login_link = $box->login; # user needs to click this link and login
37             $box->auth; # oauth keys get exchanged
38             my $info = $box->account_info; # and here we have our account info
39              
40             See the examples for a working Mojolicious web client using the Dropbox
41             API.
42              
43             You can find Dropbox's API documentation at L
44              
45             =head1 FUNCTIONS
46              
47             =cut
48              
49             has 'debug' => (is => 'rw', isa => 'Bool', default => 0);
50             has 'error' => (is => 'rw', isa => 'Str', predicate => 'has_error');
51             has 'key' => (is => 'rw', isa => 'Str');
52             has 'secret' => (is => 'rw', isa => 'Str');
53             has 'login_link' => (is => 'rw', isa => 'Str');
54             has 'callback_url' => (is => 'rw', isa => 'Str', default => 'http://localhost:3000/callback');
55             has 'request_token' => (is => 'rw', isa => 'Str');
56             has 'request_secret' => (is => 'rw', isa => 'Str');
57             has 'access_token' => (is => 'rw', isa => 'Str');
58             has 'access_secret' => (is => 'rw', isa => 'Str');
59             has 'context' => (is => 'rw', isa => 'Str', default => 'sandbox');
60              
61              
62             =head2 login
63              
64             This sets up the initial OAuth handshake and returns the login URL. This
65             URL has to be clicked by the user and the user then has to accept
66             the application in dropbox.
67              
68             Dropbox then redirects back to the callback URL defined with
69             C<$self-Ecallback_url>. If the user already accepted the application the
70             redirect may happen without the user actually clicking anywhere.
71              
72             =cut
73              
74             sub login {
75 0     0 1   my $self = shift;
76              
77 0           my $ua = LWP::UserAgent->new;
78              
79 0 0         my $request = Net::OAuth->request("request token")->new(
80             consumer_key => $self->key,
81             consumer_secret => $self->secret,
82             request_url => 'https://api.dropbox.com/1/oauth/request_token',
83             request_method => 'POST',
84             signature_method => 'HMAC-SHA1',
85             timestamp => time,
86             nonce => $self->nonce,
87             callback => $self->callback_url,
88             callback_confirmed => ($self->callback_url ? 'true' : undef)
89             );
90              
91 0           $request->sign;
92 0           my $res = $ua->request(POST $request->to_url);
93              
94 0 0         if ($res->is_success) {
95 0           my $response = Net::OAuth->response('request token')->from_post_body($res->content);
96 0           $self->request_token($response->token);
97 0           $self->request_secret($response->token_secret);
98 0 0         print "Got Request Token ", $response->token, "\n" if $self->debug;
99 0 0         print "Got Request Token Secret ", $response->token_secret, "\n" if $self->debug;
100 0           return 'https://www.dropbox.com/1/oauth/authorize?oauth_token='.$response->token.'&oauth_callback='.$self->callback_url;
101             }
102             else {
103 0           $self->error($res->status_line);
104 0           warn "Something went wrong: " . $res->status_line;
105             }
106             }
107              
108             =head2 auth
109              
110             The auth method changes the initial request token into access token that we need
111             for subsequent access to the API. This method only has to be called once
112             after login.
113              
114             =cut
115              
116             sub auth {
117 0     0 1   my $self = shift;
118              
119 0           my $ua = LWP::UserAgent->new;
120 0           my $request = Net::OAuth->request("access token")->new(
121             consumer_key => $self->key,
122             consumer_secret => $self->secret,
123             request_url => 'https://api.dropbox.com/1/oauth/access_token',
124             request_method => 'POST',
125             signature_method => 'HMAC-SHA1',
126             timestamp => time,
127             nonce => $self->nonce,
128             callback => $self->callback_url,
129             token => $self->request_token,
130             token_secret => $self->request_secret,
131             );
132              
133 0           $request->sign;
134 0           my $res = $ua->request(POST $request->to_url);
135              
136 0 0         if ($res->is_success) {
137 0           my $response = Net::OAuth->response('access token')->from_post_body($res->content);
138 0           $self->access_token($response->token);
139 0           $self->access_secret($response->token_secret);
140 0 0         print "Got Access Token ", $response->token, "\n" if $self->debug;
141 0 0         print "Got Access Token Secret ", $response->token_secret, "\n" if $self->debug;
142             }
143             else {
144 0           $self->error($res->status_line);
145 0           warn "Something went wrong: ".$res->status_line;
146             }
147             }
148              
149             =head2 account_info
150              
151             account_info polls the users info from dropbox.
152              
153             =cut
154              
155             sub account_info {
156 0     0 1   my $self = shift;
157              
158 0           return from_json($self->_talk('account/info'));
159             }
160              
161             =head2 list
162              
163             lists all files in the path defined:
164              
165             $data = $box->list(); # top-level
166             $data = $box->list( "/Photos" ); # folder
167              
168             The data returned is a ref to a hash containing various fields returned
169             by Dropbox, including a C value, which can be used later to check
170             if Dropbox data beneath a specified folder has changed since the last call.
171              
172             For this, C accepts an optional 'hash' argument:
173              
174             $data = $box->list({ hash => "ce9ccbfb8f255f234c93adcfef33b5a6" },
175             "/Photos");
176              
177             This will either return
178              
179             { http_response_code => 304 }
180              
181             in which case nothing has changed since the last call, or
182              
183             { http_response_code => 200,
184             # ... various other fields
185             }
186              
187             if there were modifications.
188              
189             =cut
190              
191             sub list {
192 0     0 1   my $self = shift;
193 0           my $opts = {};
194 0 0 0       if(defined $_[0] and ref($_[0]) eq "HASH") {
195             # optional option hash present
196 0           $opts = shift;
197             }
198 0           my $path = shift;
199 0 0         $path = '' unless defined $path;
200 0 0         $path = '/'.$path if $path=~m|^[^/]|;
201              
202 0           my $uri = URI->new('files/'.$self->context.$path);
203 0 0         $uri->query_form($opts) if scalar keys %$opts;
204              
205 0           my $talk_opts = {};
206              
207 0 0         if(exists $opts->{hash}) {
208             $talk_opts = {
209             error_handler => sub {
210 0     0     my $obj = shift;
211 0           my $resp = shift;
212             # HTTP::Status is nice but old RHEL5 has issues with it
213             # so we use plain codes
214 0 0         if( $resp->code == 304 ) {
215 0           return to_json({ http_response_code => 304 });
216             } else {
217 0           return $self->_talk_default_error_handler($resp);
218             }
219             },
220 0           };
221             }
222              
223 0           return from_json($self->_talk($talk_opts, $uri->as_string));
224             }
225              
226             =head2 copy
227              
228             copies a folder
229             copy($from, $to)
230              
231             =cut
232              
233             sub copy {
234 0     0 1   my $self = shift;
235 0           my ($from, $to) = @_;
236              
237 0           my $opts = 'root='.$self->context;
238 0           return from_json($self->_talk('fileops/copy?'.$opts,
239             undef, undef, undef, undef, undef,
240             { from_path => $from, to_path => $to }));
241             }
242              
243             =head2 move
244              
245             move a folder
246             move($from, $to)
247              
248             =cut
249              
250             sub move {
251 0     0 1   my $self = shift;
252 0           my ($from, $to) = @_;
253              
254 0           my $opts = 'root='.$self->context;
255 0           return from_json($self->_talk('fileops/move?'.$opts,
256             undef, undef, undef, undef, undef,
257             { from_path => $from, to_path => $to }));
258             }
259              
260             =head2 mkdir
261              
262             creates a folder
263             mkdir($path)
264              
265             =cut
266              
267             sub mkdir {
268 0     0 1   my $self = shift;
269 0           my ($path) = @_;
270              
271 0           my $opts = 'root='.$self->context;
272 0           return from_json($self->_talk('fileops/create_folder?'.$opts,
273             undef, undef, undef, undef, undef,
274             { path => $path }));
275             }
276              
277             =head2 delete
278              
279             delete a folder
280             delete($path)
281              
282             =cut
283              
284             sub delete {
285 0     0 1   my $self = shift;
286 0           my ($path) = @_;
287              
288 0           my $opts = 'root='.$self->context;
289 0           return from_json($self->_talk('fileops/delete?'.$opts,
290             undef, undef, undef, undef, undef,
291             { path => $path }));
292             }
293              
294             =head2 view
295              
296             creates a cookie protected link for the user to look at.
297             view($path)
298              
299             =cut
300              
301             sub view {
302 0     0 1   my $self = shift;
303 0           my ($path) = @_;
304              
305 0           return from_json($self->_talk('fileops/links/'.$self->context.'/'.$path));
306             }
307              
308             =head2 metadata
309              
310             creates a cookie protected link for the user to look at.
311             metadata($path)
312              
313             =cut
314              
315             sub metadata {
316 0     0 1   my $self = shift;
317 0   0       my $path = shift || '';
318              
319 0           return from_json($self->_talk('metadata/'.$self->context.'/'.$path));
320             }
321              
322             =head2 putfile
323              
324             uploads a file to dropbox
325              
326             =cut
327              
328             sub putfile {
329 0     0 1   my $self = shift;
330 0           my $file = shift;
331 0   0       my $path = shift || '';
332 0   0       my $filename = shift || basename( $file );
333              
334 0           return from_json(
335             $self->_talk(
336             'files/'.$self->context.'/'.$path,
337             'POST',
338             { file => [ $file ] },
339             $filename, # can't decode_utf8
340             'api-content',
341             undef,
342             { file => decode_utf8($filename) }
343             )
344             );
345              
346             }
347              
348             =head2 getfile
349              
350             get a file from dropbox
351              
352             =cut
353              
354             =head2 debug
355              
356             Set this to a non-false value in order to print some debugging information to STDOUT.
357             debug(1)
358              
359             =cut
360              
361             sub getfile {
362 0     0 1   my $self = shift;
363 0   0       my $path = shift || '';
364 0   0       my $file = shift || '';
365              
366 0           return $self->_talk('files/'.$self->context.'/'.$path, undef, undef, undef, 'api-content', $file);
367             }
368              
369              
370             =head1 INTERNAL API
371              
372             =head2 _talk
373              
374             _talk handles the access to the restricted resources. You should
375             normally not need to access this directly.
376              
377             =cut
378              
379             =head2 nonce
380              
381             Generate a different nonce for every request.
382              
383             =cut
384              
385 0     0 1   sub nonce { join( '', rand_chars( size => 16, set => 'alphanumeric' )); }
386              
387             sub _talk {
388 0     0     my $self = shift;
389 0           my $opts = {};
390 0 0 0       if(defined $_[0] and ref($_[0]) eq "HASH") {
391             # optional option hash present
392 0           $opts = shift;
393             }
394 0           my $command = shift;
395 0   0       my $method = shift || 'GET';
396 0           my $content = shift;
397 0           my $filename= shift;
398 0   0       my $api = shift || 'api';
399 0           my $content_file = shift;
400 0           my $extra_params = shift;
401              
402 0 0         if( !defined $opts->{error_handler} ) {
403 0           $opts->{error_handler} = \&_talk_default_error_handler;
404             }
405              
406 0           my $ua = LWP::UserAgent->new;
407              
408 0           my %opts = (
409             consumer_key => $self->key,
410             consumer_secret => $self->secret,
411             request_url => 'https://'.$api.'.dropbox.com/1/'.$command,
412             request_method => $method,
413             signature_method => 'HMAC-SHA1',
414             timestamp => time,
415             nonce => $self->nonce,
416             #callback => $self->callback_url,
417             token => $self->access_token,
418             token_secret => $self->access_secret,
419             extra_params => $extra_params
420             );
421 0 0         if($filename) {
422 0           push @{$content->{file}},$filename;
  0            
423             }
424              
425 0           my $request = Net::OAuth->request("protected resource")->new( %opts );
426              
427 0           $request->sign;
428 0 0         print "_talk URL: ", $request->to_url, "\n" if $self->debug;
429              
430 0           my $res;
431 0 0         if($content_file) {
    0          
432 0           $res = $ua->get($request->to_url, ':content_file' => $content_file);
433             } elsif($method =~ /get/i){
434 0           $res = $ua->get($request->to_url);
435             } else {
436 0           $res = $ua->post($request->to_url, Content_Type => 'form-data', Content => $content );
437             }
438              
439 0 0         if ($res->is_success) {
440 0 0         print "Got Content ", $res->content, "\n" if $self->debug;
441 0           my $data;
442 0           eval {
443 0           $data = from_json($res->content);
444             };
445 0 0         if($@) {
446             # this doesn't look like JSON, might be file content
447 0           return $res->content;
448             }
449 0           $data->{http_response_code} = $res->code();
450 0           return to_json($data);
451             } else {
452 0           $self->error($res->status_line);
453 0           return $opts->{error_handler}->($self, $res);
454             }
455 0           return;
456             }
457              
458             sub _talk_default_error_handler {
459 0     0     my $self = shift;
460 0           my $res = shift;
461              
462 0           warn "Something went wrong: ".$res->status_line;
463 0           return to_json({error => $res->status_line,
464             http_response_code => $res->code});
465             }
466              
467             =head1 AUTHOR
468              
469             Lenz Gschwendtner, C<< >>
470              
471             With Bug fixes from:
472              
473             Greg Knauss C<< gknauss at eod.com >>
474              
475             Chris Prather C<< chris at prather.org >>
476              
477             Shinichiro Aska
478              
479             [ktdreyer]
480              
481             SureVoIP L
482              
483             =head1 BUGS
484              
485             Please report any bugs through the web interface at
486             L. I will be notified, and then you'll
487             automatically be notified of progress on your bug as I make changes.
488              
489             =head1 SUPPORT
490              
491             You can find documentation for this module with the perldoc command.
492              
493             perldoc Net::Dropbox::API
494              
495             You can also look for information at:
496              
497             =over 4
498              
499             =item * AnnoCPAN: Annotated CPAN documentation
500              
501             L
502              
503             =item * CPAN Ratings
504              
505             L
506              
507             =item * Search CPAN
508              
509             L
510              
511             =back
512              
513              
514             =head1 COPYRIGHT & LICENSE
515              
516             Copyright 2010 Lenz Gschwendtner.
517              
518             This program is free software; you can redistribute it and/or modify it
519             under the terms of either: the GNU General Public License as published
520             by the Free Software Foundation; or the Artistic License.
521              
522             See http://dev.perl.org/licenses/ for more information.
523              
524              
525             =cut
526              
527             1; # End of Net::Dropbox