FlutterでGoogle Driveにファイルをアップロードして参照する ※2023/12/15更新

# はじめに

ちょっとGoogleドライブにファイルをアップロードして参照したくなりました。
昔、Javaでやったときに面倒だった記憶がありますが、Flutterでも面倒そうなので、ちゃんとやったことを残しておきたいので記事にします。

バージョン関係は以下の通り

```console
> Flutter --version
Flutter 3.10.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 796c8ef792 (6 weeks ago) • 2023-06-13 15:51:02 -0700
Engine • revision 45f6e00911
Tools • Dart 3.0.5 • DevTools 2.23.1
```

現時点ではAndroidのみの話になります。
また、お試し実装なのでエラーチェック等かなり甘いのでご注意を。

## 更新情報

2023/12/15 共有設定を追加しました。
2023/10/14 フォルダの作成サンプルを追加しました。
2023/10/02 DriveApi.files.list()メソッドを使ったファイルやフォルダの検索サンプルを追加しました。

# ゴール

ログインして、Google Driveにファイルをアップロードしてそのあとダウンロードする。


# 準備

やることがいくつかあります。
ドキュメントにすると多いように思えますが、複雑なことはありません。

## Flutterプロジェクトの作成

Android StudioでFlutterのプロジェクトを作成しておきます。

## Google Cloud

Google Cloud に新しいプロジェクトを作成する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/934f3beb-4fee-fd38-b678-5f236d3149e2.png)

作成後、プロジェクトを選択し、Drive APIを検索して選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/f5fb6d9a-fa28-5d79-4c02-3c1320b12711.png)

有効にするをクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/12e2a51d-c09c-2b40-78d9-1fce393b6343.png)

OAuth同意画面をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/425bb3b2-ef4a-c173-db3d-4ae4b4bbe56c.png)

User Typeを選択します。
今回は外部を選択して作成ボタンをクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/4cfa360c-41a3-9973-c7e1-3ca6d3eae790.png)

次の画面でアプリ情報を入力して、保存して次へボタンをクリック。
次にスコープの設定。
ここで必要なGoogle Drive APIを追加して更新。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/ecb7862b-8b9f-0bdb-a8ab-2c2144a7ecb0.png)

設定ができたら保存して次へをクリック
次はテストユーザーの設定。
必要に応じてユーザーを追加して、保存して次へをクリック。

概要が表示されるので確認します。
この時点で設定が終わっています。


## Firebase

次にFirebaseプロジェクトを作成します。
[google_sign_in](https://pub.dev/packages/google_sign_in)を使用する場合にFirebase Authenticationを利用するので必要になります。

Firebase Consoleで、プロジェクトを追加をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/d6883a4e-6f8c-1879-0b06-6fa6dd44d9e4.png)

プロジェクト名を入力します にフォーカスを当てると、Google Cloudで作成したプロジェクト名がドロップダウンで表示されるので選択して、続行ボタンをクリック。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/23d175c2-154b-a12b-f517-1b685639db9d.png)


注意事項が表示されるので確認して続行ボタンをクリック。

Googleアナリティクスの設定が出ます。
お好みの設定をして続行ボタンをクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/b60161bb-f1b0-0149-48da-ac08737e1bc3.png)

アナリティクスの設定を有効にした場合、アナリティクスの構成の設定が表示されます。
適当なアカウントを選択(何もしてなければDefault Account for Firebase)してFirebaseを追加ボタンをクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/9d8468a2-b05a-0afb-068e-790b12571a35.png)

少し待つとFirebaseプロジェクトが作成されます。

## Authenticationの設定

Firebaseプロジェクトを作成すると、プロジェクトのトップ画面に遷移します。
そこで、Authenticationをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/7e7687c9-52c4-9703-330a-a89995f07f47.png)

Authenticationの画面に遷移したら 始めるボタンをクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/d0f45c79-44f8-fef8-782d-3c6dc1ab3243.png)

ログイン方法の設定画面に映るので、追加のプロバイダでGoogleをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/36a32fbd-97d3-a73e-453c-a79eb027bdcd.png)

Googleのログインプロバイダの設定画面になるので、有効にするをクリックし、保存ボタンをクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/f5c3d9e3-27c5-301d-6354-c77c40264231.png)

保存が完了すると、ログインプロバイダ一覧の表示に遷移します。

## フィンガープリントの登録

