GASのタイムアウトを乗り越えよう
Google製品のものを自動化するならGoogle Apps Script一択ですよね。
ほぼJSみたいな書き心地で、何よりもこの機能性を持っているにも関わらず無料で動かせるという、なかなかにぶっ飛んだサービスです。
そんな便利なGASですが、1個だけ弱点があります。
…時間がかかる処理だとタイムアウトしてしまう!
GASは無料アカウントの場合は6分、Google Workspaceを契約しているアカウントの場合は30分でタイムアウトします。
先日だいぶ大量のデータ(ファイル数は100万弱くらい)をコピーしたいという依頼を頂いたのですが、このタイムアウトの壁でかなり苦戦しました
ファイルサイズにもよりますがコピーする場合、大体1ファイルあたり4秒くらいかかるので、1つのディレクトリに500ファイルとか入っていると途中で止まります。
ということでそんな巨大なファイルを処理するための便利な機能があったのでご紹介していこうと思います。
トークンを取得して保存する
大体こういう巨大なディレクトリを扱う場合、複雑怪奇な階層構造になっていると思うのですが、今回は1ディレクトリに大量のファイルが格納されており、それをコピーしたいというシンプルな例で紹介します。
対象ディレクトリのディレクトリの一覧を取得するにはDriveAppオブジェクトのgetFilesメソッドを使います。
const files = DriveApp.getFiles();
getFilesはFileIteratorクラスのオブジェクトが返されます。
FileIteratorは典型的なイテレータの実装になっているので、hasNextメソッドで次のアイテムがあるかを確認し、あればnextメソッドで取得するインターフェースになっています。
そして、ドキュメントを見るともう一つgetContinuationTokenというメソッドがあります。
FileIteratorドキュメント
説明を見ると、「このイテレーションを後で再開するために使用できるトークンを取得します。」とあります。
これを使えば処理を途中でセーブして後から再開するとかができそう!
また、このトークンは次に実行される処理に渡したいので、データを変数ではなくプロパティサービスで保存して、次の処理で取り出せるようにしましょう。
プロパティサービスは以下のようにして使うことができます。
// 保存する時
PropertiesService.getScriptProperties().setProperty(key, token);
// 取り出す時
PropertiesService.getScriptProperties().getProperty(key);
スクリプトプロパティはスクリプト単位で作れる一時的なデータストアみたいなもので、キーを使って処理をまたいでデータを保存できます。
ということで、まずはファイルを繰り返しコピーし、一定数の処理が完了したらトークンを保存するところまでの処理を書いていきます。
function main() {
// プロパティに用のキー(わかりやすければなんでも良い)
const propertyKey = 'fileCopyToken';
const sourceFolderId = 'コピー元フォルダのID';
const sourceFiles = DriveApp.getFolderById(sourceFolderId).getFiles();
const destinationFolderId = 'コピー先フォルダのID';
const destinationFolder = DriveApp.getFolderById(destinationFolderId);
let processedCount = 0;
// 一度で処理するファイル数(タイムアウトしないくらいの量にするのがよい)
const pageSize = 50;
while (sourceFiles.hasNext() && processedCount < pageSize) {
const file = sourceFiles.next();
file.makeCopy(destinationFolder);
processedCount++;
}
if (sourceFiles.hasNext()) {
// 現在の処理位置までのトークンを取得する
const nextToken = sourceFiles.getContinuationToken();
// プロパティに保存する
PropertiesService.getScriptProperties().setProperty(propertyKey, nextToken);
}
}
これでとりあえず現在処理した位置を保存できるようになりました。
次はトークンがあったらそこから再開できるようにします。
トークンを使って処理を再開する
トークンを使って処理を再開する場合、先ほどの処理でプロパティに保存したトークンを取得し、あればそれを使って、なければ最初から処理をするようにします。
トークンはFilesIteratorクラスで取得しましたが、トークンを使って途中になっているFileIteratorを取り出すときは、DriveAppのcontinueFileIteratorメソッドの引数に渡してあげます。
function main() {
const propertyKey = 'fileCopyToken';
// プロパティにトークンがあるかを検索する
const token = PropertiesService.getScriptProperties().getProperty(propertyKey);
// 再代入するようになったのでletで宣言
let sourceFiles;
// トークンがあればトークンで続きを取得、なければ最初から処理をする
if (token) {
sourceFiles = DriveApp.continueFileIterator(token);
} else {
const sourceFolderId = 'コピー元フォルダのID';
sourceFiles = DriveApp.getFolderById(sourceFolderId).getFiles();
}
const destinationFolderId = 'コピー先フォルダのID';
const destinationFolder = DriveApp.getFolderById(destinationFolderId);
let processedCount = 0;
const pageSize = 50;
while (sourceFiles.hasNext() && processedCount < pageSize) {
const file = sourceFiles.next();
file.makeCopy(destinationFolder);
processedCount++;
}
if (sourceFiles.hasNext()) {
const token = sourceFiles.getContinuationToken();
PropertiesService.getScriptProperties().setProperty(propertyKey, token);
} else {
// 処理が全て完了した時の後処理(必要なら)
}
}
これで大量のファイルをコピーする時に途中から再開できるスクリプトが完成しました。
実際に使う上での注意点
今回のコードは単純に大量のファイルがあった場合のスクリプトなので、もし階層構造が複雑だったりする場合は適宜再起的に処理を読んだりする必要があるかと思います。
その場合、複数のイテレータがどのようにトークンを処理するかは私の方では未検証なので、必要に応じてご自身で調べて実装する必要があるかと思います。
また、大量にGASの処理を動かすとタイムアウト以外にも、ごく稀にサーバーエラーで処理が止まることがあります。
大量にスクリプトを実行したのですが、ログでサーバエラーが数回発生しているのを見かけました。
今回のスクリプトは途中で処理が止まると次回再開する時に前回の中途半端な実行結果があったら重複して処理されるようになっています。
重複を防ぐ場合、例外処理を使ったり、whileのところで毎回トークンを取得してプロパティに保存するなど少し工夫をする必要があります。
あくまで本記事は処理を途中でセーブし、次の実行で続きから再開する処理を書くためのサンプルとして参考にしていただければと思います。