GoogleDriveの特定フォルダ配下のフォルダ・ファイルの権限をチェックするGoogleAppsScriptを作りました

実行するアカウントが参照出来る範囲で、GoogleDriveの指定フォルダ配下のアクセス権限を調査するGoogleAppsScriptを作成しました。

folderIdで指定したフォルダ配下のフォルダ・ファイルの調査結果を、spreadsheetUrlで指定したスプレッドシートまたはこのスクリプトのコンテナになるスプレッドシートのsheetNameで指定したシート名のシートに書き出します。書き出されたスプレッドシートのイメージは下記表です。

階層種別IDパスアクセス範囲アクセス権限オーナー編集権限所有者参照権現所有者アクセス権限継承サブフォルダチェック
1FolderxxxxxyyyyPRIVATENONExxx@xxx.comxxx@xxx.comyyy@xxx.comFALSETRUE
2Filecccccyyyy/zzzzPRIVATENONExxx@xxx.comxxx@xxx.comyyy@xxx.comTRUE
2Folderdddddyyyy/aaaaPRIVATENONExxx@xxx.comxxx@xxx.comyyy@xxx.com,zzz@xxx.comFALSETRUE

アクセス権限継承という列は親フォルダと、”アクセス範囲”、”アクセス権限”、”オーナー”、”編集権限所有者”、”参照権限所有者”が一致している場合にTRUEで、一致していない場合にFALSEで表示されます。

サブフォルダチェックという列は、該当行のフォルダの一階層したのフォルダ・ファイルのチェックが終わっていない場合はTRUEで、終わっていない場合はFALSEで表示されます。1回のスクリプト実行で指定フォルダ配下のフォルダ・ファイルのチェックが全て終わらないため、どこまでチェックが終わっていて、どこから調査を再開すればいいかを判断するためのフラグとして利用しています。種別がFileの行は空行、種別がFolderの行はTRUEまたはFALSEが表示されます。全てのフォルダのチェックが終わったら、全フォルダの行がTRUEで表示されます。

調査できるフォルダ・ファイルはスクリプトを実行するアカウントの権限に左右されます。実行アカウントが参照出来ないフォルダ・ファイルはリストアップされません。実行アカウントが編集できないフォルダ・ファイルの編集権限所有者と参照権限所有者は表示されません。

1回のスクリプト実行時間はGoogleWorkspaceのBusinessの実行時間の30分を想定して下記スクリプトを作成していますので、実行時間が6分の環境の場合は、想定通りに動かない可能性があります。具体的にはfolderIdで指定したフォルダの直下のフォルダ・ファイルのチェックが6分以内に終わらない場合は、スプレッドシートに調査結果が書き出されません。folderIdで指定したフォルダ直下にあるフォルダ・ファイルの数が300以上あると6分以内に終わらない可能性がでてきます。また実行時間が6分の環境では、処理を中断して再実行用のトリガーを設定する部分を書き換える必要があります。

このスクリプトをご利用される場合は、まずフォルダ数・ファイル数が少ないフォルダに対して実行いただき、動きを確かめていただけますようお願いいたします。もっといい書き方を教えていただけると大変ありがたいです。


2023年8月9日追記
調査完了後に、調査結果をSortしてFilterを作る部分にバグがあったので修正しました。修正箇所は、下記の赤文字部分です。

<<修正前>>

  // スプレッドシートに書き出されたデータがある場合は、調査が途中である可能性があるため、調査を再開するためのトリガーを設定する
  // 書き出されたデータがない場合は、調査終了と判断し、調査結果を階層が見やすいようにソートする
  if (currentRange != sheet.getDataRange()) {
      setTrigger_();
  } else {
      // 絶対パス名で昇順ソートし、各項目でフィルターできるようにフィルターを作成する
      // ソートは2階層目のサブフォルダ以降の調査結果に対して行う(1階層目のフォルダと、2階層目のファイルはソート済なので対象にしない
      let sortStartRowCnt = 1
      for (let value of values) {
        if (value[0] == 2 && value[1] == 'Folder') break;
        sortStartRowCnt++;
      }
      sheet.getRange(sortStartRowCnt, 1, maxRow, maxCol).sort([{column: 4, ascending: true}]);
      sheet.getRange(1, 1, maxRow, maxCol).createFilter();
  }

