Using Google Drive with Perl
Tags: perl, googledrive, googlecloud
Published: 2021-10-14 22:23:47

I often use [Google Drive](https://en.wikipedia.org/wiki/Google_Drive) to transfer files between devices, for example from my Mac to my Linux laptop. It works great, but there can be some boring repetitive work involved depending on how often you do the same operation. For example, when I want to transfer a file from the Mac, I open up a web browser (usually Google Chrome) and point it to `drive.google.com`, the Google Drive web interface. From here, I try to locate the directory I want to upload the file to which might involve some scrolling in the directory listing. After that, I need to click on the directory name to view its content. Next, I right-click on an empty area in the directory list and select "Upload files" from a drop-down menu. Then a file selection dialog appears and I have to locate the directory and file I want to upload in the dialog, and after some more mouse movements and clicks I finally press the "Ok" button to select the file. Similarly, on the other device (like my Linux laptop) where I want to download the file, I need to locate the file in web UI and then right-click it and select "Download" from the drop-down menu to download the file. 

Luckily, Google has provided the Google Drive API that can be used by user scripts to communicate with the cloud server. So the first thing I wanted to check was if someone already had written Perl code that made use of this API. After a quick search on [MetaCPAN](https://en.wikipedia.org/wiki/CPAN) I found a module  [Net::Google::Drive::Simple](https://metacpan.org/pod/Net::Google::Drive::Simple) that looked promising. When reading through the module's documentation, I realized that the tricky part could be to get hold of an "access token". The script needs this token to be able to connect to the Google Drive API server. The module claimed that it could generate the access token from a secret credential token which I first had to obtain from Google Cloud. Then, when the module had generated this access token the rest should be fairly simple was my impression. Well.., let's see what happend 👾

## Creating a Google account

In order to get the Google Drive credentials, the first thing you need to have is a Google account (obviously). This allows you to use Google developer products, including Google Cloud Console, Cloud SDK, Cloud Logging, and Cloud Monitoring. If you don't have a Google account, you can visit   https://cloud.google.com/apis/docs/getting-started to get started. Creating a Google account, with some amount of cloud storage is free, see https://cloud.google.com/free for more information.

## Creating a Google project

Next, to use the Cloud APIs you need to set up a Google project. A project is equivalent to a developer account. It serves as a resource container for your Google Cloud resources and provides an isolation boundary for your usage of Google Cloud services, so you can manage quota limits and billing independently at the project level. Usage telemetry and dashboards are grouped by projects as well. If you don't already have a project, you can create one using the Cloud Console, see [this]( https://cloud.google.com/resource-manager/docs/creating-managing-projects) link for more information. Here is what I did to create a new project:

- In the [Google Cloud console](https://console.cloud.google.com/apis/dashboard), click the drop down menu in top left corner, and select "IAM & Admin" -> "Manage Resources",

- from the "Select organization" drop-down list at the top of the page, you can select the organization in which you want to create a project. If you are a free trial user, skip this step, as this list does not appear.

- Then click "Create Project", and in the "New Project" window that appears, enter a project name.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tlsm3j83guxata4j0tgi.png)  

     A project name can contain only letters, numbers, single quotes, hyphens, spaces, or exclamation points, and must be between 4 and 30 characters. I chose "DesktopApp" for the project name, and will use this name in the further discussion.

-  Enter the parent organization or folder in the "Location" box. That resource will be the hierarchical parent of the new project. If you do not have a parent folder, just leave "No organization" (as I did) in the "Location" box.

- When you're finished entering new project details, click the "CREATE" button.

## Enabling the Google Drive API

Next, in order to use Google Drive API from a script you need to enable it.

- In the [Google Cloud console](https://console.cloud.google.com/apis/dashboard), click the drop down menu in top left corner, and select "APIs & Services" -> "Library". Type "Google Drive" in the search bar. Select "Google Drive API" from the result list.

- Select the "DesktopApp" project, by clicking project button in the top menu bar. If it already shows "DesktopApp", you can skip this step.

- Click the "ENABLE" button.


## Configure the app's scopes and the consent screen

The script that we will write to access Google Drive is called an "app". The "user" of the script is in our case the same as the writer (ourselves) of the script, but it need not be. You could send the script to someone else or publish it on the internet. In order for the app to get access to the user's Google Drive, the user must approve that script can access his drive under a certain "scope". For example, the user may give the script read-only access. 

The first time the script is trying to access the drive, the user is presented with a "consent screen" where they can select which scopes the script can use to access their drive. After the user has given their consent, the Google Cloud server returns an access token to the script which it can use to access the drive without asking the user for permission the next time. The access token will expire after a certain time, and then the script needs to refresh the token. The user can also revoke the access right of the script at a later time if they so wishes, then the script's access token becomes invalid and it cannot be refreshed. The script must then repeat the process with presenting the user with the consent screen in order to get further access. Read more about the OAuth2 protocol for authentication [here](https://developers.google.com/identity/protocols/oauth2).

Now, in order to setup what scopes should be presented to the user in the consent screen (see [workspace/guides/create-credentials](https://developers.google.com/workspace/guides/create-credentials)) do the following:

- In the [Google Cloud console](https://console.cloud.google.com/apis/dashboard), click the drop down menu in top left corner, and select "APIs & Services" -> "Credentials". The credential page for your project appears.

- Select the "DesktopApp" project, by clicking project button in the top menu bar. If it already shows "DesktopApp", you can skip this step.

- Click the "CONFIGURE CONSENT SCREEN" button on the right side. (If you have already configured the consent screen and want to modify it, you should instead click on "OAuth consent screen" in the left side bar)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j19n9m1x6124aeamcq1r.png)

- Select the user type for the app. Click "External".
  (If you are a google workspace user and wants the app to be internal to your organization, you could select "Internal" instead.)

- Click the "CREATE" button

- In the App information screen, type in a name for the app. This name will appear in the consent screen that is presented to the user during the authorization procedure.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wpb1cge1fff0tuzltist.png)

- Enter your email in the "User support email" field.

- Skip the "App logo" field.

- Also skip the "App domain" fields.

- Also skip the "Authorized domains" field.

- Enter your email address in the "Developer contact information" field.

- Click the "SAVE AND CONTINUE" button

- In the "Edit app registration" screen, click the "ADD OR REMOVE SCOPES" button.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hbf84frm5bwu67oh8j5f.png)

- From the popup window "Update selected scopes" click the checkmark next to the following scopes:

  -  `.../auth/drive.file`
  -  `.../auth/drive.metadata.readonly`
  -  `.../auth/drive.readonly`
    ![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6wklz7q6ll7fxou15ne3.png)

      Note that only the scopes you select in the popup window can be requested by the app. For instance, if the app later requests all of the above three scopes, the user will be presented with a consent screen with the three alternatives, like this: 
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oigchzayg9mjwck70h6g.png)
      The first alternative in the screen shot above corresponds to the `drive.readonly` scope, the second corresponds to the `drive.metadata.readonly` scope, and the third one corresponds to the `drive.file` scope.
 
- Click the "UPDATE" button at the bottom of the window

- Click "SAVE AND CONTINUE" in the "Edit app registration" screen.

- In the "Test users" screen, click the "+ ADD USERS" button, and add yourself (your gmail address) to the list.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9asem9f77t2f1p7ikrg7.png)

- Click "SAVE AND CONTINUE".

- The Summary screen is shown. Click the "BACK TO DASHBOARD" button.

## Create credentials

In order for the app (the script) to obtain an access token, it needs to pass a set of credentials to to the Google authorization server. Together with the credentials, the script passes (as a parameter) the set of scopes that it would like to access. The authorization server then present the user of the script with a screen where they can log in with their Google account. After logging in, the user is asked whether they are willing to grant one or more permissions that your app is requesting. This process is called user consent as discussed in the previous section.

To obtain the set of credentials that the script should present to the authorization server (see: [workspace/guides/create-credentials](https://developers.google.com/workspace/guides/create-credentials)), we do the following:
  
- In the Google cloud console, click the drop down menu in top left corner, and select "APIs & Services" -> "Credentials". The credential page for your project appears.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9iq785aga4qfahi6sirs.png)

- Select the "DesktopApp" project, by clicking project button in the top menu bar. If it already shows "DesktopApp", you can skip this step.

- Click the "+ CREATE CREDENTIALS" button on the top of the
page. Select "OAuth Client ID" from the drop down menu.

- Select "Desktop app" from the "Application type" drop down menu.

- Select a client name in the "Name" field, or use the suggested name.

- Click the "CREATE" button at the bottom of the page.

- Click "OK" in the "OAuth client created" dialog box.

- Next, still in the Google cloud console: click the drop down menu in top left corner, and select "APIs & Services" -> "Credentials". The credential page for your project appears.

- Select the "DesktopApp" project, by clicking project button in the top menu bar. If it already shows "DesktopApp", you can skip this step.

- Under "OAuth 2.0 Client IDs", click the download button under the "Actions" heading.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4b8mwjqq5q4gpqc3gii5.png)

- Select "DOWNLOAD JSON" in the popup window.

- Rename the file as "credentials.json" on your computer.

## Obtaining the access token

Now as we have the credentials, we can obtain the access token. I have rewritten the [google-drive-init](https://metacpan.org/release/ATOOMIC/Net-Google-Drive-Simple-0.19/source/eg/google-drive-init) script provided by [Net::Google::Drive::Simple](https://metacpan.org/pod/Net::Google::Drive::Simple) slightly:

```perl
use v5.26; # Indented here-docs introduced in 5.26
use feature qw(say);
use strict;
use warnings;
use experimental qw(signatures);
use open qw(:std :encoding(utf-8));
use OAuth::Cmdline::GoogleDrive;
use OAuth::Cmdline::Mojo;
use JSON;
{
    my $credentials = read_credentials();
    my $token_dir = '.';
    # NOTE: if you change the scopes below, you should delete the previous
    #  access token file (.google-drive.yml) and then rerun this script..
    my @scopes  = (
        "drive.file",
        "drive.metadata.readonly",
        "drive.readonly"
    );
    my $scopes = join ' ', map { "https://www.googleapis.com/auth/$_" } @scopes;
    my $oauth = OAuth::Cmdline::GoogleDrive->new(
        client_id     => $credentials->{client_id},
        client_secret => $credentials->{client_secret},
        login_uri     => $credentials->{auth_uri},
        token_uri     => $credentials->{token_uri},
        scope         => $scopes,
        homedir       => $token_dir
    );
    print <<~'INFO_STR';
    Running the token collector web server at http://localhost:8082
    - Open your web browser and go to http://localhost:8082, then click the link
      "Login on google-drive" and fill out the OAuth consent screen granting
      your Perl scripts access to your google drive.
    - Click the check boxes for what the app can access (the scopes)
    - Then click "Continue"
    - When the message "Tokens saved in ./.google-drive.yml" appears, the
      access token was successfully generated and saved to the local file
      .google-drive.yml.
    - You can then quit the server (this script) by pressing CTRL-C.
    ----------------------------------------------------------------
    INFO_STR
    my $app = OAuth::Cmdline::Mojo->new(
        oauth => $oauth,
    );
    $app->start( 'daemon', '-l', $oauth->local_uri );
    # After you filled out the consent screens, you need to press CTRL-C in the shell
    #  window to exit this script.
    say "Server finished.";
    say "Done.";
}


sub read_credentials {
    my $fn = 'credentials.json';
    open ( my $fh, '<', $fn ) or die "Could not open file '$fn': $!";
    my $str = do { local $/; <$fh> };
    close $fh;
    my $json = JSON->new();
    my $hash = $json->decode( $str );
    die "$fn: Expected installed app" if !(exists $hash->{installed});
    return $hash->{installed};
}
```
If you run this script (after installing [OAuth::Cmdline](https://metacpan.org/pod/OAuth::Cmdline)), the access token should be saved in a file `.google-drive.yml` in the current directory.

## Uploading a file to Google Drive

Finally, we have the access token and we can start to use the module [Net::Google::Drive::Simple](https://metacpan.org/pod/Net::Google::Drive::Simple). We will start by uploading a file to Google Drive. Unfortunately, when I tested the [`file_upload( $file, $dir_id )`](https://metacpan.org/pod/Net::Google::Drive::Simple#METHODS) method, it did not work as expected, see [issue # 47](https://github.com/mschilli/net-google-drive-simple/issues/47). I created [a pull request](https://github.com/mschilli/net-google-drive-simple/pull/48) to fix the issue. Since the pull request has not yet been merged I suggest that you (if you want to upload files) install the module (`Net::Google::Drive::Simple`) like this:

```bash
git clone https://github.com/mschilli/net-google-drive-simple.git
cd net-google-drive-simple
git fetch origin pull/48/head:pr_48
git checkout pr_48
cpanm .
```

Now, create a dummy `hello.txt` file that we will upload to the root directory of Google Drive:

```bash
$ echo "Hello world!" > hello.txt 
```
Then save the following script as `upload.pl` (in the same directory as you have the access token file):

```perl
use feature qw(say);
use strict;
use warnings;
use Net::Google::Drive::Simple;
use OAuth::Cmdline::GoogleDrive;

my $token_dir = '.';
my $oauth = OAuth::Cmdline::GoogleDrive->new(
    scope         => "https://www.googleapis.com/auth/drive.file",
    homedir       => $token_dir
);
my $gd = Net::Google::Drive::Simple->new( oauth => $oauth );
my $file = 'hello.txt';
my $parent = 'root';
my $file_id = $gd->file_upload( $file, $parent ) or die "Upload failed: $!";
say "Uploaded file: $file (id: $file_id) to directory with id: $parent";
```
Then run the script: 
```bash
$ perl upload.pl
Uploaded file: hello.txt (id: 1x5pc9_rvAFeLgkp0zLUeuKAIMWptplz9) to directory with id: root
```
to upload the `hello.txt` file to your Google Drive!

## Downloading a file from Google Drive

It is not enough to simply specify the directory and filename to download a file. There can be multiple files with the same name in the same directory, however a file's ID is unique. When we uploaded the `hello.txt` the API returned the ID of the uploaded file, namely `1x5pc9_rvAFeLgkp0zLUeuKAIMWptplz9`. Another way to determine the ID is to right click a file in the Google Drive UI and select "Get link" from the drop down menu. If you know that the file name is unique, a third way is to use the [`search()`](https://metacpan.org/pod/Net::Google::Drive::Simple#METHODS) method. I first show how to download a file when you know the ID. The following script can download a file that the app created (if you want to download any file, you should replace the `drive.file` scope with the `drive.readonly` scope) :

```perl
use feature qw(say);
use strict;
use warnings;
use Net::Google::Drive::Simple;
use OAuth::Cmdline::GoogleDrive;

my $token_dir = '.';
my $oauth = OAuth::Cmdline::GoogleDrive->new(
    scope         => "https://www.googleapis.com/auth/drive.file",
    homedir       => $token_dir
);
my $gd = Net::Google::Drive::Simple->new( oauth => $oauth );
my $file_id = '1x5pc9_rvAFeLgkp0zLUeuKAIMWptplz9';
my $url = download_url($file_id);
say "Downloading from: $url";
my $save_fn = 'hello2.txt';
if ($gd->download($url, $save_fn)) {
    say "Downloaded file with id '$file_id' as '$save_fn'";
}
else {
    die "Failed to download $file_id: ", $gd->error();
}

sub download_url {
        'https://www.googleapis.com/drive/v2/files/'
      . $_[0]
      . '?alt=media&source=downloadUrl';
}

``` 

Alternatively, if you know that the file name is unique across the whole Google Drive and you don't want to look in the UI for the file ID, you can use the `search()` method like this:

```perl
use feature qw(say);
use strict;
use warnings;
use Net::Google::Drive::Simple;
use OAuth::Cmdline::GoogleDrive;

my $token_dir = '.';
my $oauth = OAuth::Cmdline::GoogleDrive->new(
    scope         =>  ( "https://www.googleapis.com/auth/drive.readonly" . ' '
                     . "https://www.googleapis.com/auth/drive.file"),
    homedir       => $token_dir
);
my $gd = Net::Google::Drive::Simple->new( oauth => $oauth );
my $fn = 'hello.txt';
my $children= $gd->search(
    { maxResults => 2 },
    { page => 1 },
    "title = '$fn'"
);
die "Multiple files matching '$fn' found." if @$children > 1;
die "No files matching '$fn' found." if @$children == 0;

my $child = $children->[0];
my $save_fn = 'hello2.txt';
if ($gd->download($child, $save_fn)) {
    say "Downloaded file with name '$fn' as '$save_fn'";
}
else {
    die "Failed to download $fn: ", $gd->error();
}
```