File Coverage

blib/lib/SugarSync/API.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             #! perl
2              
3             package SugarSync::API;
4              
5 1     1   19187 use warnings;
  1         3  
  1         25  
6 1     1   4 use strict;
  1         1  
  1         27  
7 1     1   11 use v5.10;
  1         6  
  1         32  
8              
9 1     1   941 use LWP::UserAgent;
  1         991757  
  1         42  
10 1     1   12 use Carp;
  1         1  
  1         111  
11 1     1   487 use XML::Simple;
  0            
  0            
12             use Data::Dumper;
13              
14             use constant APIURL => 'https://api.sugarsync.com';
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.06';
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             my ( $pkg, $user, $pass, $akeyid, $pacckey ) = @_;
48             my $self = {};
49             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. Thanks.
54             $self->{_akeyid} = $akeyid // 'NDA4Mzg1MTI3Mzk2NjIwMTYwNA';
55             $self->{_pacckey} = $pacckey // 'ZjZlZmUyZmUyNjUwNGQ1NmJjOGMyOTA1YzljYjYyYTI';
56              
57             if ( defined($user) and defined($pass) ) {
58             $self->get_auth( $user, $pass );
59             }
60             $self;
61             }
62              
63             sub api_url {
64             # Convenience: API url plus possible addition.
65             my ( $self, $extra ) = @_;
66             my $ret = APIURL;
67             $ret .= "/" . $extra if $extra;
68             $ret;
69             }
70              
71             =head2 get_auth( username, password )
72              
73             Get the authorization token for subsequent calls and stores it
74             internally to be used with other method calls.
75              
76             Returns the authorization token.
77              
78             =cut
79              
80             sub get_auth {
81             my ( $self, $username, $password ) = @_;
82              
83             # Use stored information if available.
84             $username //= $self->{_authdata}->[0];
85             $password //= $self->{_authdata}->[1];
86              
87             # Strictly speaking, we need to encode the fields into UTF-8.
88             # Currently, assume ASCII...
89             my $msg = <
90            
91            
92             $username
93             $password
94             @{[ $self->{_akeyid} ]}
95             @{[ $self->{_pacckey} ]}
96            
97             EOD
98              
99             # Alternatively, use XML::Writer.
100             # A bit overkill since this is the only piece of XML we'll need.
101             # my $xml = XML::Writer->new( OUTPUT => \$msg );
102             # $xml->startTag("authRequest");
103             # $xml->dataElement( "username", $username );
104             # $xml->dataElement( "password", $password );
105             # $xml->dataElement( "accessKeyId", AKEYID );
106             # $xml->dataElement( "privateAccessKey", PACCKEY );
107             # $xml->endTag("authRequest");
108             # $xml->end;
109              
110             my $ua = LWP::UserAgent->new( agent => 'perl post' );
111             my $res = $ua->post( $self->api_url('authorization'),
112             Content_Type => 'text/xml',
113             Content => $msg );
114              
115             # Returns "201 Created" upon success.
116             Carp::croak( $res->error_as_HTML ) unless $res->is_success;
117             warn( $res->as_string ) if $debug;
118             my $loc = $1 if $res->as_string =~ /Location:\s+(.*)$/m;
119             Carp::croak("Authentication failed") unless $loc;
120              
121             # Store information so we can re-authenticate when the token expires.
122             # Currently, 1 hour.
123             $self->{_authdata} = [ $username, $password ];
124              
125             # Store authentication token.
126             $self->{_auth} = $loc;
127             }
128              
129             =head2 get_userinfo
130              
131             Retrieves the user info, e.g., quota, shared folders and so on.
132              
133             =cut
134              
135             sub get_userinfo {
136             my ( $self ) = @_;
137             $self->{userinfo} = 'xxx'; # prevent recursion
138             my $ui = $self->get_url_xml( $self->api_url('user') );
139             $self->{userinfo} = $ui;
140             $self->{$_} = $ui->{$_}
141             for qw(nickname username receivedShares syncfolders);
142             }
143              
144             =head2 get_receivedShares
145              
146             Returns the data for the shared folders.
147              
148             =cut
149              
150             sub get_receivedShares {
151             my ( $self ) = @_;
152              
153             # Make sure we have user info.
154             $self->get_userinfo unless $self->{userinfo};
155              
156             $self->{receivedShare} = $self->get_url_xml( $self->{receivedShares} )->{receivedShare};
157             }
158              
159             =head2 get_receivedShare( $share )
160              
161             Retrieves detailed information for a shared folder.
162              
163             =cut
164              
165             sub get_receivedShare {
166             my ( $self, $share ) = @_;
167             $self->get_url_xml($share);
168             }
169              
170             =head2 get_files( $folder )
171              
172             Retrieves the files data for a folder.
173              
174             =cut
175              
176             sub get_files {
177             my ( $self, $folder ) = @_;
178             my $res = $self->get_url_xml($folder);
179             if ( $res->{hasMore} eq 'false' ) {
180             return $res->{file}; # ????
181             }
182             else {
183             croak("Files has more -- NYI");
184             }
185             }
186              
187             =head2 get_collections( $folder )
188              
189             Retrieves the collections data for a folder.
190              
191             =cut
192              
193             sub get_collections {
194             my ( $self, $folder ) = @_;
195             my $res = $self->get_url_xml($folder);
196             if ( $res->{hasMore} eq 'false' ) {
197             return $res->{collection};
198             }
199             else {
200             croak("Collection has more -- NYI");
201             }
202             }
203              
204             =head2 get_url_data( $url )
205              
206             Retrieves the raw data for a given url.
207              
208             Handles basic errors, like 401 (authentication token expired) and
209             temporary server failures.
210              
211             =cut
212              
213             my $error;
214              
215             sub get_url_data {
216              
217             # Get the data for the url which must be valid XML.
218             # This is the central query function.
219             # Upon some other errors, it will retry.
220             # Upon auth errors, it will try to re-authenticate.
221              
222             my ( $self, $url ) = @_;
223              
224             # Make sure we have user info.
225             $self->get_userinfo unless $self->{userinfo};
226              
227             my $ua = LWP::UserAgent->new( agent => 'perl get' );
228             $ua->default_header( 'Authorization', $self->{_auth} );
229              
230             my $res = $ua->get($url);
231             unless ( $res->is_success ) {
232             my $line = $res->status_line;
233             if ( $line =~ /^(401)/ && $error++ < 10 ) {
234             # Authentication token expired.
235             warn("Reauth... ($line) #$error\n");
236             sleep( $error || 1 );
237             $self->get_auth;
238             return $self->get_url_data($url);
239             }
240             elsif ( $line =~ /^(50\d)/ && $error++ < 10 ) {
241             # Server unavailable of some sort.
242             warn("Retry... ($line) #$error\n");
243             sleep( $error || 1);
244             return $self->get_url_data($url);
245             }
246             Carp::croak( $line );
247             }
248             $error = 0;
249             return $res->content;
250             }
251              
252             =head2 get_url_xml( $url, $dump )
253              
254             Retrieves the XML data for a given url and returns it as a Perl structure.
255              
256             Optionally, dumps (using Data::Dumper) the structure to STDERR.
257              
258             =cut
259              
260             sub get_url_xml {
261              
262             # Get the data for the url which must be valid XML.
263             # Return the XML data as a Perl structure.
264             # Optionally, dump the structure for debugging.
265              
266             my ( $self, $url, $ddump ) = @_;
267              
268             local( $Data::Dumper::Indent ) = 1;
269              
270             my $res = XMLin( $self->get_url_data($url) );
271             Carp::croak( "Not a HASH result: $res")
272             unless UNIVERSAL::isa( $res, 'HASH' );
273             warn Data::Dumper->Dump( [$res], [qw(xml_result)] ) if $ddump;
274              
275             # Make single-element list for lists.
276             if ( keys(%$res) == 1 ) {
277             my $k = (keys(%$res))[0];
278             $res = { $k => [ $res->{$k} ] };
279             warn Data::Dumper->Dump( [$res], [qw(xml_cooked)] ) if $ddump;
280             }
281              
282             return $res;
283             }
284              
285             use Time::Local;
286              
287             sub ts_deparse {
288              
289             # Deparse a 2011-08-28T23:03:48.000-07:00 into a Unix epoch time.
290              
291             my ( $self, $ts ) = @_;
292             Carp::croak("Invalid timestamp: $ts")
293             unless $ts =~ /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d).(\d+)-(\d+):(\d+)$/;
294             my $t = timegm( $6, $5, $4, $3, $2-1, $1 ) + 3600*$8 + 60*$9;
295             #warn("$ts -> ".localtime($t)."\n");
296             return $t;
297             }
298              
299             1;
300              
301             =SEE ALSO
302              
303             L
304              
305             =head1 AUTHOR
306              
307             Johan Vromans, C<< >>
308              
309             =head1 BUGS
310              
311             Please report any bugs or feature requests to C
312             rt.cpan.org>, or through the web interface at
313             L. I
314             will be notified, and then you'll automatically be notified of
315             progress on your bug as I make changes.
316              
317             =head1 SUPPORT
318              
319             You can find documentation for this module with the perldoc command.
320              
321             perldoc SugarSync::API
322             perldoc SugarSync::API::Data
323              
324             You can also look for information at:
325              
326             =over 4
327              
328             =item * RT: CPAN's request tracker
329              
330             L
331              
332             =item * CPAN Ratings
333              
334             L
335              
336             =item * Search CPAN
337              
338             L
339              
340             =back
341              
342             =head1 ACKNOWLEDGEMENTS
343              
344             Thanks to Mark Willis for producing a non-functional php module.
345              
346             =head1 COPYRIGHT & LICENSE
347              
348             Copyright 2011 Johan Vromans, all rights reserved.
349              
350             This program is free software; you can redistribute it and/or modify it
351             under the same terms as Perl itself.
352              
353             =cut
354              
355             1;