Net-Google-Drive/lib/Net/Google/Drive.pm
package Net::Google::Drive;
use 5.008001;
use strict;
use warnings;
use utf8;
use LWP::UserAgent;
use HTTP::Request;
use JSON::XS;
use URI;
use File::Basename;
use Carp qw/carp croak/;
use Net::Google::OAuth;
our $VERSION = '0.02';
our $DOWNLOAD_BUFF_SIZE = 1024;
our $UPLOAD_BUFF_SIZE = 256 * 1024;
our $FILE_API_URL = 'https://www.googleapis.com/drive/v3/files';
our $FILE_API2_URL = 'https://www.googleapis.com/drive/v2/files';
our $UPLOAD_FILE_API_URL = 'https://www.googleapis.com/upload/drive/v3/files';
sub new {
my ($class, %opt) = @_;
my $self = {};
my $client_id = $opt{-client_id} // croak "You must specify '-client_id' param";
my $client_secret = $opt{-client_secret} // croak "You must specify '-client_secret' param";
$self->{access_token} = $opt{-access_token} // croak "You must specify '-access_token' param";
$self->{refresh_token} = $opt{-refresh_token} // croak "You must specify '-refresh_token' param";
$self->{ua} = LWP::UserAgent->new();
$self->{oauth} = Net::Google::OAuth->new(
-client_id => $client_id,
-client_secret => $client_secret,
);
bless $self, $class;
return $self;
}
sub searchFileByName {
my ($self, %opt) = @_;
my $filename = $opt{-filename} || croak "You must specify '-filename' param";
my $search_res = $self->__searchFile('name=\'' . $filename . "'");
return $search_res;
}
sub searchFileByNameContains {
my ($self, %opt) = @_;
my $filename = $opt{-filename} || croak "You must specify '-filename' param";
my $search_res = $self->__searchFile('name contains \'' . $filename . "'");
return $search_res;
}
sub downloadFile {
my ($self, %opt) = @_;
my $file_id = $opt{-file_id} || croak "You must specify '-file_id' param";
my $dest_file = $opt{-dest_file} || croak "You must specify '-dest_file' param";
my $ua = $self->{ua};
my $access_token = $self->__getAccessToken();
my $uri = URI->new(join('/', $FILE_API_URL, $file_id));
$uri->query_form(
'alt' => 'media',
);
my $headers = [
'Authorization' => 'Bearer ' . $access_token,
];
my $request = HTTP::Request->new( 'GET',
$uri,
$headers,
);
my $FL;
my $response = $ua->request($request, sub {
if (not $FL) {
open $FL, ">$dest_file" or croak "Cant open $dest_file to write $!";
binmode $FL;
}
print $FL $_[0];
},
$DOWNLOAD_BUFF_SIZE
);
if ($FL) {
close $FL;
}
my $response_code = $response->code();
if ($response_code != 200) {
my $error_message = __readErrorMessageFromResponse($response);
croak "Can't download file id: $file_id to destination file: $dest_file. Code: $response_code. Message: '$error_message'";
}
return 1;
}
sub deleteFile {
my ($self, %opt) = @_;
my $file_id = $opt{-file_id} || croak "You must specify '-file_id' param";
my $access_token = $self->__getAccessToken();
my $uri = URI->new(join('/', $FILE_API_URL, $file_id));
my $headers = [
'Authorization' => 'Bearer ' . $access_token,
];
my $request = HTTP::Request->new( 'DELETE',
$uri,
$headers,
);
my $response = $self->{ua}->request($request);
my $response_code = $response->code();
if ($response_code =~ /^[^2]/) {
my $error_message = __readErrorMessageFromResponse($response);
croak "Can't delete file id: $file_id. Code: $response_code. Message: $error_message";
}
return 1;
}
sub uploadFile {
my ($self, %opt) = @_;
my $source_file = $opt{-source_file} || croak "You must specify '-source_file' param";
if (not -f $source_file) {
croak "File: $source_file not exists";
}
my $file_size = (stat $source_file)[7];
my $part_upload_uri = $self->__createEmptyFile($source_file, $file_size, $opt{-parents});
open my $FH, "<$source_file" or croak "Can't open file: $source_file $!";
binmode $FH;
my $filebuf;
my $uri = URI->new($part_upload_uri);
my $start_byte = 0;
while (my $bytes = read($FH, $filebuf, $UPLOAD_BUFF_SIZE)) {
my $end_byte = $start_byte + $bytes - 1;
my $headers = [
'Content-Length' => $bytes,
'Content-Range' => sprintf("bytes %d-%d/%d", $start_byte, $end_byte, $file_size),
];
my $request = HTTP::Request->new('PUT', $uri, $headers, $filebuf);
# Send request to upload part of file
my $response = $self->{ua}->request($request);
my $response_code = $response->code();
# On end part, response code is 200, on middle part is 308
if ($response_code == 200 || $response_code == 201) {
if ($end_byte + 1 != $file_size) {
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";
}
return decode_json($response->content());
}
elsif ($response_code != 308) {
croak "Wrong response code on upload part file. Code: $response_code. File: $source_file";
}
$start_byte += $bytes;
}
close $FH;
return;
}
sub setFilePermission {
my ($self, %opt) = @_;
my $file_id = $opt{-file_id} || croak "You must specify '-file_id' param";
my $permission_type = $opt{-type} || croak "You must specify '-type' param";
my $role = $opt{-role} || croak "You must specify '-role' param";
my %valid_permissions = (
'user' => 1,
'group' => 1,
'domain' => 1,
'anyone' => 1,
);
my %valid_roles = (
'owner' => 1,
'organizer' => 1,
'fileOrganizer' => 1,
'writer' => 1,
'commenter' => 1,
'reader' => 1,
);
#Check permission in param
if (not $valid_permissions{$permission_type}) {
croak "Wrong permission type: '$permission_type'. Valid permissions types: " . join(' ', keys %valid_permissions);
}
#Check role in param
if (not $valid_roles{$role}) {
croak "Wrong role: '$role'. Valid roles: " . join(' ', keys %valid_roles);
}
my $access_token = $self->__getAccessToken();
my $uri = URI->new(join('/', $FILE_API_URL, $file_id, 'permissions'));
my $headers = [
'Authorization' => 'Bearer ' . $access_token,
'Content-Type' => 'application/json',
];
my $request_content = {
'type' => $permission_type,
'role' => $role,
};
my $request = HTTP::Request->new('POST', $uri, $headers, encode_json($request_content));
my $response = $self->{ua}->request($request);
my $response_code = $response->code();
if ($response_code != 200) {
my $error_message = __readErrorMessageFromResponse($response);
croak "Can't share file id: $file_id. Code: $response_code. Error message: $error_message";
}
return decode_json($response->content());
}
sub getFileMetadata {
my ($self, %opt) = @_;
my $file_id = $opt{-file_id} || croak "You must specify '-file_id' param";
my $access_token = $self->__getAccessToken();
my $uri = URI->new(join('/', $FILE_API2_URL, $file_id));
$uri->query_form('supportsTeamDrives' => 'true');
my $headers = [
'Authorization' => 'Bearer ' . $access_token,
];
my $request = HTTP::Request->new("GET", $uri, $headers);
my $response = $self->{ua}->request($request);
my $response_code = $response->code();
if ($response_code != 200) {
my $error_message = __readErrorMessageFromResponse($response);
croak "Can't get metadata from file id: $file_id. Code: $response_code. Error message: $error_message";
}
return decode_json($response->content());
}
sub shareFile {
my ($self, %opt) = @_;
my $file_id = $opt{-file_id} || croak "You must specify '-file_id' param";
## Adding permissions to file
my $permission = $self->setFilePermission(
-file_id => $file_id,
-type => 'anyone',
-role => 'reader',
);
if ((not exists $permission->{type}) || ($permission->{type} ne 'anyone')) {
croak "Can't set permission to anyone for file id: $file_id";
}
my $metadata = $self->getFileMetadata( -file_id => $file_id );
return $metadata->{webContentLink};
}
sub __createEmptyFile {
my ($self, $source_file, $file_size, $parents) = @_;
my $access_token = $self->__getAccessToken();
my $body = {
'name' => basename($source_file),
};
$body->{parents} = $parents if $parents;
my $body_json = encode_json($body);
my $uri = URI->new($UPLOAD_FILE_API_URL);
$uri->query_form('upload_type' => 'resumable');
my $headers = [
'Authorization' => 'Bearer ' . $access_token,
'Content-Length' => length($body_json),
'Content-Type' => 'application/json; charset=UTF-8',
'X-Upload-Content-Length' => $file_size,
];
my $request = HTTP::Request->new('POST', $uri, $headers, $body_json);
my $response = $self->{ua}->request($request);
my $response_code = $response->code();
if ($response_code != 200) {
my $error_message = __readErrorMessageFromResponse($response);
croak "Can't upload part of file. Code: $response_code. Error message: $error_message";
}
my $location = $response->header('Location') or croak "Location header not defined";
return $location;
}
sub __readErrorMessageFromResponse {
my ($response) = @_;
my $error_message = eval {decode_json($response->content)};
if ($error_message) {
return $error_message->{error}->{message};
}
return '';
}
sub __searchFile {
my ($self, $q) = @_;
my $access_token = $self->__getAccessToken();
my $headers = [
'Authorization' => 'Bearer ' . $access_token,
];
my $uri = URI->new($FILE_API_URL);
$uri->query_form('q' => $q);
my $request = HTTP::Request->new('GET',
$uri,
$headers,
);
my $files = [];
$self->__apiRequest($request, $files);
return $files;
}
sub __apiRequest {
my ($self, $request, $files) = @_;
my $response = $self->{ua}->request($request);
my $response_code = $response->code;
if ($response_code != 200) {
croak "Wrong response code on search_file. Code: $response_code";
}
my $json_res = decode_json($response->content);
if (my $next_token = $json_res->{next_token}) {
my $uri = $request->uri;
$uri->query_form('next_token' => $next_token);
$self->__apiRequest($request, $files);
}
push @$files, @{$json_res->{files}};
return 1;
}
sub __getAccessToken {
my ($self) = @_;
my $oauth = $self->{oauth};
my $token_info =
eval {
$oauth->getTokenInfo( -access_token => $self->{access_token} );
};
# If error on get token info or token is expired
if (not $@) {
if ((exists $token_info->{expires_in}) && ($token_info->{expires_in} > 5)) {
return $self->{access_token};
}
}
#Refresh token
$oauth->refreshToken( -refresh_token => $self->{refresh_token} );
$self->{refresh_token} = $oauth->getRefreshToken();
$self->{access_token} = $oauth->getAccessToken();
return $self->{access_token};
}
1;
=encoding utf8
=head1 NAME
B<Net::Google::Drive> - simple Google drive API module
=head1 SYNOPSIS
This module use to upload, download, share file on Google drive
use Net::Google::Drive;
#Create disk object. You need send in param 'access_token', 'refresh_token', 'client_id' and 'client_secret'.
#Values of 'client_id' and 'client_secret' uses to create Net::Google::OAuth object so that update value of 'access_token'.
my $disk = Net::Google::Drive->new(
-client_id => $client_id,
-client_secret => $client_secret,
-access_token => $access_token,
-refresh_token => $refresh_token,
);
# Search file by name
my $file_name = 'upload.doc';
my $files = $disk->searchFileByName( -filename => $file_name ) or croak "File '$file_name' not found";
my $file_id = $files->[0]->{id};
print "File id: $file_id\n";
#Download file
my $dest_file = '/home/upload.doc';
$disk->downloadFile(
-file_id => $file_id,
-dest_file => $dest_file,
);
#Upload file
my $source_file = '/home/upload.doc';
my $res = $disk->uploadFile( -source_file => $source_file );
print "File: $source_file uploaded. File id: $res->{id}\n";
=head1 METHODS
=head2 new(%opt)
Create L<Net::Google::Disk> object
%opt:
-client_id => Your app client id (Get from google when register your app)
-client_secret => Your app client secret (Get from google when register your app)
-access_token => Access token value (Get from L<Net::Google::OAuth>)
-refresh_token => Refresh token value (Get from L<Net::Google::OAuth>)
=head2 searchFileByName(%opt)
Search file on google disk by name. Return arrayref to info with found files. If files not found - return empty arrayref
%opt:
-filename => Name of file to find
Return:
[
[0] {
id "1f13sLfo6UEyUuFpn-NWPnY",
kind "drive#file",
mimeType "application/x-perl",
name "drive.t"
}
]
=head2 searchFileByNameContains(%opt)
Search files on google drive by name contains value in param '-filename'
Param and return value same as in method L<searchFileByName>
=head2 downloadFile(%opt)
Download file from google dist to -dest_file on local system. Return 1 if success, die in otherwise
%opt:
-dest_file => Name of file on disk in which you will download file from google disk
-file_id => Id of file on google disk
=head2 deleteFile(%opt)
Delete file from google disk. Return 1 if success, die in otherwise
%opt:
-file_id => Id of file on google disk
=head2 uploadFile(%opt)
Upload file from local system to google drive. Return file_info hashref if success, die in otherwise
%opt:
-source_file => File on local system
-parents => Optional arrayref of parent ids
Return:
{
id "1LVAr2PpqX9m314JyZ6YJ4v_KIzG0Gey2",
kind "drive#file",
mimeType "application/octet-stream",
name "gogle_upload_file"
}
=head2 setFilePermission(%opt)
Set permissions for file on google drive. Return permission hashref, die in otherwise
%opt:
-file_id => Id of file on google disk
-type => The type of the grantee. Valid values are: (user, group, domain, anyone)
-role => The role granted by this permission. Valid values are: (owner, organizer, fileOrganizer, writer, commenter, reader)
Return:
{
allowFileDiscovery JSON::PP::Boolean {
Parents Types::Serialiser::BooleanBase
public methods (0)
private methods (0)
internals: 0
},
id "anyoneWithLink",
kind "drive#permission",
role "reader",
type "anyone"
}
=head2 getFileMetadata(%opt)
Get metadata of file. Return hashref with metadata if success, die in otherwise
%opt:
-file_id => Id of file on google disk
Return:
{ alternateLink "https://drive.google.com/file/d/10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut/view?usp=drivesdk",
appDataContents JSON::PP::Boolean {
Parents Types::Serialiser::BooleanBase
public methods (0)
private methods (0)
internals: 0
},
capabilities {
canCopy JSON::PP::Boolean {
Parents Types::Serialiser::BooleanBase
public methods (0)
private methods (0)
internals: 1
},
canEdit var{capabilities}{canCopy}
},
copyable var{capabilities}{canCopy},
copyRequiresWriterPermission var{appDataContents},
createdDate "2018-10-04T12:05:15.896Z",
downloadUrl "https://doc-0g-7o-docs.googleusercontent.com/docs/securesc/ck8i7vfbvef13kb30b8mkrcjv4ihp2uj/3mfn1kbr655euhlo7tctg5mmn8oirg
gf/1538654400000/10526805100525201667/10526805100525201667/10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut?e=download&gd=true",
editable var{capabilities}{canCopy},
embedLink "https://drive.google.com/file/d/10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut/preview?usp=drivesdk",
etag ""omwGuTP8OdxhZkubyp-j43cFdJQ/MTUzODY1NDcxNTg5Ng"",
explicitlyTrashed var{appDataContents},
fileExtension "",
fileSize 1000000,
headRevisionId "0B4HgPHxdPy22UmZXSFVRTkRLbXhFakdzZjFSUGkrNWZIVFN3PQ",
iconLink "https://drive-thirdparty.googleusercontent.com/16/type/application/octet-stream",
id "10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut",
kind "drive#file",
labels {
hidden var{appDataContents},
restricted var{appDataContents},
starred var{appDataContents},
trashed var{appDataContents},
viewed var{appDataContents}
},
lastModifyingUser {
displayName "Ларри Уолл",
emailAddress "perlgogledrivemodule@gmail.com",
isAuthenticatedUser var{capabilities}{canCopy},
kind "drive#user",
permissionId 10526805100525201667
},
lastModifyingUserName "Ларри Уолл",
markedViewedByMeDate "1970-01-01T00:00:00.000Z",
md5Checksum "ded2a2983b3e1743152d8224549510e1",
mimeType "application/octet-stream",
modifiedByMeDate "2018-10-04T12:05:15.896Z",
modifiedDate "2018-10-04T12:05:15.896Z",
originalFilename "gogle_upload_file",
ownerNames [
[0] "Ларри Уолл"
],
owners [
[0] {
displayName "Ларри Уолл",
emailAddress "perlgogledrivemodule@gmail.com",
isAuthenticatedUser var{capabilities}{canCopy},
kind "drive#user",
permissionId 10526805100525201667
}
],
parents [
[0] {
id "0AIHgPHxdPy22Uk9PVA",
isRoot var{capabilities}{canCopy},
kind "drive#parentReference",
parentLink "https://www.googleapis.com/drive/v2/files/0AIHgPHxdPy22Uk9PVA",
selfLink "https://www.googleapis.com/drive/v2/files/10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut/parents/0AIHgPHxdPy22Uk9PVA"
}
],
quotaBytesUsed 1000000,
selfLink "https://www.googleapis.com/drive/v2/files/10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut",
shared var{appDataContents},
spaces [
[0] "drive"
],
title "gogle_upload_file",
userPermission {
etag ""omwGuTP8OdxhZkubyp-j43cFdJQ/N52l-iUAo-dARaTch8nQXOzl348"",
id "me",
kind "drive#permission",
role "owner",
selfLink "https://www.googleapis.com/drive/v2/files/10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut/permissions/me",
type "user"
},
version 2,
webContentLink "https://drive.google.com/uc?id=10Z5YDCHn3gnj0S4_Lf0poc2Lm5so0Sut&export=download",
writersCanShare var{capabilities}{canCopy}
}
=head2 shareFile(%opt)
Share file for download. Return download link if success, die in otherwise
%opt:
-file_id => Id of file on google disk
=head1 DEPENDENCE
L<Net::Google::OAuth>, L<LWP::UserAgent>, L<JSON::XS>, L<URI>, L<HTTP::Request>, L<File::Basename>
=head1 AUTHORS
=over 4
=item *
Pavel Andryushin <vrag867@gmail.com>
=back
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2018 by Pavel Andryushin.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut