大容量のデータをGoogleドライブにアップしよう

こんにちは。@yuki_utsumiです。

突然ですが、皆さんは数十テラバイトのデータをクラウドサーバーにアップロードしたことはありますか？
業務でやったので私はあります。

というわけで、私の所属している会社ではオンプレのファイルサーバーの内容（数十TB）をGoogleDriveに移すことになり、その実行部隊の一人に私が選ばれました。その際の経験をここで紹介しようと思います。

## 紹介する内容
* rcloneで大規模のデータをGoogleDriveにアップロードする方法
    * 前提条件（ファイル数、階層数、プランなど）
    * rcloneのインストール方法
    * rcloneの実行方法
    * より効率的なrcloneの回し方
    * エラーとその対策

社内サーバー（NAS）からGoogleDriveの共有ドライブにデータをほぼ丸ごと移す、という内容になっています。
## こんな人におすすめ
* 大規模なファイルサーバーの内容をクラウドサーバーに移す必要がある
* 定期的にたくさんのファイルをクラウドサーバーにアップロードするのが面倒

今回はGoogleDriveでの事例紹介ですが、ツール自体は複数のクラウドサーバーに対応しているため他サービスでも参考になるかもしれません。


#始めに

これは[CYBIRDエンジニア Advent Calendar](https://qiita.com/advent-calendar/2021/cybird) 8日目の記事になります。
7日目は@kenta_kitagawaさんの[「supervisorとLaravelのQueueを使ってみた」](https://qiita.com/kenta_kitagawa/private/c9144fe0e33e4e98d844)でした。Laravel上での作業の効率化するための知見を書いてくれています。このコマンド何回打ったんだろ…ってなることありますよね。Laravelでそういう状況に陥った方は是非見てみてください。


#余談：なんでツール使うの？


GoogleDriveにデータをアップロードする方法としては、例えば以下のような方法があります。

* ブラウザ版GoogleDrive
    * 直接ドラッグ＆ドロップで可能
* パソコン版GoogleDrive
    * インストールすることで、PCにGoogleDriveの内容をマウントして操作できる

普段使いでGoogleDriveを利用する場合は以上の方法がベストだと思われます。
しかし複雑かつ大規模なデータをアップロードする場合、アップロード（同期）に失敗したり、途中でクラッシュしてしまったりします。だから、外部ツールを使う必要があったんですね。


#　前提条件：GoogleDriveの仕様
じゃあツールを使ってデータを送るぞ、となる前にいろいろな確認事項を紹介しておきます。
GoogleDriveにはファイル数や階層数などで固有の制限があります。
そのため、無策にファイルサーバーの内容をGoogleDriveにアップロードしようとすると、ロクなことになりません。実際のアップロードの前にしっかり確認と調整をしておきましょう。

* （そもそも）有料プランかどうか
    * 無料プランではマイドライブに15GBのみ、共有ドライブは使用できない
* ファイル数
    * 共有ドライブごとにファイル、フォルダ合計で40万個まで
* 最大階層数
    * 20まで

参考リンク：[Googleのヘルプ](https://support.google.com/a/answer/7338880?hl=ja)

ファイル数、階層数は制限を超過するとその共有ドライブにはファイルをアップロードできなくなります。
今回の場合、制限にバリバリ引っかかっていることがわかったので「共有ドライブを分ける」ことによって問題を回避しました。部署やプロジェクトなどごとに共有ドライブを作るということですね。
単純計算だと１億個ファイルがある場合、250個の共有ドライブを作成するということになります。大変ですね。
また、クラウドストレージにデータをアップする関係上どうしても時間がかかります。ネットワーク環境などによりますが、データの移行は10TBにつき１ヶ月かかるくらいのスケジュール感で考えておくと危なくないと思います。

# rcloneをインストールする：準備編

今回はrcloneというコマンドラインツールを使用します。[（公式サイト)](https://rclone.org/)
クラウドストレージに対応したファイル管理用のツールで、GoogleDriveのAPIを利用して安定してファイルを転送することができるのが特徴となっています。

まずインストール、セットアップを目指します。

``` :Linux/macOS
curl https://rclone.org/install.sh | sudo bash
```

```:macOS
brew install rclone
```
上記以外にも方法は多くあります。インストールは他Qiita記事でも紹介されているのでそちらも参考に。

インストールしたら、まずは移行先のGoogleDriveに接続できるように設定を行います。

```
rclone config
```
「リモート」の一覧が表示されます。

```
Current remotes:

Name                 Type
====                 ====

example              drive

e) Edit existing remote
n) New remote
d) Delete remote
r) Rename remote
c) Copy remote
s) Set configuration password
q) Quit config
e/n/d/r/c/s/q>
```

（初回は多少表示が異なります）
ここでいう「リモート」は「どのクラウドストレージのどのフォルダに、どの認証情報を使ってアクセスするかの設定」くらいの意味合いで使われています。共有ドライブ1つごとに一つ「リモート」を作成していきます。

新規作成するのでnを入力

```
e/n/d/r/c/s/q> n
```

適当な識別しやすい名前を入力

```
name> test
```

サービスの種類を指定します。2021年12月現在、43種類のサービスに対応しているようです。

```
Type of storage to configure.
Enter a string value. Press Enter for the default ("").
Choose a number from below, or type in your own value
```

今回はGoogleドライブを指定します。」

```
15 / Google Drive
   \ "drive"

Storage> 15
```

クライアントIDを入力します。
ここで、GoogleDriveのAPIを利用する準備をします。

1. Google Cloud Platform にログイン後、適当なプロジェクトを作成する
1. そのプロジェクトでGoogle Drive API を有効にする
1. 認証情報画面で「OAuth クライアント ID」の認証情報を作成する
1. クライアントIDとクライアントシークレットをメモする

クライアントIDを入力しなくても問題ないですが、動作が遅くなります。

```
Google Application Client Id
Setting your own is recommended.
See https://rclone.org/drive/#making-your-own-client-id for how to create your own.
If you leave this blank, it will use an internal key which is low performance.
Enter a string value. Press Enter for the default ("").
client_id> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com
```

クライアントシークレットを入力します。

```
OAuth Client Secret
Leave blank normally.
Enter a string value. Press Enter for the default ("").
client_secret> xxxxxxxxxxxxxxxxxxxxxxxxx
```

権限を設定します。基本的に完全な権限の1を指定します。

```
1 / Full access all files, excluding Application Data Folder.
  \ "drive"
2 / Read-only access to file metadata and file contents.
  \ "drive.readonly"
  / Access to files created by rclone only.
3 | These are visible in the drive website.
  | File authorization is revoked when the user deauthorizes the app.
  \ "drive.file"
  / Allows read and write access to the Application Data folder.
4 | This is not visible in the drive website.
  \ "drive.appfolder"
  / Allows read-only access to file metadata but
5 | does not allow any access to read or download file content.
  \ "drive.metadata.readonly"
scope> 1
```

ルートフォルダの設定：基本的に入力なしでOK

```
Fill in to access "Computers" folders (see docs), or for rclone to use
a non root folder as its starting point.

Enter a string value. Press Enter for the default ("").
root_folder_id>
```

サービスアカウントファイルの設定：基本的に入力なしでOK

```
Service Account Credentials JSON file path
Leave blank normally.
Needed only if you want use SA instead of interactive login.

Leading `~` will be expanded in the file name as will environment variables such
 as `${RCLONE_CONFIG_DIR}`.

Enter a string value. Press Enter for the default ("").
service_account_file>
```

詳しい設定はしないのでNo（空白）

```
Edit advanced config?
y) Yes
n) No (default)
y/n>
```

ブラウザが開ける環境で設定しているならYes（空白）

```
Use auto config?
 * Say Y if not sure
 * Say N if you are working on a remote or headless machine

y) Yes (default)
n) No
y/n>
```

Enterを押すとブラウザが自動が開き、アカウントの認証が求められるので認証します。
認証が成功するとコンソールに戻ります。

チームドライブ（共有ドライブ）向けの設定なのでyと入力する

```
Configure this as a team drive?
y) Yes
n) No
y/n> y
```

認証したアカウントで閲覧できる共有ドライブが表示されるので、そこから共有ドライブを指定します。
コンソール上で割り振られた数字、もしくはID（"xxxxxxxxxxxxxxxxxxx"の部分）のどちらでも指定できます。

```
1 / 共有ドライブ1
   \ "xxxxxxxxxxxxxxxxxxx"
2 / 共有ドライブ2
   \ "xxxxxxxxxxxxxxxxxxx"
3 / 共有ドライブ3
   \ "xxxxxxxxxxxxxxxxxxx"
4 / 共有ドライブ4
   \ "xxxxxxxxxxxxxxxxxxx"
config_team_drive> 1
```

最後に確認が入るので、設定が間違っていなかったらy（空白）


```
--------------------
[test]
type = drive
client_id = xxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent
.com
client_secret = xxxxxxxxxxxxxxxxxxxx
scope = drive
token = {"access_token":"xxxxxxxxxxxxxxxxx","token_type":"Bearer","refresh_token":"xxxxxxxxxxxxxxx","expiry":"xxxxxxxxxxxxxxxxx"}
team_drive = xxxxxxxxxxxxx
root_folder_id =
--------------------
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote
y/e/d>
```



以上で設定完了となります。

## 準備編（RTA版）
<b>この作業ドライブひとつひとつでやるのすごく面倒では…？</b>
ということで、最適化された設定の流れが以下になります。

* 前提：設定が完了したリモートがひとつある
* ここまで数字でリモートなどを指定してきましたが、ここからは名前やIDで直接指定します。設定したリモートの数字が分からなくなってむしろ大変になります。

まず、設定が完了したリモートの内容をコピーします。

```
Name                 Type
====                 ====

example              drive
test                 drive

e) Edit existing remote
n) New remote
d) Delete remote
r) Rename remote
c) Copy remote
s) Set configuration password
q) Quit config
e/n/d/r/c/s/q> c
Choose a number from below, or type in an existing value
 1 > example
 2 > test
remote> test
Enter name for copy of "test" remote.
name> test2
```

コピーしたリモートを設定しなおします。


```
Name                 Type
====                 ====

example              drive
test                 drive
test2                drive

e) Edit existing remote
n) New remote
d) Delete remote
r) Rename remote
c) Copy remote
s) Set configuration password
q) Quit config
e/n/d/r/c/s/q> e
Choose a number from below, or type in an existing value
 1 > example
 2 > test
 3 > test2
remote> test2
```

ここから

* クライアントIDの設定
* クライアントシークレットの設定
* 権限の設定
* ルートフォルダの設定
* サービスアカウントファイルの設定
* 詳しい設定するか否か

が聞かれますが、全部コピー前と一緒なのでEnterを連打します。

ここで質問の内容が変わり、認証トークンを更新するか否かを聞かれます。
一応更新しておくのでこれもEnter。

```
Already have a token - refresh?
y) Yes (default)
n) No
y/n> 
```

つまり、<b>なんかブラウザが強制的に開かれるまではEnterを連打でいいです。</b>
ブラウザが開かれたら認証。

すると、共有ドライブの設定を変えるか聞かれます。
これは変更するのでyを入力します。

```
Change current Shared Drive (Team Drive) ID "xxxxxxxxxxxxxx"?

y) Yes
n) No (default)
y/n> y
```

すると、共有ドライブの一覧が表示されます。
共有ドライブをブラウザ上で閲覧する際のURLが以下だとすると、```folders/```以降がそのドライブのIDとなります。
```https://drive.google.com/drive/folders/xxxxxxxxxxxx```
ID部分（xxxxxxxxxxxxxx)をコピーしてきて貼り付けます。

```
1 / 共有ドライブ1
   \ "xxxxxxxxxxxxxxxxxxx"
2 / 共有ドライブ2
   \ "xxxxxxxxxxxxxxxxxxx"
3 / 共有ドライブ3
   \ "xxxxxxxxxxxxxxxxxxx"
4 / 共有ドライブ4
   \ "xxxxxxxxxxxxxxxxxxx"
config_team_drive> xxxxxxxxxxxxxxxxxxx
```

最後に確認が入るのでEnterを押して設定完了。これが一番早いと思います。
（認証を更新しないならrclone.confファイルを直接編集するのが一番早いです）


# rcloneコマンドについて：実践編
データ移行に際しては以下のコマンドとオプションを使用しています。

```
rclone copy -n -P -v --log-file logpath --exclude-from excludefile.txt sourcepath dest:destpath
```
* -n：ドライラン（実際にはコピーしない空運転）で実行する。これで確認したのちにこのオプションは外す。
* -P：進行状況を表示する
* -v：より多くの情報を表示する
* --log-file logpath：指定のパスにログを出力する
* --exclude-from excludefile.txt：指定したファイルから、コピー対象から除外するファイルを読み込む（後述）
* sourcepath：コピー元のパス。
* dest:destpath：コピー先のパス。「リモート名：パス（記載しなければ共有ドライブ直下）」という記法

似たコマンドとして```rclone sync```があり、これだとコピー先に「コピー元にはないファイル」があった場合削除します。場合によって使い分けましょう。

### --exclude-from オプションについて
一行ごとにコピーの対象に含めたくないファイルを記述します。globのパターン記法に従っています。
例えば以下のように記述したファイルを用意します。

```exclude.txt
desktop.ini
._*
/test/trash/**
```

すると

```
desktop.ini　    　→ あらゆる場所の「desktop.ini」を無視する
._*             → あらゆる場所の「"._"から始まるファイル」を無視する
/test/trash/**  → sourcepathで指定した場所から直下の「"test"下の"trash"フォルダ」を無視する
```
というふうに解釈されます。

参考：[rcloneドキュメント「Filtering」](https://rclone.org/filtering/)

# 効率的にrcloneコマンドを回す：応用編
以上のコマンドを、今回はたくさん実行することになります。その上ファイル数によって数秒から数日まで、コマンドの所要時間の幅が大きくいつ終わるかわかりません。
そのため、rubyで自動実行するようにします。

まず、入力ファイルとして「コピー元、コピー先、除外ファイル（あれば）」をCSVファイルで用意します。（コピー対象などのまとめがスプレッドシート で行われていたため）

```input_file.csv
コピー元,コピー先,サブフォルダ（, あれば除外ファイルを記述した.txtを指定）
\\sample\事業部\部署\フォルダ,remote,subfolder\subfolder
\\sample\事業部\部署\フォルダ,remote,subfolder\subfolder
\\sample\事業部\部署\フォルダ,remote,subfolder\subfolder, exclude.txt
```
以上のようなcsvを読み込んで、

```
rclone copy -n -P -v --log-file フォルダ_mm_dd_HH_MM_SS.txt --exclude-from exclude.txt フォルダ_mm_dd_HH_MM_SS.txt "\\sample\事業部\部署\フォルダ" remote:subfolder\subfolder
```
のようなコマンドを自動で打つことを目指して、rubyでプログラムします。

<details><summary><b>そして、完成したプログラムがこちらになります。（折り畳み）</b></summary>
<div>

雑多な部分を整理したり、公開できるように編集したり、いろいろ実際のものから手を加えています。


```ruby:rclone_copy.rb
require "logger"
require "csv"
require 'date'

$logger = Logger.new(STDOUT)

#rclone でコピーを実行する
#前準備としてrclone configで移行先を設定しておく必要がある
def rclone(target)
  if target.length < 5 then
    return false
  end

  source = target[0]
  #末尾にバックスラッシュが入っている場合エスケープする必要がある
  if source[-1] == "\\" then
    source = source + "\\"
  end

  #ログファイル名を指定
  #ファイル名に使うと不都合な文字を除外する。少し汚い
  log_name = "log_" + Time.now.strftime("%m-%d-%H-%M-%S") + "_" +  get_filename(source).gsub(/[[:space:]]/, '').gsub(/\?/, '').gsub(/\&/, '') + ".txt"

  root = TARGET_LIST[target[1]]

  subfolder = target[2]


  str = "rclone copy -n -P -v"

  #除外フォルダの指定
  #除外ファイル指定する場合は列が多いので配列の長さが違う
  if target.length == 4
    #regular_exclude.txtには共通でコピーから弾きたいゴミファイル（desktop.iniなど）を記載する
    str = str + " --exclude-from " + "regular_exclude.txt"
  else
    str = str + " --exclude-from " + target[3].to_s
  end

  #ログファイル名の指定、コピー元の指定
  #sourceには空白が含まれるかもしれないので引用符で囲う
  str = str + ' --log-file ' + log_name + ' "' + source + '" '

  #サブフォルダの指定
  if subfolder.nil? then
    str = str + root + ':'
  else
    #サブフォルダも空白を許容するが、末尾の引用符を省略しても解釈してくれる上、エスケープ文字関係で処理が大変なため、末尾の引用符を省略している
    str = str + '"' + root + ":" + subfolder.to_s + '\\'
  end

  #ここまでで以下のコマンドがstrに格納されている
  #rclone sync -n -P -v --log-file logfile.txt --exclude-from regular_exclude.txt "souce" "remote:subfolder

  $logger.info(str)
  system(str)

end

#最後に一連の結果ログを一つのフォルダにまとめる
def check_output

  #結果のファイル一覧
  logs = Dir.glob("log*.txt")


  for log in logs do
    str = 'move "'  + log + '"　./' + folder_name
    system(str)
  end

  str = "move conclusion.txt ./" + folder_name
  system(str)

end


#フォルダのフルパスから末尾のものを取り出してログファイル名として使えるように加工
def get_filename(path)
  arr = path.split("\\")

  filename = arr.last.to_s
  replaced = filename.gsub(/[\(\)]/, "")

  return replaced
end


# ========
# main
# ========
if __FILE__ == $0 then

  begin

      copy_list = CSV.read("input_file.csv")

      for target in copy_list do
        rclone(target)
      end

      check_output()

  rescue => ex

    $logger.fatal(ex)
    exit(-1)

  end

end

```

</div>

</details>


# エラーとその対応
```rclone copy```で発生したエラーと対策について紹介します。

* "googleapi: Error 403: The shared drive hierarchy depth will exceed the limit., "
    * フォルダの階層数を20以上にしようとしたときのエラー。該当ファイルを削除/コピー先を変更することによって対処
* "User rate limit exceeded."
    * Googleから「送りすぎなので送るのやめて」と言われているエラー。時間をおく、認証ユーザーを切り替えるなどして対処
    * １ユーザーにつき750GB/日の制限の他に、GoogleDriveへのアップロードには制限がある程度あるようですが全貌は明らかにされていないらしいので、「そういうこともあるわな」という感じで流すのが吉
* "The file limit for this shared drive has been exceeded., teamDriveFileLimitExceeded"
    * ファイル数の制限（40万）に引っかかっているエラー。ファイルの削除、コピー除外ファイルの設定で対処
* "failed to open directory ~ Access is denied."
    * コピー元が開けないエラー。コマンドを実行するユーザーで該当フォルダが開ける権限を与えることで対処
    * もしくは単に存在しないコピー元を指定しているかも
* コピー元のフォルダ指定、除外ファイル設定に「？」が使用されている時の挙動が変
    * エクスプローラでは全角の？なのに、半角?で指定する必要がある
    * 除外ファイルの指定に「?」を含む場合、エスケープ処理（\?)が必要
    * 完全には対応し切れておらず、コマンドライン実行だと上手くいくので個別で対応した

# 終わりに
感想としては、どれだけツールを使っても大変な部分が残るもんだなぁと、そう思いました。とはいえ最終的には効率的にデータコピーできたと思います。どのツールを使うかの検証から始め、どうやってコピーするか、どうやって効率化するかをコピーしながら考えたり……いろいろいい経験と勉強になったと思います。

他にGoogleDriveなどに膨大なデータを送る方の助けになれれば幸いです。長めの記事になってしまったかと思いますがここまで読んでくださった方はありがとうございます。
また、繰り返しですがこれは[CYBIRDエンジニア Advent Calendar](https://qiita.com/advent-calendar/2021/cybird)の8日目の記事でした。
明日、9日目の記事は「JSTQB社内勉強会についてのあれそれ」（@kazurasakaさん）です。お楽しみに！
JSTQBはテスト技術者の資格です。キャリアの関係などで興味のある方は是非見てみてください。

ところで、まるで過去を振り返るようなノリでここまでお話ししてきましたが、実は今まさにコピーの真っ最中です（スケジュール通り）。コピー中の時間を利用してこの記事を書いています。
というわけで、私はこれでGoogleDriveにファイルを移行する作業に戻ります。さようなら。