PageRenderTime 64ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Net/Dropbox/API.pm

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