次にプロジェクトの設定でSHA1フィンガープリントを追加します。
これを追加しないと、Androidでログイン処理がエラーになります。
公式ドキュメントの[クライアントの認証](https://developers.google.com/android/guides/client-auth?hl=ja)の手順に従って実施します。

keytoolを使って署名を作成します。
今回は以下のように実施しました。


```dosbatch:コマンドプロンプト
keytool -list -v -alias androiddebugkey -keystore %USERPROFILE%\.android\debug.keystore
```

```powershell:PowerShell
keytool -list -v -alias androiddebugkey -keystore $env:USERPROFILE\.android\debug.keystore
```

今回はAndroidStudioのTerminalで実行しました。

```powershell
> keytool -list -v -alias androiddebugkey -keystore $env:USERPROFILE\.android\debug.keystore
キーストアのパスワードを入力してください:  
別名: androiddebugkey
作成日: 2022/01/30
エントリ・タイプ: PrivateKeyEntry
証明書チェーンの長さ: 1
証明書[1]:
所有者: C=US, O=Android, CN=Android Debug
発行者: C=US, O=Android, CN=Android Debug
シリアル番号: 1
有効期間の開始日: Sun Jan 30 00:38:45 JST 2022終了日: Tue Jan 23 00:38:45 JST 2052
証明書のフィンガプリント:
         SHA1: DB:62:CF:E0:18:2F:07:7B:20:BC:8C:1D:17:A8:97:D0:0A:74:15:BB
         SHA256: 56:E6:F9:F5:36:AC:06:50:68:CC:F5:B1:5C:FF:C8:B2:60:57:4F:72:65:14:27:97:9D:18:0A:AB:28:B1:14:1F
署名アルゴリズム名: SHA1withRSA (弱)
サブジェクト公開キー・アルゴリズム: 2048ビットRSAキー
バージョン: 1

Warning:
証明書はSHA1withRSA署名アルゴリズムを使用しており、これはセキュリティ・リスクとみなされます。このアルゴリズムは将来の更新で無効化されます。
```

ここで出力されたSHA1をFirebaseのプロジェクト設定で登録します。
プロジェクト設定は左上の歯車アイコンで表示できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/dd21aed8-4bdb-9e0e-cd9a-6e63166da6b5.png)

プロジェクトの設定を開いたら、マイアプリでAndroidのアイコンをクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/c73baf32-b5ed-8e45-bcd0-9e6f5834ba60.png)

アプリの登録画面になるので、パッケージ名を入力し、デバッグ用の署名証明書SHA-1に先ほどのフィンガープリントを入力します。
入力が完了したら、アプリを登録ボタンをクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/9e491b5f-7423-fb76-bd44-7a35fbb07a76.png)

次の構成ファイルをダウンロードして追加はFirebase CLIを使うので、特に何もせず次へ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/dc413d88-ef07-47bc-d4df-2d65772966bf.png)

Firebase SDKの追加も特に何もすることはないので次へ

最後もそのままコンソールに進むボタンをクリックして終了。

Android StudioのTerminalに移り、flutterfire configureを実行。
ここでは、作成したプロジェクトを選択して進めていきます。

```powershell
> flutterfire configure
i Found 3 Firebase projects.
✔ Select a Firebase project to configure your Flutter application with · drivesample-393913 (DriveSample)
✔ Which platforms should your configuration support (use arrow keys & space to select)? · android, ios
i Firebase android app com.example.drivetest registered.
i Firebase ios app com.example.drivetest is not registered on Firebase project drivesample-393913.
i Registered a new Firebase ios app on Firebase project drivesample-393913.

Firebase configuration file lib\firebase_options.dart generated successfully with the following Firebase apps:

Platform  Firebase App Id
android   1:xxxxxxxxxxxx:android:xxxxxxxxxxxxxxxxxxxxxx
ios       1:xxxxxxxxxxxx:ios:xxxxxxxxxxxxxxxxxxxxxx

Learn more about using this file and next steps from the documentation:
 > https://firebase.google.com/docs/flutter/setup

```

## Flutter側の準備

必要なパッケージを追加していきます。

追加するものは以下になります。