<<修正後>>

  // スプレッドシートに書き出されたデータがある場合は、調査が途中である可能性があるため、調査を再開するためのトリガーを設定する
  // 書き出されたデータがない場合は、調査終了と判断し、調査結果を階層が見やすいようにソートする
  if (currentRange.getLastRow() != sheet.getDataRange().getLastRow()) {
      setTrigger_();
  } else {
      // 絶対パス名で昇順ソートし、各項目でフィルターできるようにフィルターを作成する
      // ソートは2階層目のサブフォルダ以降の調査結果に対して行う(1階層目のフォルダと、2階層目のファイルはソート済なので対象にしない
      let sortStartRowCnt = 1
      for (let value of values) {
        if (value[0] == 2 && value[1] == 'Folder') break;
        sortStartRowCnt++;
      }
      sheet.getRange(sortStartRowCnt, 1, maxRow - (sortStartRowCnt - 1), maxCol).sort([{column: 4, ascending: true}]);
      sheet.getDataRange().createFilter();
  }

<<全体>>

//----------------
// パラメタ設定 
//----------------
// 書き出し用スプレッドシートURL
// (スプレッドシートをコンテナとしたスクリプトとして利用する際は''のままでOK)
const spreadsheetUrl = '';
//const spreadsheetUrl = 'https://docs.google.com/spreadsheets/d/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/';

// 書き出し用スプレッドーシートのシート名
const sheetName = 'Sheet1';

// 調査対象フォルダのフォルダID
const folderId = 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy';

// デバッグフラグ
// trueにすると、Excecution logに、アクセス権情報を1件作成する度に表示する
const debugFlg = false;




/**
 * 指定フォルダ(サブフォルダ・ファイル含む)の調査を開始する
 * 調査結果は指定したスプレッドシートに書き出す
 */
function initialCheckPermission() {
  // 初期調査が30分以内に終わらないことは想定していない  

  const sheet = getSheet_();                                // 調査結果を書き出すスプレッドシート
  const folder = getFolder_(folderId);                      // 調査対象フォルダ
  const absolutePath = folder.getName();                    // 調査対象フォルダのフルパス
  const checkLimitDepth = getPathDepth_(absolutePath) + 1;  // 調査する階層の深さ(調査対象フォルダの直下を指定)
  let folderData = new Array();                             // 調査したアクセス権を保存する変数
  let parentPermissionInfo = new Array();                   // 親フォルダのアクセス権情報(最上位フォルダのため空配列)

  // 調査結果を書き出すスプレッドシートの内容をクリアして、指定フォルダのチェックを行う
  refleshSheet_(sheet);
  checkFolder_(folder, absolutePath, checkLimitDepth, parentPermissionInfo, folderData);

  // 調査結果をスプレットシートに書き出す
  let maxRow = sheet.getDataRange().getLastRow();
  sheet.getRange(maxRow + 1, 1, folderData.length, folderData[0].length).setValues(folderData);

  // さらに深い階層を調査するために調査再開トリガーを設定する
  setTrigger_();
}

/**
 * 中断したフォルダ・ファイルのアクセス権調査を再開する
 * アクセス権調査結果が書き出されているスプレッドシートを参照し、
 * "サブフォルダチェック済"欄が"FALSE"のフォルダを対象にチェックを行う
 */
