File Coverage

blib/lib/Net/Google/Drive/Simple/V2.pm
Criterion Covered Total %
statement 56 224 25.0
branch 11 90 12.2
condition 1 17 5.8
subroutine 11 23 47.8
pod 15 15 100.0
total 94 369 25.4


line stmt bran cond sub pod time code
1             ###########################################
2             ###########################################
3              
4             use strict;
5 3     3   19 use warnings;
  3         5  
  3         71  
6 3     3   12  
  3         4  
  3         69  
7             use parent qw< Net::Google::Drive::Simple::Core >;
8 3     3   729 use LWP::UserAgent ();
  3         494  
  3         12  
9 3     3   133 use HTTP::Request ();
  3         6  
  3         50  
10 3     3   13  
  3         6  
  3         61  
11             use File::Basename qw( basename );
12 3     3   12  
  3         4  
  3         1411  
13             use JSON qw( from_json to_json );
14 3     3   16 use Log::Log4perl qw(:easy);
  3         7  
  3         15  
15 3     3   250  
  3         5  
  3         14  
16             our $VERSION = '3.01';
17              
18             ###########################################
19             ###########################################
20             my ( $class, %options ) = @_;
21             return $class->SUPER::new(
22 2     2 1 4 %options,
23 2         14 api_file_url => 'https://www.googleapis.com/drive/v2/files',
24             api_upload_url => 'https://www.googleapis.com/upload/drive/v2/files',
25             );
26             }
27              
28             ###########################################
29             ###########################################
30             my ( $self, $opts, $search_opts ) = @_;
31              
32             if ( !defined $search_opts ) {
33 0     0 1 0 $search_opts = {};
34             }
35 0 0       0 $search_opts = {
36 0         0 page => 1,
37             %$search_opts,
38             };
39 0         0  
40             if ( !defined $opts ) {
41             $opts = {};
42             }
43 0 0       0  
44 0         0 $self->init();
45              
46             if ( my $title = $search_opts->{title} ) {
47 0         0 $title =~ s|\'|\\\'|g;
48             if ( defined $opts->{q} && length $opts->{q} ) {
49 0 0       0 $opts->{q} .= ' AND ';
50 0         0 }
51 0 0 0     0  
52 0         0 $opts->{q} .= "title = '$title'";
53             }
54              
55 0         0 my @docs = ();
56              
57             while (1) {
58 0         0 my $url = $self->file_url($opts);
59             my $data = $self->http_json($url);
60 0         0 return unless defined $data;
61 0         0 my $next_item = $self->item_iterator($data);
62 0         0  
63 0 0       0 while ( my $item = $next_item->() ) {
64 0         0 if ( $item->{kind} eq "drive#file" ) {
65             my $file = $item->{originalFilename};
66 0         0 if ( !defined $file ) {
67 0 0       0 DEBUG "Skipping $item->{ title } (no originalFilename)";
68 0         0 next;
69 0 0       0 }
70 0         0  
71 0         0 push @docs, $self->data_factory($item);
72             }
73             else {
74 0         0 DEBUG "Skipping $item->{ title } ($item->{ kind })";
75             }
76             }
77 0         0  
78             if ( $search_opts->{page} and $data->{nextPageToken} ) {
79             $opts->{pageToken} = $data->{nextPageToken};
80             }
81 0 0 0     0 else {
82 0         0 last;
83             }
84             }
85 0         0  
86             return \@docs;
87             }
88              
89 0         0 ###########################################
90             ###########################################
91             my ( $self, $title, $parent ) = @_;
92              
93             return $self->file_create( $title, "application/vnd.google-apps.folder", $parent );
94             }
95 0     0 1 0  
96             ###########################################
97 0         0 ###########################################
98             my ( $self, $title, $mime_type, $parent ) = @_;
99              
100             my $url = URI->new( $self->{api_file_url} );
101              
102             my $data = $self->http_json(
103 0     0 1 0 $url,
104             {
105 0         0 title => $title,
106             parents => [ { id => $parent } ],
107 0         0 mimeType => $mime_type,
108             }
109             );
110              
111             return unless defined $data;
112              
113             return $data->{id};
114             }
115              
116 0 0       0 ###########################################
117             ###########################################
118 0         0 my ( $self, $file, $parent_id, $file_id, $opts ) = @_;
119              
120             $opts = {} if !defined $opts;
121              
122             # Since a file upload can take a long time, refresh the token
123             # just in case.
124 0     0 1 0 $self->{oauth}->token_expire();
125              
126 0 0       0 my $title = basename $file;
127              
128             # First, insert the file placeholder, according to
129             # http://stackoverflow.com/questions/10317638
130 0         0 my $mime_type = $self->file_mime_type($file);
131              
132 0         0 my $url;
133              
134             if ( !defined $file_id ) {
135             $url = URI->new( $self->{api_file_url} );
136 0         0  
137             my $data = $self->http_json(
138 0         0 $url,
139             {
140 0 0       0 mimeType => $mime_type,
141 0         0 parents => [ { id => $parent_id } ],
142             title => $opts->{title} ? $opts->{title} : $title,
143             description => $opts->{description},
144             }
145             );
146              
147             return unless defined $data;
148              
149             $file_id = $data->{id};
150             }
151 0 0       0  
152             $url = URI->new( $self->{api_upload_url} . "/$file_id" );
153 0 0       0 $url->query_form( uploadType => "media" );
154              
155 0         0 my $file_length = -s $file;
156             my $file_data = $self->_content_sub($file);
157              
158 0         0 if (
159 0         0 $self->http_put(
160             $url,
161 0         0 {
162 0         0 'Content-Type' => $mime_type,
163             'Content' => $file_data,
164 0 0       0 'Content-Length' => $file_length
165             }
166             )
167             ) {
168             return $file_id;
169             }
170             }
171              
172             ###########################################
173             ###########################################
174 0         0 my ( $self, $file_id, $new_name ) = @_;
175              
176             my $url = URI->new( $self->{api_file_url} . "/$file_id" );
177              
178             if (
179             $self->http_put(
180             $url,
181 0     0 1 0 {
182             "Accept" => "application/json",
183 0         0 "Content-Type" => "application/json",
184             Content => to_json( { title => $new_name } ),
185 0 0       0 }
186             )
187             ) {
188             return 1;
189             }
190             return;
191              
192             }
193              
194             ###########################################
195 0         0 ###########################################
196             my ( $self, $url, $params ) = @_;
197 0         0  
198             my $content = delete $params->{Content};
199             my $req = HTTP::Request->new(
200             'PUT',
201             $url->as_string,
202             [ $self->{oauth}->authorization_headers(), %$params ],
203             );
204 0     0 1 0  
205             # $content can be a string or a CODE ref. For example rename() calls us with a string, but
206 0         0 # file_upload() calls us with a CODE ref. The HTTP::Request::new() only accepts a string,
207             # so we set the content of the request after calling the constructor.
208             $req->content($content);
209             my $resp = $self->http_loop($req);
210 0         0  
211             if ( $resp->is_error ) {
212             $self->error( $resp->message() );
213             return;
214             }
215             DEBUG $resp->as_string;
216 0         0 return $resp;
217 0         0 }
218              
219 0 0       0 ###########################################
220 0         0 ###########################################
221 0         0 my ( $self, $path, $target_folder ) = @_;
222              
223 0         0 my $url;
224 0         0  
225             if ( !defined $path or !defined $target_folder ) {
226             LOGDIE "Missing parameter";
227             }
228              
229             # Determine the file's parent in the path
230 0     0 1 0 my ( $file_id, $folder_id ) = $self->path_resolve($path);
231              
232 0         0 if ( !defined $file_id ) {
233             LOGDIE "Cannot find source file: $path";
234 0 0 0     0 }
235 0         0  
236             my ($target_folder_id) = $self->path_resolve($target_folder);
237              
238             if ( !defined $target_folder_id ) {
239 0         0 LOGDIE "Cannot find destination path: $target_folder";
240             }
241 0 0       0  
242 0         0 print "file_id=$file_id\n";
243             print "folder_id=$folder_id\n";
244             print "target_folder_id=$target_folder_id\n";
245 0         0  
246             # Delete it from the current parent
247 0 0       0 $url = URI->new( $self->{api_file_url} . "/$folder_id/children/$file_id" );
248 0         0 if ( !$self->http_delete($url) ) {
249             LOGDIE "Failed to remove $path from parent folder.";
250             }
251 0         0  
252 0         0 # Add a new parent
253 0         0 $url = URI->new( $self->{api_file_url} . "/$target_folder_id/children" );
254             if ( !$self->http_json( $url, { id => $file_id } ) ) {
255             LOGDIE "Failed to insert $path into $target_folder.";
256 0         0 }
257 0 0       0  
258 0         0 return 1;
259             }
260              
261             ###########################################
262 0         0 ###########################################
263 0 0       0 my ( $self, $path, $search_opts ) = @_;
264 0         0  
265             $search_opts = {} if !defined $search_opts;
266              
267 0         0 my @parts = grep { $_ ne '' } split '/', $path;
268              
269             my @ids = qw(root);
270             my $folder_id = my $parent = "root";
271             DEBUG "Parent: $parent";
272              
273 0     0 1 0 PART: for my $part (@parts) {
274              
275 0 0       0 DEBUG "Looking up part $part (folder_id=$folder_id)";
276              
277 0         0 my $children = $self->children_by_folder_id(
  0         0  
278             $folder_id,
279 0         0 {
280 0         0 maxResults => 100, # path resolution maxResults is different
281 0         0 },
282             { %$search_opts, title => $part },
283 0         0 );
284              
285 0         0 return unless defined $children;
286              
287 0         0 for my $child (@$children) {
288             DEBUG "Found child ", $child->title();
289             if ( $child->title() eq $part ) {
290             $folder_id = $child->id();
291             unshift @ids, $folder_id;
292             $parent = $folder_id;
293             DEBUG "Parent: $parent";
294             next PART;
295 0 0       0 }
296             }
297 0         0  
298 0         0 my $msg = "Child $part not found";
299 0 0       0 $self->error($msg);
300 0         0 ERROR $msg;
301 0         0 return;
302 0         0 }
303 0         0  
304 0         0 if ( @ids == 1 ) {
305              
306             # parent of root is root
307             return ( @ids, @ids );
308 0         0 }
309 0         0  
310 0         0 return (@ids);
311 0         0 }
312              
313             ###########################################
314 0 0       0 ###########################################
315             my ( $self, $file_id ) = @_;
316              
317 0         0 my $url;
318              
319             LOGDIE 'Deletion requires file_id' if ( !defined $file_id );
320 0         0  
321             $url = URI->new( $self->{api_file_url} . "/$file_id" );
322              
323             if ( $self->http_delete($url) ) {
324             return $file_id;
325             }
326 0     0 1 0  
327             return;
328 0         0 }
329              
330 0 0       0 ###########################################
331             ###########################################
332 0         0 my ( $self, $url ) = @_;
333              
334 0 0       0 my $req = HTTP::Request->new(
335 0         0 'DELETE',
336             $url,
337             [ $self->{oauth}->authorization_headers() ],
338 0         0 );
339              
340             my $resp = $self->http_loop($req);
341              
342             DEBUG $resp->as_string;
343              
344 0     0 1 0 if ( $resp->is_error ) {
345             $self->error( $resp->message() );
346             return;
347             }
348              
349 0         0 return 1;
350             }
351              
352 0         0 ###########################################
353             ###########################################
354 0         0 my ( $self, $folder_id, $opts, $search_opts ) = @_;
355              
356 0 0       0 $self->init();
357 0         0  
358 0         0 $search_opts = {} unless defined $search_opts;
359             $search_opts->{page} = 1 unless exists $search_opts->{page};
360              
361 0         0 if ( !defined $opts ) {
362             $opts = {
363             maxResults => 100,
364             };
365             }
366              
367 1     1 1 2 my $url = URI->new( $self->{api_file_url} );
368             $opts->{'q'} = "'$folder_id' in parents";
369 1         4  
370             if ( my $title = $search_opts->{title} ) {
371 1 50       6 $title =~ s|\'|\\\'|g;
372 1 50       3 $opts->{q} .= " AND title = '$title'";
373             }
374 1 50       3  
375 1         2 my @children = ();
376              
377             while (1) {
378             $url->query_form($opts);
379              
380 1         8 my $data = $self->http_json($url);
381 1         204 return unless defined $data;
382              
383 1 50       3 my $next_item = $self->item_iterator($data);
384 0         0  
385 0         0 while ( my $item = $next_item->() ) {
386             push @children, $self->data_factory($item);
387             }
388 1         2  
389             if ( $search_opts->{page} and $data->{nextPageToken} ) {
390 1         15 $opts->{pageToken} = $data->{nextPageToken};
391 1         4 }
392             else {
393 1         4 last;
394 1 50       5 }
395             }
396 1         6  
397             return \@children;
398 1         3 }
399 2         11  
400             ###########################################
401             ###########################################
402 1 50 33     5 my ( $self, $path, $opts, $search_opts ) = @_;
403 0         0  
404             DEBUG "Determine children of $path";
405             LOGDIE "No $path given" unless defined $path;
406 1         5  
407             $search_opts = {} unless defined $search_opts;
408              
409             my ( $folder_id, $parent ) = $self->path_resolve( $path, $search_opts );
410 1         8  
411             return unless defined $folder_id;
412              
413             DEBUG "Getting content of folder $folder_id";
414             my $children = $self->children_by_folder_id(
415             $folder_id, $opts,
416 1     1 1 5 $search_opts
417             );
418 1         5  
419 1 50       8 return unless defined $children;
420              
421 1 50       2 return wantarray ? ( $children, $folder_id ) : $children;
422             }
423 1         4  
424             ###########################################
425 1 50       5 ###########################################
426             my ( $self, $opts, $search_opts, $query ) = @_;
427 1         3 $search_opts ||= { page => 1 };
428 1         6  
429             $self->init();
430              
431             if ( !defined $opts ) {
432             $opts = {
433 1 50       2 maxResults => 100,
434             };
435 1 50       4 }
436              
437             my $url = URI->new( $self->{api_file_url} );
438              
439             $opts->{'q'} = $query;
440              
441 0     0 1   my @children = ();
442 0   0        
443             while (1) {
444 0           $url->query_form($opts);
445              
446 0 0         my $data = $self->http_json($url);
447 0           return unless defined $data;
448              
449             my $next_item = $self->item_iterator($data);
450              
451             while ( my $item = $next_item->() ) {
452 0           push @children, $self->data_factory($item);
453             }
454 0            
455             if ( $search_opts->{page} and $data->{nextPageToken} ) {
456 0           $opts->{pageToken} = $data->{nextPageToken};
457             }
458 0           else {
459 0           last;
460             }
461 0           }
462 0 0          
463             return \@children;
464 0           }
465              
466 0           ###########################################
467 0           ###########################################
468             my ( $self, $url, $local_file ) = @_;
469              
470 0 0 0       $self->init();
471 0            
472             if ( ref $url ) {
473             $url = $url->downloadUrl();
474 0           }
475              
476             my $req = HTTP::Request->new(
477             GET => $url,
478 0           );
479             $req->header( $self->{oauth}->authorization_headers() );
480              
481             my $ua = LWP::UserAgent->new();
482             my $resp = $ua->request( $req, $local_file );
483              
484 0     0 1   if ( $resp->is_error() ) {
485             my $msg = "Can't download $url (" . $resp->message() . ")";
486 0           ERROR $msg;
487             $self->error($msg);
488 0 0         return;
489 0           }
490              
491             if ($local_file) {
492 0           return 1;
493             }
494              
495 0           return $resp->content();
496             }
497 0            
498 0           1;
499              
500 0 0         =head2 METHODS
501 0            
502 0           =over 4
503 0            
504 0           =item C<new>
505              
506             my $gd_v2 = Net::Google::Drive::Simple::V2->new();
507 0 0          
508 0           # same as:
509             my $gd_v2 = Net::Google::Drive::Simple->new( 'version' => 2 );
510              
511 0           # same as:
512             my $gd_v2 = Net::Google::Drive::Simple->new();
513              
514             =item C<my $children = $gd-E<gt>children( "/path/to" )>
515              
516             Return the entries under a given path on the Google Drive as a reference
517             to an array. Each entry
518             is an object composed of the JSON data returned by the Google Drive API.
519             Each object offers methods named like the fields in the JSON data, e.g.
520             C<originalFilename()>, C<downloadUrl>, etc.
521              
522             Will return all entries found unless C<maxResults> is set:
523              
524             my $children = $gd->children( "/path/to", { maxResults => 3 } )
525              
526             Due to the somewhat capricious ways Google Drive handles its directory
527             structures, the method needs to traverse the path component by component
528             and determine the ID of each directory to get to the next level. To speed
529             up subsequent lookups, it also returns the ID of the last component to the
530             caller:
531              
532             my( $children, $parent ) = $gd->children( "/path/to" );
533              
534             If the caller now wants to e.g. insert a file into the directory, its
535             ID is available in $parent.
536              
537             Each child comes back as a files#resource type and gets mapped into
538             an object that offers access to the various fields via methods:
539              
540             for my $child ( @$children ) {
541             print $child->kind(), " ", $child->title(), "\n";
542             }
543              
544             Please refer to
545              
546             https://developers.google.com/drive/v2/reference/files#resource
547              
548             for details on which fields are available.
549              
550             =item C<my $files = $gd-E<gt>files( )>
551              
552             Return all files on the drive as a reference to an array.
553             Will return all entries found unless C<maxResults> is set:
554              
555             my $files = $gd->files( { maxResults => 3 } )
556              
557             Note that Google limits the number of entries returned by default to
558             100, and seems to restrict the maximum number of files returned
559             by a single query to 3,500, even if you specify higher values for
560             C<maxResults>.
561              
562             Each file comes back as an object that offers access to the Google
563             Drive item's fields, according to the API (see C<children()>).
564              
565             =item C<my $id = $gd-E<gt>folder_create( "folder-name", $parent_id )>
566              
567             Create a new folder as a child of the folder with the id C<$parent_id>.
568             Returns the ID of the new folder or undef in case of an error.
569              
570             =item C<my $id = $gd-E<gt>file_create( "folder-name", "mime-type", $parent_id )>
571              
572             Create a new file with the given mime type as a child of the folder with the id C<$parent_id>.
573             Returns the ID of the new file or undef in case of an error.
574              
575             Example to create an empty google spreadsheet:
576              
577             my $id = $gd->file_create( "Quarter Results", "application/vnd.google-apps.spreadsheet", "root" );
578              
579             =item C<$gd-E<gt>file_upload( $file, $dir_id )>
580              
581             Uploads the content of the file C<$file> into the directory with the ID
582             $dir_id on Google Drive. Uses C<$file> as the file name.
583              
584             To overwrite an existing file on Google Drive, specify the file's ID as
585             an optional parameter:
586              
587             $gd->file_upload( $file, $dir_id, $file_id );
588              
589             =item C<$gd-E<gt>rename( $file_id, $name )>
590              
591             Renames the file or folder with C<$file_id> to the specified C<$name>.
592              
593             =item C<$gd-E<gt>download( $item, [$local_filename] )>
594              
595             Downloads an item found via C<files()> or C<children()>. Also accepts
596             the downloadUrl of an item. If C<$local_filename> is not specified,
597             C<download()> will return the data downloaded (this might be undesirable
598             for large files). If C<$local_filename> is specified, C<download()> will
599             store the downloaded data under the given file name.
600              
601             my $gd = Net::Google::Drive::Simple->new();
602             my $files = $gd->files( { maxResults => 20 }, { page => 0 } );
603             for my $file ( @$files ) {
604             my $name = $file->originalFilename();
605             print "Downloading $name\n";
606             $gd->download( $file, $name ) or die "failed: $!";
607             }
608              
609             Be aware that only documents like PDF or png can be downloaded directly. Google Drive Documents like spreadsheets or (text) documents need to be exported into one of the available formats.
610             Check for "exportLinks" on a file given. In case of a document that can be exported you will receive a hash in the form:
611              
612             {
613             'format_1' => 'download_link_1',
614             'format_2' => 'download_link_2',
615             ...
616             }
617              
618             Choose your download link and use it as an argument to the download() function which can also take urls directly.
619              
620             my $gd = Net::Google::Drive::Simple->new();
621             my $children = $gd->children( '/path/to/folder/on/google/drive' );
622             for my $child ( @$children ) {
623             if ($child->can( 'exportLinks' )){
624             my $type_chosen;
625             foreach my $type (keys %{$child->exportLinks()}){
626             # Take any type you can get..
627             $type_chosen = $type;
628             # ..but choose your preferred format, opendocument here:
629             last if $type =~/oasis\.opendocument/;
630             }
631             my $url = $child->exportLinks()->{$type_chosen};
632              
633             $gd->download($url, 'my/local/file');
634              
635             }
636             }
637              
638             =item C<my $files = $gd-E<gt>search( )>
639              
640             my $children= $gd->search({ maxResults => 20 },{ page => 0 },
641             "title contains 'Futurama'");
642              
643             Search files for attributes. See
644             L<https://developers.google.com/drive/web/search-parameters>
645             for a definition of the attributes.
646              
647             To list all available files, those on the drive, those directly shared
648             with the user, and those generally available to the user, use an
649             empty search:
650              
651             my $children= $gd->search({},{ page => 0 },"");
652              
653             =item C<$gd-E<gt>file_delete( file_id )>
654              
655             Delete the file with the specified ID from Google Drive.
656              
657             =item C<$gd-E<gt>drive_mvdir( "/gdrive/path/to/file", "/path/to/new/folder" )>
658              
659             Move an existing file to a new folder. Removes the file's "parent"
660             setting (pointing to the old folder) and then adds the new folder as a
661             new parent.
662              
663             =item C<children_by_folder_id>
664              
665             =item C<file_mvdir>
666              
667             =item C<http_delete>
668              
669             =item C<http_put>
670              
671             =back
672              
673             =head1 LEGALESE
674              
675             Copyright 2012-2019 by Mike Schilli, all rights reserved.
676             This program is free software, you can redistribute it and/or
677             modify it under the same terms as Perl itself.
678              
679             =head1 AUTHOR
680              
681             2019, Nicolas R. <cpan@atoomic.org>
682             2012-2019, Mike Schilli <cpan@perlmeister.com>