GASでGoogle Driveを定期的にダイエットさせる

# TL;DR
社の先輩「カメラので撮影した画像をGoogle Driveにバンバンぶち込むんだけど15GB超えそうになるたびに整理するの地味にめんどくさい。スクリプトでよしなにしたい。」

ぼく「Google Apps Script...そういうのもあるのか。」


というわけで勉強もかねて触ってみました。

今回は↑のユースケースでとりあえず書いてみた、という記事になります。
Google Apps Scriptの素晴らしい機能の数々は是非ドキュメントとリファレンスをお読みください。
※所々ナンセンスな書き方してる気もしますが、追々勉強していきます。


# Google Apps Script とは?
[Google Apps Script](https://developers.google.com/apps-script/) は、Googleが提供するサーバーサイド・スクリプト環境です。
カレンダー、ドキュメント、ドライブ、Gmail、スプレッドシート、スライドなどをスクリプトでよしなに操作できます。

スクリプト自体はJavaScriptで書きます。
JavaScriptにGoogleのサービスを利用するためのさまざまなオブジェクトを追加したもの、といった感じでしょうか。


# 事前準備
Google Apps Script用のChrome拡張機能が必要です。
[こちら](https://chrome.google.com/webstore/detail/google-apps-script/eoieeedlomnegifmaghhjnghhmcldobl?hl=ja)から拡張機能を有効にしましょう。

拡張機能を有効にした後は、Google DriveからGoogle Apps Scriptプロジェクトを作成できます。
Google Driveにログイン後、
新規→その他→Google Apps Scriptと選択していき、下図のようなエディタが開けばプロジェクトは作成完了です。

![2019-08-23 23.28.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319120/84ff987b-afa0-f86b-0c4b-7f4564f37f1e.png)

プロジェクト自体はGoogle Driveに保存されます。


# Hello World!
挨拶、大事。

```javascript
function myFunction() {
  Logger.log("Hello World!!");
}
```

### いざ実行
関数を記述したら、エディタ上部のプルダウンから実行したい関数を選択して▶︎ボタンを押します。

![2019-08-23 23.34.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319120/c1616409-6a8d-1963-5c21-58bd23e7fb6e.png)

### ログの確認
Logger.log()などはログコンソールに出力されます。
開き方はエディタ上でctrl+enter。

![2019-08-23 23.37.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319120/399e6017-78e4-a315-7af3-94f60f387201.png)


# Driveをダイエットさせる
さて、いよいよ本題です。
Driveに保存されているファイルとサイズを取得し、一定以下のサイズになるまで古い順に削除していきます。

### Drive上にあるファイルを取得する
Drive上にあるファイルを取得するには.getFiles()を使用します。

```javascript
var files = DriveApp.getFiles();
```

#### 注1
.getFiles()の返り値はFileではなくFileIteratorです。
FileIteratorとは、取得したFileの一覧、のようなものでしょうか。
そのため、実際に取得したFileを利用するためには、以下のメソッドを利用してFileIteratorからFileオブジェクトを取り出す必要があります。

```javascript
var file = files.next();
```

最初の.next()でFileIteratorから最初のFileオブジェクトが、2回目の.next()でFileIteratorから次のFileオブジェクトが...といったように、.next()を呼ぶ度に順番にFileオブジェクトが返ります。.getFiles()で取得した全てのFileオブジェクトを取り出した後、更に.next()を呼ぶとnullが返ります。

また、.next()でFileオブジェクトを取り出した後、FileIteratorに次のFileオブジェクトが残っているかは、以下のメソッドで調べられます。

```javascript
var next = files.hasNext();
```

Fileオブジェクトが残っているならtrueが、残っていないならfalseが返ります。

#### 注２
.getFiles()はDrive上にある全てのファイルを取得します。
Google Driveでは、ファイルやフォルダを複数の場所に設置できるため、おなじみの階層構造はありません。
File/Folderオブジェクト自体が、その親となるフォルダの情報を持ちます。
(前述の通り複数の場所に設置できるため、複数の情報を持つこともあります。)

### Drive上にある特定のファイルを取得する
今回の場合は写真だけを整理したいので、ひと工夫する必要がありそうです。
何でもいいからとにかく古い順に消そう！という場合は.getFiles()を使いましょう。

さて、特定のファイルを探す方法ですが、
.searchFiles()
.searchFolders()
などがあります。

直感的には特定のフォルダ配下のファイル全てを〜とやりたいところですが、前述の通り階層構造がないためこれを実現しようとすると、全ファイルを取得→全ファイルひとつひとつの親フォルダ情報を取得→特定のフォルダを親フォルダに持つファイルをフィルタリング→...と地味に面倒くさそうです。
今回はさくっとチュートリアルだけするモチベだったので、安直にファイル名でフィルタリングしました。

```javascript
var files = DriveApp.searchFiles('title contains "fig_sample_"');
```

Driveにある全ファイルの中から、ファイル名に"fig_sample_"を含むファイルを取得します。
.searchFiles()の中身はGoogle Drive SDKの[Search query](https://developers.google.com/drive/api/v2/ref-search-terms#fn1)を使います。

### 取得したファイルを作成日順にソートする
.getFiles()で.next()するとデフォルトで作成日順にソートされてるそうですが、後学のためにもここは陽に記述しておきます。

```javascript
var list = [];
var totalSize = 0.0;
while (files.hasNext()) {
  var file = files.next();
  list.push({file:file , date:file.getDateCreated()});
  totalSize += file.getSize();
}

list.sort(function (a, b) {
  return a.date - b.date;
}); 
```

.next()で取り出したFileオブジェクトを一度配列に格納します。
今回は作成日順にソートしたいので.getDateCreated()情報と、後でいろいろ使いたいのでFileオブジェクト自体も入れておきます。

ソートとは関係ないですがwhile文でFileオブジェクトを１つ１つ見ているので、ついでに取得したFileオブジェクトの合計サイズも計算しておきます。

### 指定したサイズ以下になるまで順番に削除する
配列がソートできたら、上から順番にいい感じのサイズになるまで１つずつ削除していきます。

```javascript
var targetSize = 1024*1024*1024;

for (i = 0; i<list.length; i++) {
  if (totalSize > targetSize){
    var file = list[i].file
    var fileSize = file.getSize();
    file.setTrashed(true);
    totalSize -= fileSize;
  } else {
    break;
  }
}
```

.setTrashed(true)でFileオブジェクトをゴミ箱に移動できます。
.setTrashed(Falese)でゴミ箱から復元できます。

### ゴミ箱を空にする
怖いので手動にしましょう。
ゴミ箱に移動するだけで満足して、空にして容量を解放することをQiita書くまで失望していたとかでは、ないです。
本当です。

### いざ実行
ここまでできたら一度実行してみます。

```javascript
function dietDrive() {
  
  //fig_sample_を全て取得
  var files = DriveApp.searchFiles('title contains "fig_sample_"');
  var targetSize = 100*1024;
  
  //取得したファイルと作成日時を配列に格納
  var list = [];
  var totalSize = 0.0;
  while (files.hasNext()) {
    var file = files.next();
    list.push({file:file , date:file.getDateCreated()});
    totalSize += file.getSize();
  }
  
  //配列を作成日時順にソート
  list.sort(function (a, b) {
    return a.date - b.date;
  }); 
 
  //targetSize以下になるまで古いファイルから削除
  for (i = 0; i<list.length; i++) {
    if (totalSize > targetSize){
      var file = list[i].file
      var fileSize = file.getSize();
      file.setTrashed(true);
      totalSize -= fileSize;
    } else {
      break;
    }
  }
  
}
```
今回はテストとして、17KB程度の画像を10枚用意し、100KB以下まで削除します。

* 実行前
![2019-08-24 0.24.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319120/730d6410-0bf4-0c58-dff8-fd672331ca79.png)

* 実行後
![2019-08-24 0.27.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319120/9083327c-0378-482c-4377-c7d1a581a986.png)

先にアップロードしたものから5枚がゴミ箱に移動しました。
期待通りの動作のようです。

# Driveのダイエットを定期的に実行する
上のスクリプトを思い立った時に実行してもいいのですが、どうせなら定期的にサイズチェックをしてよしなにダイエットしておいてくれると便利そうです。
ScriptApp.newTrigger('myFunction')を利用すると、様々なイベントをトリガにスクリプトを実行することができます。

例として週に1回、金曜日にスクリプトを実行するトリガは以下のようになります。

```javascript
function createTimeDrivenTriggers() {
  
  // Trigger every friday.
  ScriptApp.newTrigger('dietDrive')
      .timeBased()
      .onWeekDay(ScriptApp.WeekDay.FRIDAY)
      .create();
  
}
```

その他、.everyHours()などで一定時間間隔で実行できたりもします。

トリガの作成に成功すると、ダッシュボードのトリガ一覧に表示されます。

![スクリーンショット 2019-08-24 0.39.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319120/1ed5c5ec-8790-ff55-6484-cf96c6db6b6c.png)

ちなみに右側の鉛筆マークからトリガの実行間隔などをGUIで編集できたりもします。

### トリガを削除する
スクリプトでトリガを削除する関数を作ることもできるのですが、特定のトリガを削除するためにはトリガ作成時にIDを控えておかなければいけなかったりこれまた面倒くさそうなのでダッシュボードから手動でぽちぽちっと削除しましょう。

![2019-08-24 0.46.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319120/07b937ad-c80a-e0da-f257-756e817d7be7.png)

一応、実行中のトリガを全削除、ならさくっと書けます。

```javascript
var triggers = ScriptApp.getProjectTriggers();
for (var i = 0; i < triggers.length; i++) {
  ScriptApp.deleteTrigger(triggers[i]);
}

```

# まとめ
実運用にはまだまだアラが目立つ気もしますが、ひとまず1時間で色々触ってみました。という記事でした。
モチベが続けばもう少し真面目に作り込んでいこうと思います。

# スクリプト全文
```javascript
//
function dietDrive() {
  
  var files = DriveApp.searchFiles('title contains "fig_sample_"');
  var targetSize = 100*1024;
  
  var list = [];
  var totalSize = 0.0;
  while (files.hasNext()) {
    var file = files.next();
    list.push({file:file , date:file.getDateCreated()});
    totalSize += file.getSize();
  }
  
  list.sort(function (a, b) {
    return a.date - b.date;
  }); 
 
  for (i = 0; i<list.length; i++) {
    if (totalSize > targetSize){
      var file = list[i].file
      var fileSize = file.getSize();
      file.setTrashed(true);
      totalSize -= fileSize;
    } else {
      break;
    }
  }
  
}

//
function createTimeDrivenTriggers() {
  
  ScriptApp.newTrigger('myFunction')
      .timeBased()
      .onWeekDay(ScriptApp.WeekDay.FRIDAY)
      .create();
  
}
```


# 参考
[Apps Script](https://developers.google.com/apps-script/)
[Class DriveApp](https://developers.google.com/apps-script/reference/drive/drive-app)
[Class ScriptApp](https://developers.google.com/apps-script/reference/script/script-app)
[Class ClockTriggerBuilder](https://developers.google.com/apps-script/reference/script/clock-trigger-builder)
[Array.prototype.sort()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)
[初心者のためのGoogle Apps Scriptプログラミング入門](https://www.tuyano.com/index2?id=638001)
[Google Apps Script 入門](https://qiita.com/t_imagawa/items/47fc130a419b9be0b447)