File Coverage

blib/lib/SugarSync/API.pm
Criterion Covered Total %
statement 26 144 18.0
branch 0 58 0.0
condition 0 33 0.0
subroutine 9 21 42.8
pod 10 12 83.3
total 45 268 16.7


line stmt bran cond sub pod time code
1             #! perl
2              
3             package SugarSync::API;
4              
5 1     1   15702 use warnings;
  1         2  
  1         34  
6 1     1   4 use strict;
  1         1  
  1         21  
7 1     1   10 use v5.10;
  1         11  
8              
9 1     1   739 use LWP::UserAgent;
  1         33085  
  1         27  
10 1     1   8 use Carp;
  1         1  
  1         65  
11 1     1   842 use XML::Simple;
  1         6392  
  1         6  
12 1     1   709 use Data::Dumper;
  1         4891  
  1         53  
13              
14 1     1   4 use constant APIURL => 'https://api.sugarsync.com';
  1         1  
  1         1218  
15              
16             my $debug = 0;
17              
18             =head1 NAME
19              
20             SugarSync::API - Basic API to SugarSync cloud file sharing.
21              
22             =cut
23              
24             our $VERSION = '0.07';
25              
26             =head1 SYNOPSIS
27              
28             use SugarSync::API;
29             my $sushi = SugarSync::API->new( $username, $password );
30             $sushi->get_userinfo;
31             say "My SugarSync nickname is ", $sushi->{nickname};
32              
33             Data structures are discussed in L.
34              
35             =head1 METHODS
36              
37             =head2 new [ username, password ]
38              
39             Create a new API object.
40              
41             If you pass a username and password, the object will be authorized.
42             Otherwise, an explicit call to the method C is required.
43              
44             =cut
45              
46             sub new {
47 0     0 1   my ( $pkg, $user, $pass, $akeyid, $pacckey, $appid ) = @_;
48 0           my $self = {};
49 0           bless $self, $pkg;
50              
51             # Developer keys.
52             # IMPORTANT: If you're going to develop your own application please
53             # register with SugarSync and obtain your own access keys.
54 0   0       $self->{_akeyid} = $akeyid // 'ODxxxxxxxxxxxxxxxxxxxxxxxxx';
55 0   0       $self->{_pacckey} = $pacckey // 'OTxxxxxxxxxxxxxxxxxxxxxxxxx';
56 0   0       $self->{_appid} = $appid // '/sc/xxxxxxxxxxxxxxxxxxxxxxx';
57              
58 0 0 0       if ( defined($user) and defined($pass) ) {
59 0           $self->get_auth( $user, $pass );
60             }
61 0           $self;
62             }
63              
64             sub api_url {
65             # Convenience: API url plus possible addition.
66 0     0 0   my ( $self, $extra ) = @_;
67 0           my $ret = APIURL;
68 0 0         $ret .= "/" . $extra if $extra;
69 0           $ret;
70             }
71              
72             =head2 get_auth( username, password )
73              
74             Get the authorization token for subsequent calls and stores it
75             internally to be used with other method calls.
76              
77             Returns the authorization token.
78              
79             =cut
80              
81             sub get_auth {
82 0     0 1   my ( $self, $username, $password ) = @_;
83              
84 0           my $ua = LWP::UserAgent->new( agent => 'perl post' );
85              
86 0 0         unless ( $self->{_authdata}->[2] ) {
87             # Use stored information if available.
88 0   0       $username //= $self->{_authdata}->[0];
89 0   0       $password //= $self->{_authdata}->[1];
90              
91             # Strictly speaking, we need to encode the fields into UTF-8.
92             # Currently, assume ASCII...
93 0           my $msg = <
94            
95            
96             $username
97             $password
98 0           @{[ $self->{_appid} ]}
99 0           @{[ $self->{_akeyid} ]}
100 0           @{[ $self->{_pacckey} ]}
101            
102             EOD
103              
104             # Alternatively, use XML::Writer.
105             # A bit overkill since this is the only piece of XML we'll need.
106             # my $xml = XML::Writer->new( OUTPUT => \$msg );
107             # $xml->startTag("authRequest");
108             # $xml->dataElement( "username", $username );
109             # $xml->dataElement( "password", $password );
110             # $xml->dataElement( "accessKeyId", AKEYID );
111             # $xml->dataElement( "privateAccessKey", PACCKEY );
112             # $xml->endTag("authRequest");
113             # $xml->end;
114              
115 0           my $res = $ua->post( $self->api_url('app-authorization'),
116             Content_Type => 'text/xml',
117             Content => $msg );
118              
119             # Returns "201 Created" upon success.
120 0 0         Carp::croak( $res->error_as_HTML ) unless $res->is_success;
121 0 0         warn( $res->as_string ) if $debug;
122 0 0         my $loc = $1 if $res->as_string =~ /Location:\s+(.*)$/m;
123 0 0         Carp::croak("Failed to get a refresh token") unless $loc;
124              
125             # Store information so we can re-authenticate when the token expires.
126             # Currently, 1 hour.
127 0           $self->{_authdata} = [ $username, $password, $loc ];
128             }
129              
130 0           my $loc = $self->{_authdata}->[2];
131 0           my $msg = <
132            
133            
134             $loc
135 0           @{[ $self->{_akeyid} ]}
136 0           @{[ $self->{_pacckey} ]}
137            
138             EOD
139              
140 0           my $res = $ua->post( $self->api_url('authorization'),
141             Content_Type => 'text/xml',
142             Content => $msg );
143              
144 0 0         Carp::croak( $res->error_as_HTML ) unless $res->is_success;
145 0 0         warn( $res->as_string ) if $debug;
146 0 0         $loc = $1 if $res->as_string =~ /Location:\s+(.*)$/m;
147 0 0         Carp::croak("Failed to get an access token") unless $loc;
148              
149             # Store authentication token.
150 0           $self->{_auth} = $loc;
151             }
152              
153             =head2 get_userinfo
154              
155             Retrieves the user info, e.g., quota, shared folders and so on.
156              
157             =cut
158              
159             sub get_userinfo {
160 0     0 1   my ( $self ) = @_;
161 0           $self->{userinfo} = 'xxx'; # prevent recursion
162 0           my $ui = $self->get_url_xml( $self->api_url('user') );
163 0           $self->{userinfo} = $ui;
164             $self->{$_} = $ui->{$_}
165 0           for qw(nickname username receivedShares syncfolders);
166             }
167              
168             =head2 get_receivedShares
169              
170             Returns the data for the shared folders.
171              
172             =cut
173              
174             sub get_receivedShares {
175 0     0 1   my ( $self ) = @_;
176              
177             # Make sure we have user info.
178 0 0         $self->get_userinfo unless $self->{userinfo};
179              
180 0           $self->{receivedShare} = $self->get_url_xml( $self->{receivedShares} )->{receivedShare};
181             }
182              
183             =head2 get_receivedShare( $share )
184              
185             Retrieves detailed information for a shared folder.
186              
187             =cut
188              
189             sub get_receivedShare {
190 0     0 1   my ( $self, $share ) = @_;
191 0           $self->get_url_xml($share);
192             }
193              
194             =head2 get_files( $folder )
195              
196             Retrieves the files data for a folder.
197              
198             =cut
199              
200             sub get_files {
201 0     0 1   my ( $self, $folder ) = @_;
202 0           my $res = $self->get_url_xml($folder);
203 0 0         if ( $res->{hasMore} eq 'false' ) {
204 0           return $res->{file}; # ????
205             }
206             else {
207 0           croak("Files has more -- NYI");
208             }
209             }
210              
211             =head2 get_collections( $folder )
212              
213             Retrieves the collections data for a folder.
214              
215             =cut
216              
217             sub get_collections {
218 0     0 1   my ( $self, $folder ) = @_;
219 0           my $res = $self->get_url_xml($folder);
220 0 0         if ( $res->{hasMore} eq 'false' ) {
221 0           return $res->{collection};
222             }
223             else {
224 0           croak("Collection has more -- NYI");
225             }
226             }
227              
228             =head2 get_url_data( $url )
229              
230             Retrieves the raw data for a given url.
231              
232             Handles basic errors, like 401 (authentication token expired) and
233             temporary server failures.
234              
235             =cut
236              
237             my $error;
238              
239             sub get_url_data {
240              
241             # Get the data for the url which must be valid XML.
242             # This is the central query function.
243             # Upon some other errors, it will retry.
244             # Upon auth errors, it will try to re-authenticate.
245              
246 0     0 1   my ( $self, $url ) = @_;
247              
248 0 0         unless ( $url ) {
249 0           local( $Data::Dumper::Indent ) = 1;
250 0           warn Data::Dumper->Dump( [$self], [qw(object)] );
251 0           Carp::cluck( "No URL?" );
252 0           return;
253             }
254              
255             # Make sure we have user info.
256 0 0         $self->get_userinfo unless $self->{userinfo};
257              
258 0           my $ua = LWP::UserAgent->new( agent => 'perl get' );
259 0           $ua->default_header( 'Authorization', $self->{_auth} );
260 0           $ua->default_header( 'Host', 'api.sugarsync.com' );
261              
262 0           my $res = $ua->get($url);
263 0 0         unless ( $res->is_success ) {
264 0           my $line = $res->status_line;
265 0 0 0       if ( $line =~ /^(401)/ && $error++ < 20 ) {
    0 0        
266             # Authentication token expired.
267 0           warn("Reauth... ($line) #$error\n");
268 0           sleep( 1 );
269 0           $self->get_auth;
270 0           return $self->get_url_data($url);
271             }
272             elsif ( $line =~ /^(50\d)/ && $error++ < 20 ) {
273             # Server unavailable of some sort.
274 0           warn("Retry... ($line) #$error\n");
275 0   0       sleep( $error || 1);
276 0           return $self->get_url_data($url);
277             }
278 0           Carp::croak( $line );
279             }
280 0           $error = 0;
281 0           return $res->content;
282             }
283              
284             =head2 get_url_xml( $url, $dump )
285              
286             Retrieves the XML data for a given url and returns it as a Perl structure.
287              
288             Optionally, dumps (using Data::Dumper) the structure to STDERR.
289              
290             =cut
291              
292             sub get_url_xml {
293              
294             # Get the data for the url which must be valid XML.
295             # Return the XML data as a Perl structure.
296             # Optionally, dump the structure for debugging.
297              
298 0     0 1   my ( $self, $url, $ddump ) = @_;
299              
300 0           local( $Data::Dumper::Indent ) = 1;
301              
302 0           my $res = XMLin( $self->get_url_data($url) );
303 0 0         Carp::croak( "Not a HASH result: $res")
304             unless UNIVERSAL::isa( $res, 'HASH' );
305 0 0         warn Data::Dumper->Dump( [$res], [qw(xml_result)] ) if $ddump;
306              
307             # Make single-element list for lists, if necessary.
308 0 0         if ( keys(%$res) == 1 ) {
309 0           my $k = (keys(%$res))[0];
310 0 0         unless ( UNIVERSAL::isa( $res->{$k}, 'ARRAY' ) ) {
311 0           $res = { $k => [ $res->{$k} ] };
312 0 0         warn Data::Dumper->Dump( [$res], [qw(xml_cooked)] ) if $ddump;
313             }
314             }
315              
316 0           return $res;
317             }
318              
319             =head2 delete_url
320              
321             Experimental.
322              
323             =cut
324              
325             sub delete_url {
326 0     0 1   my ( $self, $url ) = @_;
327              
328             # Make sure we have user info.
329 0 0         $self->get_userinfo unless $self->{userinfo};
330              
331 0           my $ua = LWP::UserAgent->new( agent => 'perl get' );
332 0           $ua->default_header( 'Authorization', $self->{_auth} );
333 0           $ua->default_header( 'Host', 'api.sugarsync.com' );
334              
335 0           require HTTP::Request;
336 0           my $req = HTTP::Request->new( DELETE => $url );
337 0           my $res = $ua->request($req);
338 0 0         unless ( $res->is_success ) {
339 0           my $line = $res->status_line;
340 0 0 0       if ( $line =~ /^(401)/ && $error++ < 10 ) {
    0 0        
341             # Authentication token expired.
342 0           warn("Reauth... ($line) #$error\n");
343 0   0       sleep( $error || 1 );
344 0           $self->get_auth;
345 0           return $self->delete_url($url);
346             }
347             elsif ( $line =~ /^(50\d)/ && $error++ < 10 ) {
348             # Server unavailable of some sort.
349 0           warn("Retry... ($line) #$error\n");
350 0   0       sleep( $error || 1);
351 0           return $self->delete_url($url);
352             }
353 0           Carp::croak( $line );
354             }
355 0           $error = 0;
356 0           return $res->content;
357             }
358              
359 1     1   5 use Time::Local;
  1         1  
  1         151  