- [googleapis](https://pub.dev/packages/googleapis)
- [google_sign_in](https://pub.dev/packages/google_sign_in)
- [firebase_core](https://pub.dev/packages/firebase_core)
- [extension_google_sign_in_as_googleapis_auth](https://pub.dev/packages/extension_google_sign_in_as_googleapis_auth)

以下のようなコマンドで追加してください

```
flutter pub add googleapis google_sign_in firebase_core extension_google_sign_in_as_googleapis_auth
```

ここで念のため、エラーがないかAndroid StudioのDart Analysisで確認しておくとよいです。
これで準備完了です。


# ログイン・ログアウト


```Dart
import 'dart:developer' as developer;
import 'dart:io' as io;
import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:googleapis/drive/v3.dart';
import 'package:http/src/client.dart';
import 'package:http/http.dart';
import 'package:path_provider/path_provider.dart';

// ・・・省略・・・

  final _googleSignIn = GoogleSignIn(
    scopes: [
      DriveApi.driveScope
    ]
  );

  GoogleSignInAccount? _account;
  Client? _client;
  DriveApi? _drive;

// ・・・省略・・・

  void _signIn() async {
    _account = await _googleSignIn.signIn();
    _client = await _googleSignIn.authenticatedClient();
    _drive = DriveApi(_client!);
    _googleSignIn.isSignedIn().then((value) => setState(() {
      _isSignIn = value;
    }));
  }

  void _signOut()  {
    _googleSignIn.disconnect().then((value) {
      _googleSignIn.isSignedIn().then((value) => setState(() {
        _isSignIn = value;
      }));
    });
  }

```

scopesは目的に適したものを使用するのが良いのですが、今回は何でもできてしまう DriveApi.driveScope を指定。

ログインに関しては、最初にグーグルにサインインして、そのあとドライブの権限許可を取得する感じ。
本当は許可しない場合の処理もいるとは思うけどここでは割愛。

ログアウトはグーグルからサインアウトしています。

ログイン、ログアウトは結構シンプルだと思います。

# ドライブにアクセス

ちょっと長いのでアップロード、削除、ダウンロードで分けていきます。
今回はマイドライブ直下にアップロードしています。

## アップロード

```dart
  void _upload() async {
    developer.log("file");
    const fileName = "DriveSampleUploadTest.txt";
    var tmpDir = await getTemporaryDirectory();
    var txtFile = io.File("${tmpDir.path}/test.txt");
    txtFile.writeAsStringSync("アップロードのテストです");

    developer.log("check");
    var list = (await _drive?.files.list())?.files;
    String id = "";
    list?.forEach((element) {
      if(element.name == fileName){
        id = element.id ?? "";
        developer.log("${element.name} match $id");
        //_drive?.files.delete(id!).then((value) => developer.log("delete success"));
      }
    });

    var file = File(
      name : fileName,
      modifiedTime: DateTime.now().toUtc(),
      //parents: [],    // 指定しないとマイドライブがアップロード先になる
    );
    var media = Media(txtFile.openRead(), txtFile.lengthSync());

    // 同じファイル名でcreateを呼び出すと、ID違いの同じ名前のファイルがアップロードされるので注意
    if("" == id){
      _drive?.files.create( file,
          uploadMedia: media
      )
      .then((file) => developer.log("create success"))
      .catchError((error) => developer.log("create error : $error", error : error));

    }else{
      _drive?.files.update( file, id,
          uploadMedia: media
      )
      .then((file){
        file.parents?.forEach((element) {
          developer.log("element : ${element.characters.string}");
        });
        developer.log("upload success");
      })
      .catchError((error) => developer.log("upload error : $error", error : error));
    }
  }
```

同じファイルをcreateメソッドで何度も作るとID違いの同じファイルが複数アップロードされるので、list()メソッドを使って、同名のファイルの確認をしています。
同じ名前のファイルがある場合は、IDを取得しておいて、upload()メソッドでファイルを更新します。
同じ名前のファイルがない場合はcreate()メソッドでドライブにファイルを作成します。

## 削除

```dart
  void _delete() async {
    developer.log("file");
    const fileName = "DriveSampleUploadTest.txt";

    developer.log("delete");
    var list = (await _drive?.files.list())?.files;
    String id = "";
    list?.forEach((element) {
      if(element.name == fileName){
        id = element.id ?? "";
        developer.log("${element.name} match $id");
        if("" != id){
          _drive?.files.delete(id).then((value) => developer.log("delete success"));
        }
      }
    });
  }
```

ファイル名を検索して、同じファイル名のファイルがあれば削除するようにしています。
削除はいたってシンプルですね。

## ダウンロード

```dart
  void _download() async{
    developer.log("file");
    const fileName = "DriveSampleUploadTest.txt";

    developer.log("check");
    var list = (await _drive?.files.list())?.files;
    String id = "";
    String? mimeType;
    list?.forEach((element) {
      if(element.name == fileName){
        id = element.id ?? "";
        mimeType = element.mimeType;
        developer.log("${element.name} match $id mime $mimeType");
      }
    });

    if("" == id) {
      developer.log("file not found");
      return;
    }
    _drive?.files.get( id,
      downloadOptions: DownloadOptions.fullMedia
    )
    .then((value) {
      developer.log("download success");
      if(value is File){
        developer.log("result is File");
      }else if(value is Media) {
        developer.log("result is Media ${value.contentType.toString()}");
        var stream = value.stream;
        stream.forEach((element) {
          ByteStream.fromBytes(element).bytesToString().then((value){
            setState(() {
              resultData = "$value\r\n";
            });
          });
        });
      }
    })
    .catchError((error) => developer.log("export error : $error", error : error));
  }
```

同じファイル名のファイルがある場合にダウンロードします。
DownloadOptionsの指定がポイントになります。
DownloadOptions.metadata を指定すると、ファイルの情報をダウンロードできます。
返却される値は、drive.v3.Fileオブジェクトになります。

DownloadOptions.fullMedia の場合、ファイルそのものをダウンロードできます。
返却される値は、Mediaオブジェクトになります。
ダウンロードされたファイルはcontentTypeを確認し、処理を行うのが良いと思います。

## DriveApi.files.list()メソッド
[googleapis packagedocumentationdrive_v3FilesResourcelist method](https://pub.dev/documentation/googleapis/latest/drive_v3/FilesResource/list.html) に説明がありますが、英語苦手なのでChatGPTなどを使って翻訳。
理解しないと、ファイルを探したりするのに困ってしまいます。

```dart
Future<FileList> list({
  String? corpora,
  String? corpus,
  String? driveId,
  bool? includeItemsFromAllDrives,
  String? includeLabels,
  String? includePermissionsForView,
  bool? includeTeamDriveItems,
  String? orderBy,
  int? pageSize,
  String? pageToken,
  String? q,
  String? spaces,
  bool? supportsAllDrives,
  bool? supportsTeamDrives,
  String? teamDriveId,
  String? $fields
})
```

|引数|説明|
|--|--|
|corpora|ファイルを検索するコーパスの種類を指定します。指定可能な値は'user'(デフォルト)、'domain'、'drive'、'allDrives'。|
|corpus|非推奨。'corpora' を使用してください。|
|driveId|ファイルを検索するドライブの ID を指定します。|
|includeItemsFromAllDrives|すべてのドライブからアイテムを取得するかどうかを指定します。|
|includeLabels|レスポンスのlabelInfo部分に含めるラベルのIDのカンマ区切りリスト。|
|includePermissionsForView|レスポンスに含める追加のビューの権限を指定します。サポートされているのは 'published' のみです。|
|includeTeamDriveItems|非推奨: 'includeItemsFromAllDrives' を使用してください。|
|orderBy|コンマ区切りのソートキーのリスト。有効なキーは 'createdTime'、'folder'、'modifiedByMeTime'、'modifiedTime'、'name'、'name_natural'、'quotaBytesUsed'、'recency'、'sharedWithMeTime'、'starred'、および 'viewedByMeTime' です。各キーはデフォルトで昇順に並べ替えられますが、'desc' 修飾子を使用して逆順にすることもできます。例: ?orderBy=folder,modifiedTime desc,name|
|pageSize|1ページあたりに返すファイルの最大数。ファイルリストの最後に到達する前でも、一部または空の結果ページが発生する可能性があります。値は "1" から "1000" の間である必要があります。|
|pageToken|前のリストリクエストを次のページで継続するためのトークン。これは前のレスポンスからの 'nextPageToken' の値に設定する必要があります。|
|q|ファイルの結果をフィルタリングするためのクエリ。サポートされている構文については、「[ファイルとフォルダを検索する](https://developers.google.com/drive/api/guides/search-files?hl=ja)」ガイドを参照してください。|
|spaces|corpora内でクエリを実行するスペースのカンマ区切りリスト。サポートされている値は 'drive' と 'appDataFolder' です。|
|supportsAllDrives|リクエストを行うアプリケーションが、マイドライブと共有ドライブの両方をサポートしているかどうかを示します。|
|supportsTeamDrives|非推奨: 'supportsAllDrives' を使用してください。|
|teamDriveId|非推奨: 'driveId' を使用してください。|
|$fields|応答に含めるフィールドを指定します。|

注意すべきは
- 一度に取得できる結果の量は上限がある
- qパラメタを理解しないと検索がままならない
- 非推奨なパラメタがいくつかある
- すべてのパラメタを指定する必要はない
    - なにも指定しないと、すべてのファイルとフォルダが返却されるとのこと。

### 例：フォルダの一覧取得
```dart
  void _searchFolder({
      String? id,
      String? folderName,
    }){
    var query = "mimeType = 'application/vnd.google-apps.folder'";
    if((folderName ?? "").isNotEmpty){
      query = "$query and name = '$folderName'";
    }
    developer.log("query = $query");

    _drive?.files.list(
        spaces: "drive",
        q:query
    ).then((value){
      value?.files?.forEach((element) {
        if((id ?? "").isEmpty || ((id ?? "").isNotEmpty && id == element.id)){
          developer.log("ID = ${element.id} / Name = ${element.name}");
        }
      });
    });
  }
```
実際に利用する場合は、ドライブ上に一度に取れる数を超えるファイル/フォルダがある場合の対処問う必要になります。


### 例：ファイルの一覧取得
```dart
  void _searchFile({
      String? id,
      String? fileName,
    }){
    var query = "mimeType != 'application/vnd.google-apps.folder'";
    if((fileName ?? "").isNotEmpty){
      query = "$query and name = '$fileName'";
    }
    developer.log("query = $query");

    _drive?.files.list(
        spaces: "drive",
        q: query,
    ).then((value){
        _resultTextController.text = "";
        value?.files?.forEach((element) {
          if((id ?? "").isEmpty || ((id ?? "").isNotEmpty && id == element.id)){
            developer.log("ID = ${element.id} / Name = ${element.name}");
          }
        });
    });
  }
```
実際に利用する場合は、ドライブ上に一度に取れる数を超えるファイル/フォルダがある場合の対処問う必要になります。


### 例：フォルダの作成
```dart
  void _createFolder(String folderName){
    var folder = File(
        mimeType: 'application/vnd.google-apps.folder',
        name: folderName
    );
    _drive?.files.create(folder)
   .then((value) {
      setState(() {
        _resultTextController.text = "id = ${value.id}";
      });
    });
  }
```
実際に使う場合は、フォルダの存在チェックも入れないといけない。

## 共有設定

URLを知っている人すべてに共有したい場合の例です。

```dart
        _driveApi?.permissions.create(
            Permission.fromJson({
              'role':'reader',
              'type':'anyone'
            }),
            fileId
        );

```

Permission.fromJsonで指定する内容は [REST Resource: permissions](https://developers.google.com/drive/api/reference/rest/v3/permissions?hl=ja#Permission) を確認すると参考になります。


# 最後に
Google ドライブを扱うのには事前準備がいくつかありちょっと面倒ですが、やることさえわかっていればさほど難しいことはない、という印象です。
ただ、実際に利用する場合はセキュリティ等をちゃんと考えれう必要はあるのかな？と思います。

iOSは確認してませんが、iOSでも動かす予定なので、その際にはドキュメントを更新しようと思います。
あと、フォルダを作成してアップロード、ダウンロードも実装したいので、そのあたりも更新できたらなと思います。


# 参考

- [Google APIs](https://docs.flutter.dev/data-and-backend/google-apis)
- [REST Resource: files](https://developers.google.com/drive/api/reference/rest/v3/files?hl=ja)
- [Method: files.create](https://developers.google.com/drive/api/reference/rest/v3/files/create?hl=ja)
- [Method: files.export](https://developers.google.com/drive/api/reference/rest/v3/files/export?hl=ja)
- [googleapis package documentation drive_v3 > FilesResource > list method](https://pub.dev/documentation/googleapis/latest/drive_v3/FilesResource/list.html)
- [Google Workspace > Google Drive > ガイド ファイルやフォルダを検索する](https://developers.google.com/drive/api/guides/search-files?hl=ja)
- [Google Workspace と Google ドライブでサポートされている MIME タイプ](https://developers.google.com/drive/api/guides/mime-types?hl=ja)
- [Google Workspace > Google Drive > 参照 Google Workspace ドキュメントの MIME タイプをエクスポートする](https://developers.google.com/drive/api/guides/ref-export-formats?hl=ja)
- [Google Workspace > Google Drive > 参照 検索クエリ語句と演算子](https://developers.google.com/drive/api/guides/ref-search-terms?hl=ja)
- [REST Resource: permissions](https://developers.google.com/drive/api/reference/rest/v3/permissions?hl=ja#Permission)