function continueCheckPermission() {
  const startTime = new Date();                 // 調査開始時刻

  const sheet = getSheet_();                     // 調査結果を書き出すスプレッドシート

  const currentRange = sheet.getDataRange();    // 記載されているデータ範囲のオブジェクト
  const minRow = currentRange.getRow();         // 記載されているデータ範囲の最小行数
  const maxCol = currentRange.getLastColumn();  // 記載されているデータ範囲の最大列数
  let maxRow = currentRange.getLastRow();       // 記載されているデータ範囲の最大行数
  const values = currentRange.getValues();      // 記載されているデータを2次元配列化したデータ

  let folderData = new Array();                 // 調査したアクセス権を保存する変数
  let folder;                                   // 調査対象フォルダのフォルダオブジェクト
  let checkLimitDepth;                          // アクセス権チェックを行う最大階層

  // 実行済のトリガーを削除する
  deleteTriggers_();

  let rowCnt = minRow;
  for (let value of values) {
    // サブフォルダチェック未のフォルダに対して、アクセス権チェックを行う
    if (value.length == maxCol && value[1] == 'Folder' && value[maxCol - 1] == false) {
      folderData = new Array();

      folder = getFolder_(value[2]);                // 調査対象フォルダ
      absolutePath = value[3];                      // 調査対象フォルダのフルパス
      checkLimitDepth = value[0] + 1;               // 調査する階層の深さ(調査対象フォルダの直下を指定)

      // 調査対象フォルダ直下のファイル・フォルダを調査して、調査結果をスプレッドシートに出力し、
      // スプレッドシートの対象フォルダのれ行の"サブフォルダチェック"列をTRUEに変更する
      checkSubContents_(folder, absolutePath, checkLimitDepth, value, folderData);
      if (folderData.length > 0) sheet.getRange(maxRow + 1, 1, folderData.length, maxCol).setValues(folderData);
      sheet.getRange(rowCnt, maxCol).setValue('TRUE');
      // スプレッドシートに追記した分、スプレッドシートに記載されているデータ範囲の最大行数を更新する
      maxRow = maxRow + folderData.length;

      // 対象フォルダの調査が1つ終わる度に、経過時間をチェックし、25分経過していたらトリガーを設定して処理を終了する
      if ((Date.now() - startTime) > 1000 * 60 * 25) break;
    }
    rowCnt++;
  }

  // スプレッドシートに書き出されたデータがある場合は、調査が途中である可能性があるため、調査を再開するためのトリガーを設定する
  // 書き出されたデータがない場合は、調査終了と判断し、調査結果を階層が見やすいようにソートする
  if (currentRange.getLastRow() != sheet.getDataRange().getLastRow()) {
      setTrigger_();
  } else {
      // 絶対パス名で昇順ソートし、各項目でフィルターできるようにフィルターを作成する
      // ソートは2階層目のサブフォルダ以降の調査結果に対して行う(1階層目のフォルダと、2階層目のファイルはソート済なので対象にしない
      let sortStartRowCnt = 1
      for (let value of values) {
        if (value[0] == 2 && value[1] == 'Folder') break;
        sortStartRowCnt++;
      }
      sheet.getRange(sortStartRowCnt, 1, maxRow - (sortStartRowCnt - 1), maxCol).sort([{column: 4, ascending: true}]);
      sheet.getDataRange().createFilter();
  }
}

/**
 * 調査結果を書き出すスプレッドシートのシートオブジェクトを取得する 
 * 
 * @returns {Sheet} 指定されたスプレッドーシートの指定されたシートオブジェクト
 */
function getSheet_() {
  let sheet;

  try {
    if (spreadsheetUrl.length > 0) {
      sheet = SpreadsheetApp.openByUrl(spreadsheetUrl).getSheetByName(sheetName);
    } else {
      sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
    }
  } catch (e) {
    console.error('スプレッドシートURL(spreadsheetUrl)に存在しないURLが設定されているか、このスクリプトがスプレッドシートに紐付けられていません。');
    throw e;
  }

  try {
    sheet.getName();
  } catch (e) {
    console.error('シート名(sheetName)に存在しないシート名が設定されています');
    throw e;
  }

  return sheet;
}

/**
 * チェック対象のフォルダオブジェクトを取得する 
 * 
 * @param {String} folderId   取得したいフォルダのフォルダID
 * @returns {Folder} 指定されたフォルダIDのフォルダオブジェクト
 */
function getFolder_(folderId)
{
  let folder;

  try {
    return DriveApp.getFolderById(folderId);
  } catch (e) {
    console.error('フォルダID(folderId)に存在しないフォルダのIDが設定されています');
    throw e;
  }
}

/**
 * アクセス権情報を書き出すスプレッドシートの内容を全て削除し、
 * アクセス権情報の項目名をヘッダーとして書き出す
 * 
 * @param {Sheet} sheet
 */
function refleshSheet_(sheet) {
  let range;
  let header = [
    '階層',
    '種別',
    'ID',
    'パス',
    'アクセス範囲',
    'アクセス権限',
    'オーナー',
    '編集権限所有者',
    '参照権限所有者',
    'アクセス権限継承',
    'サブフォルダチェック済'
  ]

  // 設定されいているフィルタを解除する
  if ((range = sheet.getDataRange()).getFilter() != null) range.getFilter().remove();

  range.clear();
  sheet.getRange(1, 1, 1, header.length).setValues([header]);
  SpreadsheetApp.flush();
}

/**
 * 過去に実行された調査再開トリガーを削除する 
 */