360              
361             sub ts_deparse {
362              
363             # Deparse a 2011-08-28T23:03:48.000-07:00 into a Unix epoch time.
364              
365 0     0 0   my ( $self, $ts ) = @_;
366 0 0         Carp::croak("Invalid timestamp: $ts")
367             unless $ts =~ /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d).(\d+)-(\d+):(\d+)$/;
368 0           my $t = timegm( $6, $5, $4, $3, $2-1, $1 ) + 3600*$8 + 60*$9;
369             #warn("$ts -> ".localtime($t)."\n");
370 0           return $t;
371             }
372              
373             1;
374              
375             =SEE ALSO
376              
377             L
378              
379             =head1 AUTHOR
380              
381             Johan Vromans, C<< >>
382              
383             =head1 BUGS
384              
385             Please report any bugs or feature requests to C
386             rt.cpan.org>, or through the web interface at
387             L. I
388             will be notified, and then you'll automatically be notified of
389             progress on your bug as I make changes.
390              
391             =head1 SUPPORT
392              
393             You can find documentation for this module with the perldoc command.
394              
395             perldoc SugarSync::API
396             perldoc SugarSync::API::Data
397              
398             You can also look for information at:
399              
400             =over 4
401              
402             =item * RT: CPAN's request tracker
403              
404             L
405              
406             =item * CPAN Ratings
407              
408             L
409              
410             =item * Search CPAN
411              
412             L
413              
414             =back
415              
416             =head1 ACKNOWLEDGEMENTS
417              
418             Thanks to Mark Willis for producing a non-functional php module.
419              
420             =head1 COPYRIGHT & LICENSE
421              
422             Copyright 2011 Johan Vromans, all rights reserved.
423              
424             This program is free software; you can redistribute it and/or modify it
425             under the same terms as Perl itself.
426              
427             =cut
428              
429             1;