【GAS】ドライブの階層をツリー形式で表示する

## Googleドライブのフォルダとファイルを一覧にする
「GAS ドライブ 階層」で調べると、すでに複数のQiitaの記事がありますが、
私が欲しいスタイルのツリー階層がなかったため、自分で作ることにしました。


#### 仕様
スクリプトに、階層を作りたい最初のフォルダのIDを指定すると、
フォルダ直下のファイルとフォルダを、サブフォルダ配下含めて出力します。
階層をわかりやすくするために、階層ごとに1列ずれて色も付きます。
また、カーソルをあてるとリンクが表示されます。(リッチテキスト形式)

並び順はデフォルトでは「最終更新日時」順となりますが、
それでは使いにくいため「フォルダ名/ファイル名」の昇順に並ぶようにしています。
ただし、文字列の配列に対してsortメソッドを使用すると[1,2,100,90,6]のような場合、
[1,100,2,6,90]となってしまいます。
ここに関してクリアしようとすると複雑になってしまうため、
仕様として許容することにしました。(Googleには念のため報告しています...)
![無題の動画 ‐ Clipchampで作成.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1317240/2392349d-1d11-d8c2-7a8f-25a1a801cdde.gif)

■イメージ画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1317240/53b570d7-ee45-fbe9-d059-8aca3fe7bd98.png)

出力できる階層数や、色付けできる階層数に上限はありません。
ただし、色は10色までとしており、11階層以上は再度1色目からループするようにしています。

#### 苦労したところ
appendを使って1行ずつ出力すればまだ簡単だったのですが、
1000ファイルぐらい出力しようとすると時間切れになってしまうため、
グローバルスコープに結果を詰め込むスタイルにしました。
処理も第一階層と、再帰処理の中で若干動きが違うため、
それを一つのメソッドに落とし込むために時間がかかりました...
また、Googleのアプリは拡張子がないので、フォルダと区別がつかずに困りました...
ですが、頭に📁アイコンを付けると一発で見やすくなったので、
今後は絵文字の活用も検討していきます。

## 追記
フォルダのみに色がついた、このようなデザインの方がわかりやすいかもしれないと思いました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1317240/acb4ca6b-2d11-3bcb-3c97-820654001a96.png)

