Googleドライブに用意したドキュメントのテンプレートをPyDrive2で複製する

## はじめに

この記事では、PythonでGoogleドライブを操作するパッケージの中からPyDrive2（[GitHub](https://github.com/iterative/PyDrive2)）を取り上げます。
PyDrive2からDrive APIを介してGoogleドライブを操作し、指定したドキュメントを複製する方法を備忘録としてまとめます。

## PyDrive2について

PyDrive2はPyDriveのメンテナンスされたフォークです。
PyDriveの更新は1年以上前から止まっており、2020年1月にPyDrive2がリリースされています。

もう少し詳しい説明や、PyDrive2を使う際の認証まわりの設定については以下をご確認ください。
[PythonでGoogleドライブを操作するパッケージPyDrive2の認証まわり](https://qiita.com/ftnext/items/60ced8bc432bec6101f0)

なお、PyDriveやPyDrive2は、Drive API **v2**にアクセスします。
Drive APIにはv3もあるため、ご注意ください。

## 動作環境

```console
$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G3020
$ python -V
Python 3.8.1
$ pip list | grep PyDrive2
PyDrive2                 1.4.10
```

[PythonでGoogleドライブを操作するパッケージPyDrive2の認証まわり](https://qiita.com/ftnext/items/60ced8bc432bec6101f0)に沿って、ファイルを配置しています。

```
.
├── my_client_secrets.json
├── saved_credentials.json
└── settings.yaml
```

## やりたいこと

Googleドキュメントの**議事録テンプレートの複製**をPythonスクリプトにやらせます。

### 背景

MTGの際にGoogleドキュメントに共同で議事録をとっています。

<img width="1200" alt="テンプレートドキュメント例.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82132/b17fb7c1-0c94-4665-28a1-a74d4068d25b.png">

MTGのたびに議事録テンプレートを複製するのが退屈に感じ、自動化を考えるようになりました。
そこで、自動化の一歩目として、**GoogleドキュメントのIDを指定したら複製する**スクリプトを作ってみました。

### GoogleドキュメントのIDとは

GoogleドキュメントのURLは以下のようになっています。

```
https://docs.google.com/document/d/1******************************************o/edit#
```

このうちの `1******************************************o` の部分がIDです（マスクする意味でアスタリスクを使っています）。

## 指定したドキュメントを複製するPythonスクリプト

```python:copy_template.py
import argparse

from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive


def main():
    # コマンドラインから渡された引数の解析 (1)
    parser = argparse.ArgumentParser()
    parser.add_argument("source_id")
    args = parser.parse_args()
    source_id = args.source_id

    # Googleドライブを操作するためのオブジェクトを取得 (2)
    gauth = GoogleAuth()
    gauth.LocalWebserverAuth()
    drive = GoogleDrive(gauth)

    # コピー元のファイルの名前に copied_ をつけたファイル名とする (3)
    source = drive.CreateFile({"id": source_id})
    source.FetchMetadata("title")
    dest_title = f"copied_{source['title']}"

    # コピーを作成 (4)
    copied_file = {"title": dest_title}
    f = (
        drive.auth.service.files()
        .copy(fileId=source_id, body=copied_file)
        .execute()
    )

    # 作成したファイルにアクセスするためのURLを表示 (5)
    dest = drive.CreateFile({"id": f["id"]})
    dest.FetchMetadata("alternateLink")
    print(dest["alternateLink"])


if __name__ == "__main__":
    main()
```

### 実行結果

```:ファイル配置
.
├── copy_template.py
├── my_client_secrets.json
├── saved_credentials.json
└── settings.yaml
```

```console:使い方
$ python copy_template.py --help
usage: copy_template.py [-h] source_id

positional arguments:
  source_id

optional arguments:
  -h, --help  show this help message and exit
```

実行すると、複製した結果のドキュメントのURLが表示されます。

```
$ python copy_template.py 1******************************************o
https://docs.google.com/document/d/1******************************************c/edit?usp=drivesdk
```

## コード解説

[PyDriveのドキュメント](https://gsuitedevs.github.io/PyDrive/docs/build/html/index.html)[^1]には指定したドキュメントを複製（コピー）する方法は見つけられませんでした。
そこでPyDriveのIssueを検索し、参考にできそうなIssueを見つけました。
[Create a copy of the file #85 (Comment)](https://github.com/gsuitedevs/PyDrive/issues/85#issuecomment-266235602)
こちらに沿って実装しています。

[^1]: PyDrive2のドキュメントとしてPyDriveのドキュメントが案内されています（2020年5月時点）

### 複製の仕組み（コード (4)）

認証結果を元に、Googleドライブを操作するためのオブジェクト（`gdrive`）を作成します。

複製には、複製元のドキュメントのIDと、複製先のドキュメントのタイトルが必要です。

- 複製元のドキュメントのID：コマンドラインから渡される
- 複製先のタイトル：決め打ちでもOK。今回は複製元のドキュメントのタイトルに`copied_`というprefixをつける

### Googleドライブのファイルを取得する（コード (3)）

`gdrive`オブジェクトからGoogleドライブ上のファイルを操作するには、`CreateFile`メソッドを使います。

```python
source = drive.CreateFile({"id": source_id})
```

`CreateFile`メソッドの返り値は`GoogleDriveFile`インスタンスです。
**IDを指定**して`CreateFile`し、該当のファイルを`GoogleDriveFile`インスタンスを介して操作できるようにします。

### Googleドライブのファイルのメタデータを取得する（コード (3),(5)）

Googleドライブのファイルには様々な情報（メタデータ）がありますが、上記の方法ではIDしか取得できていません。
取得したい情報がある場合は、

1. `GoogleDriveFile`の`FetchMetadata`メソッドで取得したい属性を指定
  - 例：`source.FetchMetadata("title")`
2. 取得したい属性名をキーにしてアクセス
  - 例：`source['title']`

という手順になります。

1で取得したい属性を指定する場合は、以下のリファレンスに沿って指定します。
https://developers.google.com/drive/api/v2/reference/files

- title: The title of this file.
- alternateLink: A link for opening the file in a relevant Google editor or viewer.

### Googleドライブのファイルをコピーする（コード (4)）

`GoogleDrive`や`GoogleDriveFile`にはファイルのコピーを作成するメソッドがないので、PyDrive2がラップしている`google-api-python-client`から、`Files: copy`エンドポイントを直接呼び出しているようです。

## 共有ドライブだと動かない？

共有ドライブにあるドキュメントのIDを指定したい場合は、コピーするコード (4) の変更が必要です。
`supportsAllDrives`引数に`True`を渡します。

```python:copy_template.py
    # コピーを作成 (4)
    copied_file = {"title": dest_title}
    f = (
        drive.auth.service.files()
        .copy(fileId=source_id, body=copied_file, supportsAllDrives=True)
        .execute()
    )
```

https://developers.google.com/drive/api/v2/enable-shareddrives によると、

>supportsAllDrives — Whether the requesting application supports both My Drives and shared drives. If false, then shared drive items are not included in the response.

なので、`True`と指定することで共有ドライブのファイルも操作できます。
この引数を指定しない場合は、`File not found`とDrive APIから返ってきます。

`supportsAllDrives=True`が指定されていても、マイドライブのファイルはコピーできます。
そのため、常に指定してもいいと思いますが、2020年6月以降は指定不要になるようです。
[`Files: copy`エンドポイントのドキュメント](https://developers.google.com/drive/api/v2/reference/files/copy#parameters)によると、2020年6月1日以降はsupportsAllDrives引数が廃止され、指定しなくても共有ドライブのファイルを操作できる[^4]そうです。

[^4]: 「Deprecated - Whether the requesting application supports both My Drives and shared drives. This parameter will only be effective until June 1, 2020. Afterwards all applications are assumed to support shared drives. (Default: false)」

共有ドライブの操作は、PyDriveにいくつかIssueが見られました[^2]が、PyDrive2では対応済み[^3]のようです。 

[^2]: [Downloading files from Team Drive #160](https://github.com/gsuitedevs/PyDrive/issues/160)

[^3]: [2020年1月のコミット](https://github.com/iterative/PyDrive2/commit/a4d5cc665da75a9a62d9aed29be6fb044ea80774#diff-23a6233764ad24d2243c736fc36f1553)で取り込まれていました

## スコープが絞れるか

[PythonでGoogleドライブを操作するパッケージPyDrive2の認証まわり](https://qiita.com/ftnext/items/60ced8bc432bec6101f0) にて述べたように、広めのスコープ（`https://www.googleapis.com/auth/drive`）を使っています。
ファイルの複製をする上でこれを絞れるか指定を試したのですが、絞れませんでした[^5]。

[^5]: スコープとして`drive.file`や`drive.metadata`やその組合せを試しましたが、[The user has not granted the app {appId} {verb} access to the file {fileId}](https://developers.google.com/drive/api/v2/handle-errors#resolve_a_403_error_the_user_has_not_granted_the_app_appid_verb_access_to_the_file_fileid) というエラーが解消できていません

[`Files: copy`エンドポイントのスコープ](https://developers.google.com/drive/api/v2/reference/files/copy#auth)を参照したところ、`https://www.googleapis.com/auth/drive` からは絞れなさそうとわかりました。

## 終わりに

PyDrive2でGoogleドキュメントのIDを指定して複製する方法についてまとめました。

ドキュメントもスプレッドシートもGoogleドライブから見ればどちらもファイルです。
ですので、この方法でドキュメント以外も複製できると考えています（後日検証予定）。