File Coverage

lib/Net/Google/Drive.pm
Criterion Covered Total %
statement 178 205 86.8
branch 26 46 56.5
condition 19 54 35.1
subroutine 25 26 96.1
pod 9 9 100.0
total 257 340 75.5


line stmt bran cond sub pod time code
1             package Net::Google::Drive;
2              
3 1     1   2638 use 5.008001;
  1         4  
4 1     1   8 use strict;
  1         10  
  1         36  
5 1     1   6 use warnings;
  1         1  
  1         26  
6 1     1   4 use utf8;
  1         2  
  1         4  
7              
8 1     1   725 use LWP::UserAgent;
  1         48215  
  1         39  
9 1     1   10 use HTTP::Request;
  1         2  
  1         26  
10 1     1   791 use JSON::XS;
  1         5231  
  1         58  
11 1     1   8 use URI;
  1         2  
  1         26  
12 1     1   5 use File::Basename;
  1         2  
  1         67  
13              
14 1     1   10 use Carp qw/carp croak/;
  1         1  
  1         48  
15              
16 1     1   502 use Net::Google::OAuth;
  1         1502  
  1         2714  
17              
18             our $VERSION = '0.02';
19              
20             our $DOWNLOAD_BUFF_SIZE = 1024;
21             our $UPLOAD_BUFF_SIZE = 256 * 1024;
22             our $FILE_API_URL = 'https://www.googleapis.com/drive/v3/files';
23             our $FILE_API2_URL = 'https://www.googleapis.com/drive/v2/files';
24             our $UPLOAD_FILE_API_URL = 'https://www.googleapis.com/upload/drive/v3/files';
25              
26             sub new {
27 1     1 1 163 my ($class, %opt) = @_;
28              
29 1         4 my $self = {};
30 1   33     6 my $client_id = $opt{-client_id} // croak "You must specify '-client_id' param";
31 1   33     4 my $client_secret = $opt{-client_secret} // croak "You must specify '-client_secret' param";
32 1   33     4 $self->{access_token} = $opt{-access_token} // croak "You must specify '-access_token' param";
33 1   33     15 $self->{refresh_token} = $opt{-refresh_token} // croak "You must specify '-refresh_token' param";
34 1         13 $self->{ua} = LWP::UserAgent->new();
35              
36 1         2929 $self->{oauth} = Net::Google::OAuth->new(
37             -client_id => $client_id,
38             -client_secret => $client_secret,
39             );
40 1         240386 bless $self, $class;
41 1         7 return $self;
42             }
43              
44             sub searchFileByName {
45 2     2 1 132709 my ($self, %opt) = @_;
46 2   33     11 my $filename = $opt{-filename} || croak "You must specify '-filename' param";
47              
48 2         14 my $search_res = $self->__searchFile('name=\'' . $filename . "'");
49            
50 2         12 return $search_res;
51             }
52              
53             sub searchFileByNameContains {
54 1     1 1 526 my ($self, %opt) = @_;
55 1   33     8 my $filename = $opt{-filename} || croak "You must specify '-filename' param";
56              
57 1         6 my $search_res = $self->__searchFile('name contains \'' . $filename . "'");
58            
59 1         8 return $search_res;
60             }
61              
62              
63             sub downloadFile {
64 1     1 1 1211 my ($self, %opt) = @_;
65 1   33     5 my $file_id = $opt{-file_id} || croak "You must specify '-file_id' param";
66 1   33     6 my $dest_file = $opt{-dest_file} || croak "You must specify '-dest_file' param";
67 1         3 my $ua = $self->{ua};
68 1         5 my $access_token = $self->__getAccessToken();
69              
70 1         13 my $uri = URI->new(join('/', $FILE_API_URL, $file_id));
71 1         119 $uri->query_form(
72             'alt' => 'media',
73             );
74 1         95 my $headers = [
75             'Authorization' => 'Bearer ' . $access_token,
76             ];
77              
78 1         8 my $request = HTTP::Request->new( 'GET',
79             $uri,
80             $headers,
81             );
82              
83 1         152 my $FL;
84             my $response = $ua->request($request, sub {
85 3 100   3   727858 if (not $FL) {
86 1 50       170 open $FL, ">$dest_file" or croak "Cant open $dest_file to write $!";
87 1         8 binmode $FL;
88             }
89 3         13 print $FL $_[0];
90             },
91 1         9 $DOWNLOAD_BUFF_SIZE
92             );
93 1 50       2057 if ($FL) {
94 1         62 close $FL;
95             }
96 1         8 my $response_code = $response->code();
97 1 50       16 if ($response_code != 200) {
98 0         0 my $error_message = __readErrorMessageFromResponse($response);
99 0         0 croak "Can't download file id: $file_id to destination file: $dest_file. Code: $response_code. Message: '$error_message'";
100             }
101 1         23 return 1;
102             }
103              
104             sub deleteFile {
105 1     1 1 944 my ($self, %opt) = @_;
106 1   33     6 my $file_id = $opt{-file_id} || croak "You must specify '-file_id' param";
107 1         5 my $access_token = $self->__getAccessToken();
108              
109 1         14 my $uri = URI->new(join('/', $FILE_API_URL, $file_id));
110              
111 1         405 my $headers = [
112             'Authorization' => 'Bearer ' . $access_token,
113             ];
114              
115 1         10 my $request = HTTP::Request->new( 'DELETE',
116             $uri,
117             $headers,
118             );
119 1         136 my $response = $self->{ua}->request($request);
120 1         639894 my $response_code = $response->code();
121 1 50       16 if ($response_code =~ /^[^2]/) {
122 0         0 my $error_message = __readErrorMessageFromResponse($response);
123 0         0 croak "Can't delete file id: $file_id. Code: $response_code. Message: $error_message";
124             }
125 1         22 return 1;
126             }
127              
128             sub uploadFile {
129 1     1 1 1194 my ($self, %opt) = @_;
130 1   33     21 my $source_file = $opt{-source_file} || croak "You must specify '-source_file' param";
131              
132 1 50       27 if (not -f $source_file) {
133 0         0 croak "File: $source_file not exists";
134             }
135              
136 1         14 my $file_size = (stat $source_file)[7];
137 1         9 my $part_upload_uri = $self->__createEmptyFile($source_file, $file_size, $opt{-parents});
138 1 50       91 open my $FH, "<$source_file" or croak "Can't open file: $source_file $!";
139 1         6 binmode $FH;
140              
141 1         2 my $filebuf;
142 1         502 my $uri = URI->new($part_upload_uri);
143 1         247 my $start_byte = 0;
144 1         522 while (my $bytes = read($FH, $filebuf, $UPLOAD_BUFF_SIZE)) {
145 4         21 my $end_byte = $start_byte + $bytes - 1;
146 4         33 my $headers = [
147             'Content-Length' => $bytes,
148             'Content-Range' => sprintf("bytes %d-%d/%d", $start_byte, $end_byte, $file_size),
149             ];
150 4         42 my $request = HTTP::Request->new('PUT', $uri, $headers, $filebuf);
151              
152             # Send request to upload part of file
153 4         1430 my $response = $self->{ua}->request($request);
154 4         1478671 my $response_code = $response->code();
155             # On end part, response code is 200, on middle part is 308
156 4 100 66     99 if ($response_code == 200 || $response_code == 201) {
    50          
157 1 50       5 if ($end_byte + 1 != $file_size) {
158 0         0 croak "Server return code: $response_code on upload file, but file is not fully uploaded. End byte: $end_byte. File size: $file_size. File: $source_file";
159             }
160 1         6 return decode_json($response->content());
161             }
162             elsif ($response_code != 308) {
163 0         0 croak "Wrong response code on upload part file. Code: $response_code. File: $source_file";
164             }
165 3         681 $start_byte += $bytes;
166             }
167 0         0 close $FH;
168              
169 0         0 return;
170             }
171              
172             sub setFilePermission {
173 3     3 1 1710 my ($self, %opt) = @_;
174 3   33     13 my $file_id = $opt{-file_id} || croak "You must specify '-file_id' param";
175 3   33     11 my $permission_type = $opt{-type} || croak "You must specify '-type' param";
176 3   33     8 my $role = $opt{-role} || croak "You must specify '-role' param";
177 3         16 my %valid_permissions = (
178             'user' => 1,
179             'group' => 1,
180             'domain' => 1,
181             'anyone' => 1,
182             );
183              
184 3         15 my %valid_roles = (
185             'owner' => 1,
186             'organizer' => 1,
187             'fileOrganizer' => 1,
188             'writer' => 1,
189             'commenter' => 1,
190             'reader' => 1,
191             );
192             #Check permission in param
193 3 100       9 if (not $valid_permissions{$permission_type}) {
194 1         167 croak "Wrong permission type: '$permission_type'. Valid permissions types: " . join(' ', keys %valid_permissions);
195             }
196              
197             #Check role in param
198 2 50       32 if (not $valid_roles{$role}) {
199 0         0 croak "Wrong role: '$role'. Valid roles: " . join(' ', keys %valid_roles);
200             }
201 2         11 my $access_token = $self->__getAccessToken();
202              
203 2         27 my $uri = URI->new(join('/', $FILE_API_URL, $file_id, 'permissions'));
204 2         274 my $headers = [
205             'Authorization' => 'Bearer ' . $access_token,
206             'Content-Type' => 'application/json',
207             ];
208 2         11 my $request_content = {
209             'type' => $permission_type,
210             'role' => $role,
211             };
212              
213 2         30 my $request = HTTP::Request->new('POST', $uri, $headers, encode_json($request_content));
214              
215 2         372 my $response = $self->{ua}->request($request);
216 2         1733043 my $response_code = $response->code();
217 2 50       27 if ($response_code != 200) {
218 0         0 my $error_message = __readErrorMessageFromResponse($response);
219 0         0 croak "Can't share file id: $file_id. Code: $response_code. Error message: $error_message";
220             }
221 2         10 return decode_json($response->content());
222             }
223              
224             sub getFileMetadata {
225 2     2 1 548 my ($self, %opt) = @_;
226 2   33     11 my $file_id = $opt{-file_id} || croak "You must specify '-file_id' param";
227 2         10 my $access_token = $self->__getAccessToken();
228              
229 2         28 my $uri = URI->new(join('/', $FILE_API2_URL, $file_id));
230 2         239 $uri->query_form('supportsTeamDrives' => 'true');
231              
232 2         180 my $headers = [
233             'Authorization' => 'Bearer ' . $access_token,
234             ];
235 2         16 my $request = HTTP::Request->new("GET", $uri, $headers);
236 2         272 my $response = $self->{ua}->request($request);
237 2         519518 my $response_code = $response->code();
238 2 50       27 if ($response_code != 200) {
239 0         0 my $error_message = __readErrorMessageFromResponse($response);
240 0         0 croak "Can't get metadata from file id: $file_id. Code: $response_code. Error message: $error_message";
241             }
242 2         8 return decode_json($response->content());
243             }
244              
245             sub shareFile {
246 1     1 1 1283 my ($self, %opt) = @_;
247 1   33     5 my $file_id = $opt{-file_id} || croak "You must specify '-file_id' param";
248              
249             ## Adding permissions to file
250 1         7 my $permission = $self->setFilePermission(
251             -file_id => $file_id,
252             -type => 'anyone',
253             -role => 'reader',
254             );
255 1 50 33     65 if ((not exists $permission->{type}) || ($permission->{type} ne 'anyone')) {
256 0         0 croak "Can't set permission to anyone for file id: $file_id";
257             }
258              
259 1         378 my $metadata = $self->getFileMetadata( -file_id => $file_id );
260 1         147 return $metadata->{webContentLink};
261             }
262              
263             sub __createEmptyFile {
264 1     1   5 my ($self, $source_file, $file_size, $parents) = @_;
265 1         5 my $access_token = $self->__getAccessToken();
266              
267 1         58 my $body = {
268             'name' => basename($source_file),
269             };
270 1 50       5 $body->{parents} = $parents if $parents;
271 1         9 my $body_json = encode_json($body);
272              
273 1         12 my $uri = URI->new($UPLOAD_FILE_API_URL);
274 1         114 $uri->query_form('upload_type' => 'resumable');
275 1         93 my $headers = [
276             'Authorization' => 'Bearer ' . $access_token,
277             'Content-Length' => length($body_json),
278             'Content-Type' => 'application/json; charset=UTF-8',
279             'X-Upload-Content-Length' => $file_size,
280             ];
281              
282 1         8 my $request = HTTP::Request->new('POST', $uri, $headers, $body_json);
283 1         236 my $response = $self->{ua}->request($request);
284              
285 1         240571 my $response_code = $response->code();
286 1 50       13 if ($response_code != 200) {
287 0         0 my $error_message = __readErrorMessageFromResponse($response);
288 0         0 croak "Can't upload part of file. Code: $response_code. Error message: $error_message";
289             }
290              
291 1 50       5 my $location = $response->header('Location') or croak "Location header not defined";
292              
293 1         64 return $location;
294             }
295              
296             sub __readErrorMessageFromResponse {
297 0     0   0 my ($response) = @_;
298 0         0 my $error_message = eval {decode_json($response->content)};
  0         0  
299 0 0       0 if ($error_message) {
300 0         0 return $error_message->{error}->{message};
301             }
302 0         0 return '';
303             }
304              
305              
306              
307             sub __searchFile {
308 3     3   9 my ($self, $q) = @_;
309              
310 3         12 my $access_token = $self->__getAccessToken();
311            
312 3         16 my $headers = [
313             'Authorization' => 'Bearer ' . $access_token,
314             ];
315              
316 3         27 my $uri = URI->new($FILE_API_URL);
317 3         345 $uri->query_form('q' => $q);
318 3         459 my $request = HTTP::Request->new('GET',
319             $uri,
320             $headers,
321             );
322 3         391 my $files = [];
323 3         15 $self->__apiRequest($request, $files);
324              
325              
326 3         28 return $files;
327             }
328              
329             sub __apiRequest {
330 3     3   11 my ($self, $request, $files) = @_;
331              
332 3         15 my $response = $self->{ua}->request($request);
333 3         1075245 my $response_code = $response->code;
334 3 50       42 if ($response_code != 200) {
335 0         0 croak "Wrong response code on search_file. Code: $response_code";
336             }
337              
338 3         15 my $json_res = decode_json($response->content);
339              
340 3 50       536 if (my $next_token = $json_res->{next_token}) {
341 0         0 my $uri = $request->uri;
342 0         0 $uri->query_form('next_token' => $next_token);
343 0         0 $self->__apiRequest($request, $files);
344             }
345 3         6 push @$files, @{$json_res->{files}};
  3         12  
346              
347 3         42 return 1;
348             }
349              
350             sub __getAccessToken {
351 10     10   27 my ($self) = @_;
352              
353 10         33 my $oauth = $self->{oauth};
354             my $token_info =
355 10         19 eval {
356 10         72 $oauth->getTokenInfo( -access_token => $self->{access_token} );
357             };
358             # If error on get token info or token is expired
359 10 100       1181493 if (not $@) {
360 9 50 33     102 if ((exists $token_info->{expires_in}) && ($token_info->{expires_in} > 5)) {
361 9         58 return $self->{access_token};
362             }
363             }
364              
365             #Refresh token
366 1         9 $oauth->refreshToken( -refresh_token => $self->{refresh_token} );
367 1         122191 $self->{refresh_token} = $oauth->getRefreshToken();
368 1         8 $self->{access_token} = $oauth->getAccessToken();
369              
370 1         7 return $self->{access_token};
371             }
372              
373             1;
374              
375             =encoding utf8
376              
377             =head1 NAME
378              
379             B - simple Google drive API module
380              
381             =head1 SYNOPSIS
382              
383             This module use to upload, download, share file on Google drive
384             use Net::Google::Drive;
385              
386             #Create disk object. You need send in param 'access_token', 'refresh_token', 'client_id' and 'client_secret'.
387             #Values of 'client_id' and 'client_secret' uses to create Net::Google::OAuth object so that update value of 'access_token'.
388             my $disk = Net::Google::Drive->new(
389             -client_id => $client_id,
390             -client_secret => $client_secret,
391             -access_token => $access_token,
392             -refresh_token => $refresh_token,
393             );
394              
395             # Search file by name
396             my $file_name = 'upload.doc';
397             my $files = $disk->searchFileByName( -filename => $file_name ) or croak "File '$file_name' not found";
398             my $file_id = $files->[0]->{id};
399             print "File id: $file_id\n";
400              
401             #Download file
402             my $dest_file = '/home/upload.doc';
403             $disk->downloadFile(
404             -file_id => $file_id,
405             -dest_file => $dest_file,
406             );
407              
408             #Upload file
409             my $source_file = '/home/upload.doc';
410             my $res = $disk->uploadFile( -source_file => $source_file );
411             print "File: $source_file uploaded. File id: $res->{id}\n";
412              
413             =head1 METHODS
414              
415             =head2 new(%opt)
416              
417             Create L object
418              
419             %opt:
420             -client_id => Your app client id (Get from google when register your app)
421             -client_secret => Your app client secret (Get from google when register your app)
422             -access_token => Access token value (Get from L)
423             -refresh_token => Refresh token value (Get from L)
424              
425             =head2 searchFileByName(%opt)
426              
427             Search file on google disk by name. Return arrayref to info with found files. If files not found - return empty arrayref
428              
429             %opt:
430             -filename => Name of file to find
431             Return:
432             [
433             [0] {
434             id "1f13sLfo6UEyUuFpn-NWPnY",
435             kind "drive#file",
436             mimeType "application/x-perl",
437             name "drive.t"
438             }
439             ]
440            
441             =head2 searchFileByNameContains(%opt)
442              
443             Search files on google drive by name contains value in param '-filename'
444             Param and return value same as in method L
445              
446             =head2 downloadFile(%opt)
447              
448             Download file from google dist to -dest_file on local system. Return 1 if success, die in otherwise
449              
450             %opt:
451             -dest_file => Name of file on disk in which you will download file from google disk
452             -file_id => Id of file on google disk
453              
454             =head2 deleteFile(%opt)
455              
456             Delete file from google disk. Return 1 if success, die in otherwise
457              
458             %opt:
459             -file_id => Id of file on google disk
460              
461             =head2 uploadFile(%opt)
462              
463             Upload file from local system to google drive. Return file_info hashref if success, die in otherwise
464            
465             %opt:
466             -source_file => File on local system
467             -parents => Optional arrayref of parent ids
468             Return:
469             {
470             id "1LVAr2PpqX9m314JyZ6YJ4v_KIzG0Gey2",
471             kind "drive#file",
472             mimeType "application/octet-stream",
473             name "gogle_upload_file"
474             }
475              
476             =head2 setFilePermission(%opt)
477              
478             Set permissions for file on google drive. Return permission hashref, die in otherwise
479              
480             %opt:
481             -file_id => Id of file on google disk
482             -type => The type of the grantee. Valid values are: (user, group, domain, anyone)
483             -role => The role granted by this permission. Valid values are: (owner, organizer, fileOrganizer, writer, commenter, reader)
484             Return:
485             {
486             allowFileDiscovery JSON::PP::Boolean {
487             Parents Types::Serialiser::BooleanBase
488             public methods (0)
489             private methods (0)
490             internals: 0
491             },
492             id "anyoneWithLink",
493             kind "drive#permission",
494             role "reader",
495             type "anyone"
496             }
497              
498             =head2 getFileMetadata(%opt)
499              
500             Get metadata of file. Return hashref with metadata if success, die in otherwise
501              
502             %opt:
503             -file_id => Id of file on google disk
504             Return:
505             { alternateLink "https://drive.google.com/file/d/10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut/view?usp=drivesdk",
506             appDataContents JSON::PP::Boolean {
507             Parents Types::Serialiser::BooleanBase
508             public methods (0)
509             private methods (0)
510             internals: 0
511             },
512             capabilities {
513             canCopy JSON::PP::Boolean {
514             Parents Types::Serialiser::BooleanBase
515             public methods (0)
516             private methods (0)
517             internals: 1
518             },
519             canEdit var{capabilities}{canCopy}
520             },
521             copyable var{capabilities}{canCopy},
522             copyRequiresWriterPermission var{appDataContents},
523             createdDate "2018-10-04T12:05:15.896Z",
524             downloadUrl "https://doc-0g-7o-docs.googleusercontent.com/docs/securesc/ck8i7vfbvef13kb30b8mkrcjv4ihp2uj/3mfn1kbr655euhlo7tctg5mmn8oirg
525             gf/1538654400000/10526805100525201667/10526805100525201667/10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut?e=download&gd=true",
526             editable var{capabilities}{canCopy},
527             embedLink "https://drive.google.com/file/d/10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut/preview?usp=drivesdk",
528             etag ""omwGuTP8OdxhZkubyp-j43cFdJQ/MTUzODY1NDcxNTg5Ng"",
529             explicitlyTrashed var{appDataContents},
530             fileExtension "",
531             fileSize 1000000,
532             headRevisionId "0B4HgPHxdPy22UmZXSFVRTkRLbXhFakdzZjFSUGkrNWZIVFN3PQ",
533             iconLink "https://drive-thirdparty.googleusercontent.com/16/type/application/octet-stream",
534             id "10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut",
535             kind "drive#file",
536             labels {
537             hidden var{appDataContents},
538             restricted var{appDataContents},
539             starred var{appDataContents},
540             trashed var{appDataContents},
541             viewed var{appDataContents}
542             },
543             lastModifyingUser {
544             displayName "Ларри Уолл",
545             emailAddress "perlgogledrivemodule@gmail.com",
546             isAuthenticatedUser var{capabilities}{canCopy},
547             kind "drive#user",
548             permissionId 10526805100525201667
549             },
550             lastModifyingUserName "Ларри Уолл",
551             markedViewedByMeDate "1970-01-01T00:00:00.000Z",
552             md5Checksum "ded2a2983b3e1743152d8224549510e1",
553             mimeType "application/octet-stream",
554             modifiedByMeDate "2018-10-04T12:05:15.896Z",
555             modifiedDate "2018-10-04T12:05:15.896Z",
556             originalFilename "gogle_upload_file",
557             ownerNames [
558             [0] "Ларри Уолл"
559             ],
560             owners [
561             [0] {
562             displayName "Ларри Уолл",
563             emailAddress "perlgogledrivemodule@gmail.com",
564             isAuthenticatedUser var{capabilities}{canCopy},
565             kind "drive#user",
566             permissionId 10526805100525201667
567             }
568             ],
569             parents [
570             [0] {
571             id "0AIHgPHxdPy22Uk9PVA",
572             isRoot var{capabilities}{canCopy},
573             kind "drive#parentReference",
574             parentLink "https://www.googleapis.com/drive/v2/files/0AIHgPHxdPy22Uk9PVA",
575             selfLink "https://www.googleapis.com/drive/v2/files/10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut/parents/0AIHgPHxdPy22Uk9PVA"
576             }
577             ],
578             quotaBytesUsed 1000000,
579             selfLink "https://www.googleapis.com/drive/v2/files/10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut",
580             shared var{appDataContents},
581             spaces [
582             [0] "drive"
583             ],
584             title "gogle_upload_file",
585             userPermission {
586             etag ""omwGuTP8OdxhZkubyp-j43cFdJQ/N52l-iUAo-dARaTch8nQXOzl348"",
587             id "me",
588             kind "drive#permission",
589             role "owner",
590             selfLink "https://www.googleapis.com/drive/v2/files/10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut/permissions/me",
591             type "user"
592             },
593             version 2,
594             webContentLink "https://drive.google.com/uc?id=10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut&export=download",
595             writersCanShare var{capabilities}{canCopy}
596             }
597              
598             =head2 shareFile(%opt)
599              
600             Share file for download. Return download link if success, die in otherwise
601              
602             %opt:
603             -file_id => Id of file on google disk
604              
605             =head1 DEPENDENCE
606              
607             L, L, L, L, L, L
608              
609             =head1 AUTHORS
610              
611             =over 4
612              
613             =item *
614              
615             Pavel Andryushin
616              
617             =back
618              
619             =head1 COPYRIGHT AND LICENSE
620              
621             This software is copyright (c) 2018 by Pavel Andryushin.
622              
623             This is free software; you can redistribute it and/or modify it under
624             the same terms as the Perl 5 programming language system itself.
625              
626             =cut