function deleteTriggers_() {
  let triggers = ScriptApp.getProjectTriggers();

  // 既に処理を起動したトリガーを削除
  for (let trigger of triggers) {
    if (trigger.getHandlerFunction() == 'continueCheckPermission') ScriptApp.deleteTrigger(trigger);
  }
}

/**
 * 1分後に調査を再開するトリガーを設定する
 * (調査対象フォルダの規模により30分のGASの実行時間制限内に終わないため、継続調査を自動実行する)
 */
function setTrigger_() {
  // 1分後に処理を起動するトリガーを設定
  ScriptApp.newTrigger('continueCheckPermission').timeBased().after(1000 * 60).create();
}

/**
 * チェック対象フォルダおよびフォルダ配下のフォルダ・ファイルのアクセス権情報を取得する
 * 
 * @param {Folder} folder                 チェック対象フォルダのフォルダオブジェクト 
 * @param {String} absolutePath           チェック対象フォルダのフルパス
 * @param {Number} checkLimitDepth        アクセス権チェックを行う最大階層
 * @param {Array[]} parentPermissionInfo  親フォルダのアクセス権情報
 * @param {Array[][]} folderData          調査したアクセス権を保存する変数
 * @returns 
 */
function checkFolder_(folder, absolutePath, checkLimitDepth, parentPermissionInfo, folderData) {
  // チェック対象フォルダのアクセス権情報を取得する
  folderData.push(getPermissionInfo_(folder, absolutePath, parentPermissionInfo));

  // チェック対象フォルダの調査結果したアクセス権を保存する変数内の行数を保存する
  // (調査完了後に、"サブフォルダチェック済"カラムをTRUEにするため)
  let checkFolderRow = folderData.length - 1;

  // 調査対象フォルダを1階層目として指定階層と同じ深さになったら配下のフォルダ・ファイルのチェックを止める
  // 指定階層が0の場合は、チェックは止めない
  if (checkLimitDepth != 0 && getPathDepth_(absolutePath) >= checkLimitDepth) return;

  // 調査対象フォルダのファイル・フォルダを調査して、
  // 調査結果格納変数(folderData)内の調査対象フォルダのサブフォルダチェックカラムをTRUEに変更する
  checkSubContents_(folder, absolutePath, checkLimitDepth, folderData[checkFolderRow], folderData);
  folderData[checkFolderRow][(folderData[checkFolderRow]).length - 1] = true;
}

/**
 * チェック対象ファイルのアクセス権情報を取得する
 * 
 * @param {File} file                     調査対象のファイルオブジェクト
 * @param {String} absolutePath           調査対象のファイルのフルパス
 * @param {Array[]} parentPermissionInfo  親フォルダのアクセス権情報
 * @param {Array[][]} folderData          調査したアクセス権を保存する変数
 */
function checkFile_(file, absolutePath, parentPermissionInfo, folderData) {
  // チェック対象ファイルのアクセス権情報を取得する
  folderData.push(getPermissionInfo_(file, absolutePath, parentPermissionInfo));
}

/**
 * チェック対象フォルダの配下のフォルダ(サブフォルダ)・ファイルのアクセス権情報を取得する 
 * 
 * @param {Folder} folder                 チェック対象フォルダのフォルダオブジェクト
 * @param {String} absolutePath           チェック対象フォルダのフルパス 
 * @param {Number} checkLimitDepth        アクセス権チェックを行う最大階層数
 * @param {Array[]} parentPermissionInfo  親フォルダのアクセス権情報
 * @param {Array[][]} folderData          調査したアクセス権を保存する変数
 */
function checkSubContents_(folder, absolutePath, checkLimitDepth, parentPermissionInfo, folderData) {
  let subFolders, subFolder;
  let subFiles, subFile;

  // チェック対象フォルダの直下にあるファイルのアクセス権情報を調査する
  if ((subFiles = folder.getFiles()) != null) {
    while (subFiles.hasNext()) {
      subFile = subFiles.next();
      checkFile_(subFile, getAbsolutePath_(subFile,absolutePath), parentPermissionInfo, folderData);
    }
  }

  // チェック対象フォルダのサブフォルダおよびサブフォルダ内のファイルのアクセス権情報を調査する
  // checkFolderの中からcheckFolderが呼び出され、複数階層をチェックする
  if ((subFolders = folder.getFolders()) != null) {
    while (subFolders.hasNext()) {
      subFolder = subFolders.next();
      checkFolder_(subFolder, getAbsolutePath_(subFolder, absolutePath), checkLimitDepth, parentPermissionInfo, folderData);
    }
  }
}

