[Flutter] Google Driveによるバックアップ実装時のメモ

開発中のFlutterアプリでGoogle Driveを使ったバックアップを検討した際のメモ。
Google Driveによるバックアップの実装方法についてはここでは説明しません。
下記がとても参考になりました。

https://qiita.com/YuKiO-OO/items/67b471e6be6c4c4c26e9

# 複数のファイルを１つのファイルにアーカイブする
## 背景
Google Drive APIには、ユーザーごとの同時リクエスト制限があるようで、下記のように同時に複数のリクエストを投げると、`403`エラーが返ってきてしまう。（正確な制限はわからなかったが、3~10程度で起こった。）

```dart
    final List<Future<void>> futures = <Future<void>>[];

    // Google Driveのアプリ専用領域に保存されているファイルをすべてダウンロードする
    for (int i = 0; i < fileList.files.length; i++) {
      if (fileList.files[i].name.endsWith('.lock')) {
        continue;
      }
      final Future<File> f = download(
        drive,
        fileList.files[i].id,
        fileList.files[i].name,
      );
      futures.add(f);
    }

    // 403エラーが発生
    //   User Rate Limit Exceeded. Rate of requests for user exceed configured project quota.
    await Future.wait<void>(futures);
```
https://developers.google.com/drive/api/v3/handle-errors#resolve_a_403_error_user_rate_limit_exceeded

## アーカイブ方法
下記のパッケージを利用します。

https://pub.dev/packages/archive

```dart
  File get _archive {
    // Zip形式でアーカイブ・圧縮する
    final ZipFileEncoder encoder = ZipFileEncoder();

    // アーカイブ対象のファイル群
    final List<File> _files = files;

    // 新しく作るアーカイブファイルのパス(path_providerパッケージなどで取得)
    final String path = '${tmpDocsDir.path}/archive.tar';

    // アーカイブファイルを作成
    encoder.create(path);

    // 作成したアーカイブファイルに、アーカイブ対象のファイルを放り込んでいく
    for (int i = 0; i < _files.length; i++) {
      encoder.addFile(_files[i], basename(_files[i].path));
    }
    
    // 閉じる
    encoder.close();

    return File(path);
  }
```
展開は以下のように行う(コードはTarDecodeになっている)

```dart
  void _extract(File archive) {
    final Uint8List bytes = archive.readAsBytesSync();
    final Archive tarFile = TarDecoder().decodeBytes(bytes);

    for (final ArchiveFile file in tarFile.files) {
      final String filename = file.name;

      if (file.isFile) {
        final List<int> data = file.content as List<int>;
        File('${appDocsDir.path}/$filename') // アプリ専用の領域に展開
          ..createSync(recursive: true)
          ..writeAsBytesSync(data);
      }
    }
  }
```

> `archive`パッケージのIssueに、容量が大きい&低スペックの端末で`decodeBytes`を実行すると`Out of memory`となってしまう問題があることが報告されているため、大きいファイルを扱う際は注意が必要

# Google Drive APIのダウンロードがとても遅い(と思っていた)
ダウンロードが遅いわけではなく、検証端末(iPhone 6s)の性能の問題だったようです。
## 背景
動作確認も込めて77MBのDBファイルをアップロード・ダウンロードを試していたところ、ダウンロードがいつまでたっても終わらなかった。おっそ！！と思っていたが、シミュレーターやiPhone11Proで試したところ、すんなりダウンロード完了。
そのときのコードがこちら。

```dart
    if (media is google_api.Media) {

      final List<int> bytes = <int>[];
      await media.stream.listen((List<int> data) {
        bytes.insertAll(bytes.length, data);
      }).asFuture<void>();

      final String path = '${appDocsDir.path}/$name';
      final File saveFile = File(path);
      saveFile.writeAsBytesSync(
        bytes,
        mode: FileMode.write,
      );
    }
```
## 原因分析
なんとなくこの時点でざっくりと性能問題かなあと思っていたが、詳しく調べるために下記のようなコードに修正。
すると、`82% : 64MB`の時点から全く処理が進まなくなってしまっていることが判明。(64という数字が怪しさ満載)

```dart
    if (media is google_api.Media) {
      final List<int> bytes = <int>[];  // <- 数十MBのデータが入る

      int count = 0;
      await media.stream.listen((List<int> data) {
        bytes.insertAll(bytes.length, data);
        count++;
        if (count % 100 == 0) {
          final double p = bytes.length / size * 100;
          final double mb = bytes.length / (1024 * 1024);
          print('${p.toStringAsFixed(4)} % : ${mb.toStringAsFixed(2)} MB');
        }
      }).asFuture<void>();

```
よく考えれば、数十MBの変数を保持しているのがいけない気がしてきたので、変数を介さずに直接Fileに書き込んでいく方式に修正。

```dart
    if (media is google_api.Media) {
      final String path = '${appDocsDir.path}/$name';
      final File saveFile = File(path);

      int count = 0;
      int downloaded = 0;
      final IOSink sink = saveFile.openWrite(mode: FileMode.append);

      await media.stream.listen((List<int> data) {
        sink.add(data);
        count++;
        downloaded += data.length;

        if (count % 100 == 0) {
          final double p = downloaded / size * 100;
          final double mb = downloaded / (1024 * 1024);
          print('${p.toStringAsFixed(4)} % : ${mb.toStringAsFixed(2)} MB');
        }
      }).asFuture<void>();

      await sink.flush();
      await sink.close();
```
これで試したところ、iPhone6sでもすんなり(20秒くらいで)77MBのファイルをダウンロードすることができた。