Google Apps ScriptでGoogleドライブを適正管理／ファイル格納検知・自動削除

#はじめに
会社やプロジェクトによっては、各種ファイルの扱いにルールが設けられている場合があるかと思います。ファイル管理にGoogle ドライブを使っている場合、Google Apps Scriptを使用することでこれらのルールを自動的にチェックし、ファイルを適正管理することができます。
今回は、

- ファイルの命名規則が正しいかを自動チェックし、
- 不正なファイルは自動削除後、管理者に通知する

スクリプトを組んでみます。これにより、

- 「案件A」ドライブには、「案件A」をファイル名に含むファイルのみが格納されるようになり、
- 関係のないファイルが混在することを防ぐ

ような使い方もできます。

##参照
###参考記事
本記事は下記3つの記事を組み合わせています。
[【ソースコード付き】GoogleDriveでファイルが追加・更新されたらメールで通知する](https://boomin.yokohama/archives/797)
[Google Apps Scriptでファイルフォルダの探索](https://officeforest.org/wp/2019/02/02/google-apps-script%E3%81%A7%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E3%81%AE%E6%8E%A2%E7%B4%A2/)
[GASを使って、スプレッドシート更新時にSlack通知を飛ばしてくれるbotを作ってみた](https://qiita.com/matsukazu1112/items/d47e81d4c4d08d2147d3)

###（念の為）Googleドライブ/GAS(Google Apps Scrip)とは
[Googleドライブとは](https://ja.wikipedia.org/wiki/Google_%E3%83%89%E3%83%A9%E3%82%A4%E3%83%96)
[GASとは](https://ja.wikipedia.org/wiki/Google_Apps_Script)

#やりたいこと
- ①ユーザがある共有ドライブに、プロジェクトで設けられた命名規則に則っていない資料を格納したら、
- ②自動で検知して、
- ③そのファイルを削除する。
- ④正しく格納されたら、それを通知する。

イメージはこんな感じです。
![処理流れイメージ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2286500/c9993ef9-1271-b9ac-dccd-8c72f29f1129.png)



#やったこと（ざっくり）
1. フォルダ内のファイル情報の検索をしたら、配列に保存する
2. 親フォルダの名称にある企業名と、取得したファイル名にある企業名を突き合わせて、異なっていたら削除する
3. スプレッドシートの情報を取得して、その情報と突き合わせて新しく格納されたファイルがあるか確認し、あれば通知する

※下記の前提条件を置きます

- 親フォルダ名に企業名、格納するファイル名にも企業名を記載する、という命名規則を置くこと

#やったこと（詳細）
###トリガーの設定
理想としてはファイルが格納されたタイミングで親フォルダ名とスプシ情報との突合を行いたかったのですが、ファイルの格納を検知する方法が思いつかず、今回はトリガーを利用して実現しました。
出来る限りファイル格納をリアルタイムで検知するため、トリガータイプは分ベースのタイマーとし、1分おきにスクリプトが走るようにしています。
![トリガー設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2286500/4eb22ce3-2c97-82ad-f02f-98b0b037a057.png)

###スクリプトの処理フロー
設定したトリガーが起動するスクリプトは下記フローで処理を行います。

1. フォルダID、スプレッドシート（以下スプシ）ID、スプシシート名、Slack通知用URLを取得（今回はスクリプトに直接記入）
2. 共有フォルダ内にあるフォルダ情報を取得する
3. 各フォルダ直下にあるファイル情報を取得する
4. 親フォルダ名とファイル名を突合し、異なる名称であればファイルを削除し、任意のSlackチャンネルに通知する
5. スプシに記載されているファイル情報を取得する
6. フォルダ内にあるファイル名と、スプシに記載されているファイル名を比較する
    7. 既に記載されている場合、何もしない
    7. フォルダにあってスプシにない場合、格納した旨を任意のSlackチャンネルに通知する
    7. スプシにあってフォルダにない場合、スプシから削除する

フォルダの階層・スプシの配置はこんな感じです。

- 親フォルダ名→このフォルダ名に記載の企業名を利用して4.の処理を行います。
- スプシはどこでも良いですが、今回はとりあえず親フォルダ直下へ
- 他社の情報を削除しないよう、明示的に自社／他社フォルダを分ける
- スプシのファイル名やシート名は何でも良い

![フォルダ構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2286500/504db817-16cc-06ea-afe3-3fb6b30f1521.png)

###その他
スプレッドシートからスクリプトの作成・実行の方法、トリガーの設定方法などは[こちら](https://boomin.yokohama/archives/797)をご参考ください。

###処理の詳細
####1. フォルダID、スプレッドシート（以下スプシ）ID、スプシシート名、Slack通知用URLを取得（今回はスクリプトに直接記入）
```javascript:
  //Slack通知
  var url = "https://hooks.slack.com/services/xxxxxx"; //xxxxxxには任意のIDを記入
  //対象とするGoogleDriveフォルダのID
  var FOLDER_ID = "xxxxxxx";  //xxxxxxには任意のIDを記入
  //更新日時を記録するのスプレッドシートのID
  var UPDATE_SHEET_ID = "xxxxxx"; //xxxxxxには任意のIDを記入
  //スプレッドシートのシート名
  var UPDATE_SHEET_NAME = "xxxxxx";//xxxxxxには任意のシート名を記入
```
GoogleDriveフォルダのIDは任意のGoogleドライブのURLの`folders/`以降の文字列です。
スプレッドシートのIDは`https://docs.google.com/spreadsheets/d/`**ここ**`/edit#gid=0`です。
スプレッドシートのシート名は
![スプシシート名.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2286500/ee480144-f7c2-1d6b-c3d4-5de02429f439.png)
ここです。

####2. 共有フォルダ内にあるフォルダ情報を取得する
```javascript:
  // スプレッドシートに記載されているフォルダ名と更新日時を取得。
  var spreadsheet = SpreadsheetApp.openById(UPDATE_SHEET_ID);
  var sheet = spreadsheet.getSheetByName(UPDATE_SHEET_NAME);

  //Drive APIで特定のファイルをリストとして取得
  var key = FOLDER_ID;
  var nowdir = key;
  var totalcount=0;
  var totalFilesCount=0;
  
  //folderlistシートに移動し内容をクリアする
  var Cell = sheet.getRange("A2");//offsetの参照点セルを設定
  Cell.activate();
  
  //フラグ類
  var subfolderflag = 0;  //1なら有り、0なら無し。

  //親フォルダ（スプシが格納されているフォルダ）名を取得
  var psheetfolder = DriveApp.getFileById(UPDATE_SHEET_ID).getParents();
  var sfolder = psheetfolder.next(); 
  let splitTextpFolder = sfolder.getName().split("_");
  var companyName = splitTextpFolder[0];

  //フォルダがあるか検索し、あった場合にはフォルダリスト配列に格納する
  //フォルダ直下のファイル情報を配列に格納する。totalcountを利用して配列番号を更新する。
  var folderlist = new Array();
  var folders = DriveApp.searchFolders("'"+key+"' in parents");
  var myFile = {};
```
####3. 各フォルダ直下にあるファイル情報を取得する<br>4. 親フォルダ名とファイル名を突合し、異なる名称であればファイルを削除し、任意のSlackチャンネルに通知する

情報取得しながらフォルダ名とファイル名の突合を行います。突合し誤っていればその時点で削除＆通知を送ります。

```javascript:
  //フォルダ直下にファイルがあるか確認する
  var folderDirect = DriveApp.getFolderById(FOLDER_ID); //フォルダIDなので、このフォルダ名自体は別途取得が必要
  var filesDirect = folderDirect.getFiles(); //あくまでfolder直下にあるファイルのみ探索
      while(filesDirect.hasNext()){
        var fileDirect = filesDirect.next();
        let splitTextFile = fileDirect.getName().split("_");
        if(splitTextFile[0] == companyName){
          myFile[totalFilesCount] = {
            name: fileDirect.getName(),
            lastUpdate: fileDirect.getLastUpdated(),
            url: fileDirect.getUrl(),
            diff: 0
          }
          totalFilesCount++;
        }else{
          fileDirect.setTrashed(true);
          var options = {
            'method': 'post',
            'headers': {'Content-type': 'application/json'},
            'payload' : JSON.stringify({
            'channel' : 'xxxxxx', //xxxxxxには任意のSlackチャンネル名を記入
              'attachments':[
              {                                                              
                'fallback': 'Googleドライブへ格納されたファイルが削除されました',
                'color': '#36a64f',
                'title': 'Gドライブファイル削除',
                'title_link': 'https://docs.google.com/spreadsheets/d/xxxxxx/edit#gid=0', //xxxxxxには任意のスプシIDを記入
                'text': 'test',                                                 
              }
              ]
            })
          };
          UrlFetchApp.fetch(url, options);
        }
      }


  if(folders.length == 0){
    //普通に指定フォルダ内のファイルのリストを書き出して終了
  }else{
    //サブフォルダフラグを立てる
    subfolderflag=1;
    //folderlistシートへ書き込み
    while(folders.hasNext()){
      var folder = folders.next();
      folderlist.push(folder.getId());

      var files= folder.getFiles(); //あくまでfolder直下にあるファイルのみ探索 
      while(files.hasNext()){
        var file = files.next();
        let splitTextFile = file.getName().split("_");
        if(splitTextFile[0] == companyName){
          myFile[totalFilesCount] = {
            name: file.getName(),
            lastUpdate: file.getLastUpdated(),
            url: file.getUrl(),
            diff: 0
          }
          totalFilesCount++;
        }else{
          file.setTrashed(true);
          var options = {
            'method': 'post',
            'headers': {'Content-type': 'application/json'},
            'payload' : JSON.stringify({
            'channel' : 'xxxxxx', //xxxxxxには任意のSlackチャンネル名を記入
              'attachments':[
              {                                                              
                'fallback': 'Googleドライブへ格納されたファイルが削除されました',
                'color': '#36a64f',
                'title': 'Gドライブファイル削除',
                'title_link': 'https://docs.google.com/spreadsheets/d/xxxxxx/edit#gid=0', //xxxxxxには任意のスプシIDを記入
                'text': 'test',                                                 
              }
              ]
            })
          };
          UrlFetchApp.fetch(url, options);
        }
      }
      totalcount++;
    }
   }
  
  //サブフォルダ以降のフォルダの探索ルーチン
  var sub = 1;
  while (sub == 1) {
     var nowponyo = folderlist.length;
     for(var i = 0;i<nowponyo;i++){
       //0番目の配列のkeyを取得する
       key = folderlist[0];
 
       //そのkeyのフォルダにぶら下がるフォルダをリストアップ
       folders = DriveApp.searchFolders("'"+key+"' in parents");
       nowdir = key;
       
       //folderlistシートへ書き込み
       if(folders.hasNext()==true){
         while(folders.hasNext()){
          var folder = folders.next();
          folderlist.push(folder.getId());
          totalcount++;

          var files= folder.getFiles(); //あくまでfolder直下にあるファイルのみ探索 
          while(files.hasNext()){
            var file = files.next();
            let splitTextFile = file.getName().split("_");
            if(splitTextFile[0] == companyName){
              myFile[totalFilesCount] = {
                name: file.getName(),
                lastUpdate: file.getLastUpdated(),
                url: file.getUrl(),
                diff: 0
              }
              totalFilesCount++;
            }else{
              file.setTrashed(true);
              var options = {
                'method': 'post',
                'headers': {'Content-type': 'application/json'},
                'payload' : JSON.stringify({
                'channel' : 'xxxxxx', //xxxxxxには任意のSlackチャンネル名を記入
                  'attachments':[
                  {                                                              
                    'fallback': 'Googleドライブへ格納されたファイルが削除されました',
                    'color': '#36a64f',
                    'title': 'Gドライブファイル削除',
                    'title_link': 'https://docs.google.com/spreadsheets/d/xxxxxx/edit#gid=0', //xxxxxxには任意のスプシIDを記入
                    'text': 'test',                                                 
                  }
                  ]
                })
              };
              UrlFetchApp.fetch(url, options);
            }
          }
         }
       }
      //配列0番目を削除と詰める
      folderlist.splice(0,1);
    }
 
    //folderlistが空ならsubfolderflagを0にする
    if(folderlist.length == 0){
      sub=0;
    }
  }
```

####5. スプシに記載されているファイル情報を取得する
```javascript:
  //スプシに記載されているファイル名を取得する 取得したデータをMapに変換。
  var data = sheet.getDataRange().getValues();
  var sheetData = {};
  //  headerがあるので2から開始
  for (var i = 1; i < data.length; i++) {
    sheetData[data[i][0]] = {
      name: data[i][0],
      lastUpdate: data[i][1],
      url: data[i][2],
      rowNo: i + 1
    };
  }
```

####6. フォルダ内にあるファイル名と、スプシに記載されているファイル名を比較する
```javascript:
// 実際のファイルとスプレッドシート情報を比較。
  var updateFileList = [];
  for (key in myFile) {
    if (sheetData[myFile[key].name]){
      var test = sheetData[myFile[key].name].name;
      if (test == myFile[key].name) {
          // ファイル名がシートに存在する場合。特に何もしない
      }
    } else {
      // ファイル名がシートに存在しない場合。
      var lowno = sheet.getLastRow() + 1;
      sheet.getRange(lowno, 1).setValue(myFile[key].name);
      sheet.getRange(lowno, 2).setValue(myFile[key].lastUpdate);
      sheet.getRange(lowno, 3).setValue(myFile[key].url);
      updateFileList.push(key);
      var options = {
        'method': 'post',
        'headers': {'Content-type': 'application/json'},
        'payload' : JSON.stringify({
        'channel' : 'xxxxxx', //xxxxxxには任意のSlackチャンネル名を記入
          'attachments':[
          {                                                              
            'fallback': 'Googleドライブへファイルが格納されました',
            'color': '#36a64f',
            'title': 'Gドライブファイル格納',
            'title_link': 'https://docs.google.com/spreadsheets/d/xxxxxx/edit#gid=0', //xxxxxxには任意のスプシIDを記入
            'text': 'test',                                                 
          }
          ]
        })
      };
      UrlFetchApp.fetch(url, options);
    }
  }
  
  // 削除されたファイルをチェックして、スプシから削除
  var deleteFileList = [];
  var count =0;
  for (key in sheetData){
    for (var i=0; i < totalFilesCount; i++){
      if(key == myFile[i].name){
        count++;
      }
    }
    if (count == 0){
      sheet.deleteRow(sheetData[key].rowNo)
      deleteFileList.push(key);
    }
    count = 0;
  }

  //終了処理
  SpreadsheetApp.flush();
```

#ソースコード
```javascript:fileSearchNextPutSheet.gs
function driveSearchPutArray(){

  //Slack通知
  var url = "https://hooks.slack.com/services/xxxxxx"; //xxxxxxには任意のIDを記入
  //対象とするGoogleDriveフォルダのID
  var FOLDER_ID = "xxxxxxx"; //xxxxxxには任意のIDを記入
  //更新日時を記録するのスプレッドシートのID
  var UPDATE_SHEET_ID = "xxxxxx"; //xxxxxxには任意のIDを記入
  //スプレッドシートのシート名
  var UPDATE_SHEET_NAME = "xxxxxx"; //xxxxxxには任意のシート名を記入

  // スプレッドシートに記載されているフォルダ名と更新日時を取得。
  var spreadsheet = SpreadsheetApp.openById(UPDATE_SHEET_ID);
  var sheet = spreadsheet.getSheetByName(UPDATE_SHEET_NAME);

  //Drive APIで特定のファイルをリストとして取得
  var key = FOLDER_ID;
  var nowdir = key;
  var totalcount=0;
  var totalFilesCount=0;
  
  //folderlistシートに移動し内容をクリアする
  var Cell = sheet.getRange("A2");//offsetの参照点セルを設定
  Cell.activate();
  
  //フラグ類
  var subfolderflag = 0;  //1なら有り、0なら無し。

  //親フォルダ（スプシが格納されているフォルダ）名を取得
  var psheetfolder = DriveApp.getFileById(UPDATE_SHEET_ID).getParents();
  var sfolder = psheetfolder.next(); 
  let splitTextpFolder = sfolder.getName().split("_");
  var companyName = splitTextpFolder[0];

  //フォルダがあるか検索し、あった場合にはフォルダリスト配列に格納する
  //フォルダ直下のファイル情報を配列に格納する。totalcountを利用して配列番号を更新する。
  var folderlist = new Array();
  var folders = DriveApp.searchFolders("'"+key+"' in parents");
  var myFile = {};

  //フォルダ直下にファイルがあるか確認する
  var folderDirect = DriveApp.getFolderById(FOLDER_ID); //フォルダIDなので、このフォルダ名自体は別途取得が必要
  var filesDirect = folderDirect.getFiles(); //あくまでfolder直下にあるファイルのみ探索
      while(filesDirect.hasNext()){
        var fileDirect = filesDirect.next();
        let splitTextFile = fileDirect.getName().split("_");
        if(splitTextFile[0] == companyName){
          myFile[totalFilesCount] = {
            name: fileDirect.getName(),
            lastUpdate: fileDirect.getLastUpdated(),
            url: fileDirect.getUrl(),
            diff: 0
          }
          totalFilesCount++;
        }else{
          fileDirect.setTrashed(true);
          var options = {
            'method': 'post',
            'headers': {'Content-type': 'application/json'},
            'payload' : JSON.stringify({
            'channel' : 'xxxxxx', //xxxxxxには任意のSlackチャンネル名を記入
              'attachments':[
              {                                                              
                'fallback': 'Googleドライブへ格納されたファイルが削除されました',
                'color': '#36a64f',
                'title': 'Gドライブファイル削除',
                'title_link': 'https://docs.google.com/spreadsheets/d/xxxxxx/edit#gid=0', //xxxxxxには任意のスプシIDを記入
                'text': 'test',                                                 
              }
              ]
            })
          };
          UrlFetchApp.fetch(url, options);
        }
      }

  if(folders.length == 0){
    //普通に指定フォルダ内のファイルのリストを書き出して終了
  }else{
    //サブフォルダフラグを立てる
    subfolderflag=1;
    //folderlistシートへ書き込み
    while(folders.hasNext()){
      var folder = folders.next();
      folderlist.push(folder.getId());
      var files= folder.getFiles(); //あくまでfolder直下にあるファイルのみ探索 
      while(files.hasNext()){
        var file = files.next();
        let splitTextFile = file.getName().split("_");
        if(splitTextFile[0] == companyName){
          myFile[totalFilesCount] = {
            name: file.getName(),
            lastUpdate: file.getLastUpdated(),
            url: file.getUrl(),
            diff: 0
          }
          totalFilesCount++;
        }else{
          file.setTrashed(true);
          var options = {
            'method': 'post',
            'headers': {'Content-type': 'application/json'},
            'payload' : JSON.stringify({
            'channel' : 'xxxxxx', //xxxxxxには任意のSlackチャンネル名を記入
              'attachments':[
              {                                                              
                'fallback': 'Googleドライブへ格納されたファイルが削除されました',
                'color': '#36a64f',
                'title': 'Gドライブファイル削除',
                'title_link': 'https://docs.google.com/spreadsheets/d/xxxxxx/edit#gid=0', //xxxxxxには任意のスプシIDを記入
                'text': 'test',                                                 
              }
              ]
            })
          };
          UrlFetchApp.fetch(url, options);
        }
      }
      totalcount++;
    }
   }
  
  //サブフォルダ以降のフォルダの探索ルーチン
  var sub = 1;
  while (sub == 1) {
     var nowponyo = folderlist.length;
     for(var i = 0;i<nowponyo;i++){
       //0番目の配列のkeyを取得する
       key = folderlist[0];
 
       //そのkeyのフォルダにぶら下がるフォルダをリストアップ
       folders = DriveApp.searchFolders("'"+key+"' in parents");
       nowdir = key;
       
       //folderlistシートへ書き込み
       if(folders.hasNext()==true){
         while(folders.hasNext()){
          var folder = folders.next();
          folderlist.push(folder.getId());
          totalcount++;

          var files= folder.getFiles(); //あくまでfolder直下にあるファイルのみ探索 
          while(files.hasNext()){
            var file = files.next();
            let splitTextFile = file.getName().split("_");
            if(splitTextFile[0] == companyName){
              myFile[totalFilesCount] = {
                name: file.getName(),
                lastUpdate: file.getLastUpdated(),
                url: file.getUrl(),
                diff: 0
              }
              totalFilesCount++;
            }else{
              file.setTrashed(true);
              var options = {
                'method': 'post',
                'headers': {'Content-type': 'application/json'},
                'payload' : JSON.stringify({
                'channel' : 'xxxxxx', //xxxxxxには任意のSlackチャンネル名を記入
                  'attachments':[
                  {                                                              
                    'fallback': 'Googleドライブへ格納されたファイルが削除されました',
                    'color': '#36a64f',
                    'title': 'Gドライブファイル削除',
                    'title_link': 'https://docs.google.com/spreadsheets/d/xxxxxx/edit#gid=0', //xxxxxxには任意のスプシIDを記入
                    'text': 'test',                                                 
                  }
                  ]
                })
              };
              UrlFetchApp.fetch(url, options);
            }
          }
         }
       }
      //配列0番目を削除と詰める
      folderlist.splice(0,1);
    }
 
    //folderlistが空ならsubfolderflagを0にする
    if(folderlist.length == 0){
      sub=0;
    }
  }

  //スプシに記載されているファイル名を取得する 取得したデータをMapに変換。
  var data = sheet.getDataRange().getValues();
  var sheetData = {};
  //  headerがあるので2から開始
  for (var i = 1; i < data.length; i++) {
    sheetData[data[i][0]] = {
      name: data[i][0],
      lastUpdate: data[i][1],
      url: data[i][2],
      rowNo: i + 1
    };
  }

// 実際のファイルとスプレッドシート情報を比較。
  var updateFileList = [];
  for (key in myFile) {
    if (sheetData[myFile[key].name]){
      var test = sheetData[myFile[key].name].name;
      if (test == myFile[key].name) {
          // ファイル名がシートに存在する場合。特に何もしない
      }
    } else {
      // ファイル名がシートに存在しない場合。
      var lowno = sheet.getLastRow() + 1;
      sheet.getRange(lowno, 1).setValue(myFile[key].name);
      sheet.getRange(lowno, 2).setValue(myFile[key].lastUpdate);
      sheet.getRange(lowno, 3).setValue(myFile[key].url);
      updateFileList.push(key);
      var options = {
        'method': 'post',
        'headers': {'Content-type': 'application/json'},
        'payload' : JSON.stringify({
        'channel' : '#p_test-asai',
          'attachments':[
          {                                                              
            'fallback': 'Googleドライブへファイルが格納されました',
            'color': '#36a64f',
            'title': 'Gドライブファイル格納',
            'title_link': 'https://docs.google.com/spreadsheets/d/xxxxxx/edit#gid=0',//xxxxxxには任意のスプシIDを記入
            'text': 'test',                                                 
          }
          ]
        })
      };
      UrlFetchApp.fetch(url, options);
    }
  }
  
  // 削除されたファイルをチェックして、スプシから削除
  var deleteFileList = [];
  var count =0;
  for (key in sheetData){
    for (var i=0; i < totalFilesCount; i++){
      if(key == myFile[i].name){
        count++;
      }
    }
    if (count == 0){
      sheet.deleteRow(sheetData[key].rowNo)
      deleteFileList.push(key);
    }
    count = 0;
  }

  //終了処理
  SpreadsheetApp.flush();
}
```

#まとめ
Googleドライブは非常に便利で、GASを使うことで要件毎に様々な自動処理をカスタマイズ出来るので、積極的に利用していければと思います。