/**
 * 指定されたフルパス階層数を返却する
 * (xxx/xxx/xxx というフルパスが渡されることを想定している) 
 * 
 * @param {String} absolutePath フルパス
 * @returns {Nulber}  フルパスの階層数
 */
function getPathDepth_(absolutePath) {
  let tmp = absolutePath.split('/');
  return tmp.length;
}

/**
 * チェック対象のフォルダまたはファイルのフルパスを返却する
 * (フォルダの区切り文字は"/"を利用する)
 * 
 * @param {Folder or File} obj 
 * @param {String} parentAbsolutePath 
 * @returns {String} 指定フォルダまたはファイルのフルパス
 */
function getAbsolutePath_(obj, parentAbsolutePath) {
  if (parentAbsolutePath.length > 0) {
    parentAbsolutePath = parentAbsolutePath + '/';
  }

  return parentAbsolutePath + obj.getName();
}

/**
 * チェック対象のフォルダまたはオブジェクトのアクセス権情報を返却する
 * 
 * @param {Folder or File} obj            チェック対象のファイルまたはフォルダのオブジェクト
 * @param {String} absolutePath           チェック対象のフルパス
 * @param {Array[]} parentPermissionInfo  チェック対象の親フォルダのアクセス権情報
 * @returns 
 */
function getPermissionInfo_(obj, absolutePath, parentPermissionInfo) {
  let objType = 'File';

  // Fileクラスにしかない関数を呼び出し、エラーが発生したらobjはFolderオブジェクトであると判断する
  try {
    obj.getDownloadUrl();
  }catch(e) {
    objType = 'Folder';
  }

  let permissionInfo = [
    getPathDepth_(absolutePath),
    objType,
    obj.getId(),                            // フォルダID or フォルダID
    absolutePath,                           // フォルダ名 or ファイル名 (フルパス)
    obj.getSharingAccess().toString(),      // 共有アクセス範囲(Domainなど、PrivateはRestrictを示す)
    obj.getSharingPermission().toString(),  // 共有アクセス権
    obj.getOwner().getEmail(),              // オーナーのメールアドレス
    getUsersEmail_(obj.getEditors()),       // 編集権限を持つユーザーのメールアドレス 
    getUsersEmail_(obj.getViewers()),       // 参照権限を持つユーザーのメールアドレス
  ];

  permissionInfo.push(isPermissionInherited_(permissionInfo, parentPermissionInfo));  // 親フォルダ権限継承フラグ
  permissionInfo.push(((objType == 'Folder')? false : ''));                           // サブフォルダチェック済フラグ

  if (debugFlg) console.log(permissionInfo.toString());

  return permissionInfo;
}

/**
 * 指定されたユーザーリストからメールアドレスを抽出し、
 * 1つの文字列に結合して返却する
 * 
 * @param {User[]} users  ユーザリスト
 * @returns {String} ","で区切った複数のメールアドレス文字列
 */
function getUsersEmail_(users){
  let emails = new Array();

  if (users.length > 1) {
    users.forEach((user) => emails.push(user.getEmail()));
  }

  return emails.sort().join(',');
}

/**
 * 親フォルダとアクセス権が同じ(継承している)かを判定する  
 *  
 * @param {Array[]} permissionInfo        チェック対象のアクセス権情報
 * @param {Array[]} parentPermissionInfo  親フォルダのアクセス権情報
 * @returns {BOOLEAN} 親フォルダと権限が同じならtrue, 権限が異なるならfalse
 */
function isPermissionInherited_(permissionInfo, parentPermissionInfo) {
  return (
    parentPermissionInfo.length > 8
    && permissionInfo[4] == parentPermissionInfo[4]     // 共有アクセス範囲
    && permissionInfo[5] == parentPermissionInfo[5]     // 共有アクセス権
    && permissionInfo[6] == parentPermissionInfo[6]     // オーナーのメールアドレス
    && permissionInfo[7] == parentPermissionInfo[7]     // 編集権限を持つユーザーのメールアドレス
    && permissionInfo[8] == parentPermissionInfo[8]     // 参照権限を持つユーザーのメールアドレス
  );
}