## コード
会社のドライブの一覧を作成するのに役立つと思います。
利用する場合はコードを理解してからご利用下さい。
不利益が生じた場合には一切の責任を負いません。
```gas:メイン.gs
const SPREAD_SHEET = SpreadsheetApp.getActiveSpreadsheet();
const INSERT_START_ROW = 2;////階層表示を開始する行(2列目)
const INSERT_START_COLUMN = 2;//階層表示を開始する列(B列)
const FOLDER_ICON = "📂";
const RESULT_LIST = new Array();//最終な結果を入れる箱

function ListFilesInDriveMain() {
  SPREAD_SHEET.toast("一覧作成中...", "", 0);

  //階層を作成するシート名
  var sheet = SPREAD_SHEET.getSheetByName("シート1");

  //階層を作成したいドライブのIDを入力
  var targetFolder = DriveApp.getFolderById(XXXXXXXXXXXXXXXXXXXXX);

  //シートをきれいにする
  sheet.clear();
  SpreadsheetApp.flush();

  // 指定したフォルダ配下のファイルを取得する
  pushRichFileObj(targetFolder, 0);

  // 指定したフォルダ配下のフォルダを取得する
  pushRichFolderObj(targetFolder, 0);

  //RESULT_LIST内の最大要素数を取得する
  var arrayMaxNumIndex = getArrayMaxNumIndex(RESULT_LIST);

  //2次元配列でsetするために、RESULT_LIST内の要素数を最大要素数に合わせる。
  var newResultList = arrayIncreaseNumIndex(RESULT_LIST, arrayMaxNumIndex);

  try {
    //対象のシートに出力する
    if (newResultList.length != 0) {
      outputRichText(
        sheet,
        INSERT_START_ROW,
        INSERT_START_COLUMN,
        newResultList.length,
        newResultList[0].length,
        newResultList
      );
    }
    SPREAD_SHEET.toast("ドライブの階層を作成しました", "正常終了", 5);
  } catch (e) {
    Logger.log(e.stack);
    SPREAD_SHEET.toast(e, "エラー", 0);
  }
}
```
```階層取得.gs
/**
 * フォルダ内のファイルをファイル名順に取得し、
 * グローバルスコープの結果リストに詰める
 * @param：folderObj フォルダオブジェクト
 * @param depth 階層レベル
 */
function pushRichFileObj(folderObj, depth) {
  // 指定したフォルダ配下のファイルを取得する
  var files = folderObj.getFiles();
  var fileInfoList = new Array();

  //ファイルが存在する場合、fileInfoListに詰める
  while (files.hasNext()) {
    fileInfoList.push(files.next());
  }

  //fileInfoListをファイル名の昇順に並び替える
  executeArraySort(fileInfoList);

  //fileInfoListから拡張子付きファイル名とurlを取得して、リッチテキストオブジェクトを作成する。
  //作成したリッチテキストオブジェクトはグローバルスコープの配列(RESULT_LIST)に格納する。
  for (fileInfo of fileInfoList) {
    var richFileList = new Array();
    var fileName = fileInfo.getName();
    var fileUrl = fileInfo.getUrl();
    var richTextObj = createRichTextObj(fileName, fileUrl);
    if (depth > 0) {
      for (var i = 0; i < depth; i++) {
        richFileList.push(createRichTextObj());
      }
    }
    richFileList.push(richTextObj);
    RESULT_LIST.push(richFileList);
  }
}

/**
 * フォルダ内のフォルダをフォルダ名順に取得し、
 * グローバルスコープの結果リストに詰める
 * ファイル→フォルダの順に並べるため、ファイルとは別処理にする
 * @param：folderObj フォルダオブジェクト
 * @param depth 階層レベル
 */
function pushRichFolderObj(folderObj, depth) {
  // 指定したフォルダ配下のファイルを取得する
  var folders = folderObj.getFolders();
  var folderInfoList = new Array();

  //ファイルが存在する場合、folderInfoListに詰める
  while (folders.hasNext()) {
    folderInfoList.push(folders.next());
  }

  //folderInfoListをファイル名の昇順に並び替える
  executeArraySort(folderInfoList);

  //folderInfoListからフォルダ名とurlを取得して、リッチテキストオブジェクトを作成する。
  //作成したリッチテキストオブジェクトはグローバルスコープの配列(RESULT_LIST)に格納する。
  for (folderInfo of folderInfoList) {
    var richfolderList = new Array();
    var folderName = `${FOLDER_ICON}${folderInfo.getName()}`;
    var folderUrl = folderInfo.getUrl();
    var richTextObj = createRichTextObj(folderName, folderUrl);
    //再帰処理の場合、階層レベルの数だけ空のリッチテキストオブジェクトで先頭埋めする
    if (depth > 0) {
      for (var i = 0; i < depth; i++) {
        richfolderList.push(createRichTextObj());
      }
    }
    richfolderList.push(richTextObj);
    RESULT_LIST.push(richfolderList);
    processSubfolders(folderInfo, depth + 1);
  }
}

/**
 * サブフォルダの中を再帰的に処理する
 * @param：parentFolder フォルダーオブジェクト
 * @param：depth 階層レベル
 */
function processSubfolders(parentFolder, depth) {
  // サブフォルダのファイルを処理する
  pushRichFileObj(parentFolder, depth);

  // サブフォルダのフォルダを再帰的に処理する
  pushRichFolderObj(folderInfo, depth);
}

/**
 * ファイル名でファイルリストを昇順で並び替える
 * フォルダ名でフォルダリストを昇順で並び替える
 * @param：array 1次元配列
 */
function executeArraySort(array) {
  //昇順で並び替える
  array.sort((a, b) => {
    if (a.getName() < b.getName()) return -1;
    if (a.getName() > b.getName()) return 1;
    return 0;
  });
}

/**
 * リッチテキストオブジェクトを作成する。
 * 呼び出し時に引数が空の場合は、空のリッチテキストオブジェクトを作成する
 * @param：name ファイル/フォルダの名前
 * @param：url リンク
 */
function createRichTextObj(name = "", url = "") {
  if (name != "" && url != "") {
    return SpreadsheetApp.newRichTextValue()
      .setText(name)
      .setLinkUrl(0, name.length, url)
      .build();
  } else {
    return SpreadsheetApp.newRichTextValue()
      .setText(name)
      .build();
  }
}
```
```要素数をそろえる.gs
/**
 * 二次元配列内の各要素（一次元配列）の要素数を指定された数まで、空のリッチテキストオブジェクトで埋める
 * @param：twoDimArrayList 二次元配列
 * @param：count 最大要素数
 * @return：twoDimArrayList 一次元配列の要素数が統一された二次元配列 
 */
function arrayIncreaseNumIndex(twoDimArrayList, count) {
  for (list of twoDimArrayList) {
    while (list.length < count) {
      list.push(createRichTextObj());
    }
  }
  return twoDimArrayList;
}

/**
 * 配列の最大要素数を計算する
 * @param：twoDimArrayList 二次元配列
 * @return：maxLength 最大要素数
 */
function getArrayMaxNumIndex(twoDimArrayList) {
  var maxLength = 0;
  for (list of twoDimArrayList) {
    if (maxLength < list.length) {
      maxLength = list.length;
    }
  }
  return maxLength;
}
```
```結果出力.gs
/**
 * リッチテキスト形式で出力する。
 */
function outputRichText(sheetObj, startRow, startColumn, downMovecount, rightMoveCount, richtTextObjList) {
  var insertRange = sheetObj.getRange(startRow, startColumn, downMovecount, rightMoveCount);
  //リッチテキストを出力する
  insertRange.setRichTextValues(richtTextObjList);

  //リンクになっているためアンダーバーを削除する
  insertRange.setFontLine("non");

  //リンクになっているため文字色を黒にする
  insertRange.setFontColor("BLACK");

  //階層に色を付ける
  try {
    setBackGroundColor(sheetObj, richtTextObjList);
  } catch (e) {
    throw e;
  }
}

/**
 * 階層ごとにセルに色を付ける
 * @param：richtTextObjList 二次元配列
 */
function setBackGroundColor(sheetObj, richtTextObjList) {
  //階層ごとに色を変えるための色の配列を用意する。必ず10色とすること。
  const COLOR_LIST = [
    "#C0FFCA",
    "#FFDBF6",
    "#D1F0FF",
    "#FFF9CC",
    "#E6D2FF",
    "#FFB9A3",
    "#80D9FF",
    "#FFF88E",
    "#E58FFF",
    "#d9d9d9"
  ]
  //COLOR_LISTが10色ではない場合、エラーとして終了する。
  if (COLOR_LIST.length != 10) throw new Error("COLOR_LISTが10色ではないため、階層の色付け処理は行われません。階層構造は正常に作成されています。");

  //色を変える開始位置と終了位置をまとめるリスト
  const rangeAndColorList = new Array();
  //色用のインデックスを準備する
  var colorIndex = 0;

  //階層ごとに色を付ける
  for (i = 0; i < richtTextObjList.length; i++) {
    for (k = 0; k < richtTextObjList[i].length; k++) {

      //カラーリストが一巡した場合は再度初めの色から回す(k÷COLOR_LISTの余りをcolorIndexとすることで可能となる)
      var text = richtTextObjList[i][k].getText();
      if (text) {
        if (text.match(FOLDER_ICON)) {
          colorIndex = k % COLOR_LIST.length;
          rangeAndColorList.push(
            [INSERT_START_ROW + i,
            INSERT_START_COLUMN + k,
            richtTextObjList.length - i,
            richtTextObjList[i].length - k,
            COLOR_LIST[colorIndex]]);
        } else {
          rangeAndColorList.push(
            [INSERT_START_ROW + i,
            INSERT_START_COLUMN + k,
            richtTextObjList.length - i,
            richtTextObjList[i].length - k,
              "#ffffff"]);
        }
      }

    }
  }

  //色をセルに反映させる
  for (rangeAndColor of rangeAndColorList) {
    sheetObj.getRange(rangeAndColor[0], rangeAndColor[1], rangeAndColor[2], rangeAndColor[3]).setBackground(rangeAndColor[4]);
  }
}
```

業務に役立つツールを公開していくので、
他の記事もよろしければ見ていってください！