SaaS

GWS × GAS:共有ドライブファイル/フォルダ一覧と共有状態を出力

※2024/03/25 追記:GASのタイムアウト処理が意図した動作をせず、都度シートがリセットされてしまい全件取得できない不具合を修正しました。

※2024/04/08 追記:フォルダURLにリソースキーが付与されている場合にエラーを吐く事象を解決しました。また、getOrCreateSheet()関数で共有ドライブの名称取得が上手くいかない事象を解決しました。
enpiさんありがとう!

はじめに

GWS管理者が頭を悩ませることでおなじみ、GWSの共有ドライブの権限管理わからん問題。

権限状態をぱっと見るための一覧出力GASを作りました。

利用イメージ

Google 共有ドライブのフォルダとファイルの一覧を出力!

使い方

必要なもの

  • 対象共有ドライブの管理権限または編集権限
  • Googleスプレッドシート

1. Googleスプレッドシートの準備

  1. 新しいGoogleスプレッドシートを作成し、名前を設定する。
    • (任意)スクリプトの中のシート1部分を、スプレッドシートのシート名に変更する。

2. Google Apps Scriptのプロジェクト作成

  1. Google Apps Scriptにアクセスし、ログインする。
  2. 「+ New Project」をクリックして新しいプロジェクトを作成する。

3.スクリプトの追加

  1. 下記スクリプトをコピーする。
  2. Google Apps Scriptエディタにペーストする。
const SPREADSHEET_ID = 'Sheet_ID'; // Sheet_ID をスプレッドシートのIDに変更
const SHARED_DRIVE_ID = 'Drive_ID'; // Drive_ID を共有ドライブのIDに変更
const MAX_EXECUTION_TIME = 1740000;
const BATCH_SIZE = 100; // バッチ処理のサイズ
const scriptProperties = PropertiesService.getScriptProperties();

let existingData = []; // 既存のデータを保持する配列

function listFilesInSharedDrive() {
  let startTime = new Date().getTime();
  const sheet = getOrCreateSheet();
  const rootFolder = DriveApp.getFolderById(SHARED_DRIVE_ID);
  
  // 既存のデータを取得
  existingData = sheet.getDataRange().getValues();
  
  // ファイル/フォルダの一覧出力が完了しているかどうかを判別
  const isListingCompleted = existingData.some(row => row[1] === '処理が完了しました');
  
  if (isListingCompleted) {
    // 権限確認が完了しているかどうかを判別
    const isPermissionCheckCompleted = existingData.every(row => row[8] !== '');
    
    if (isPermissionCheckCompleted) {
      Logger.log('ファイル/フォルダの一覧出力と権限確認が完了しました。');
      return;
    } else {
      // 権限確認が完了していない場合は、権限情報を取得
      getPermissionsForFilesAndFolders(sheet, startTime);
    }
  } else {
    // 共有ドライブの直下に存在するフォルダとファイルを取得
    const topLevelFoldersAndFiles = [];
    const folders = rootFolder.getFolders();
    while (folders.hasNext()) {
      const folder = folders.next();
      topLevelFoldersAndFiles.push([folder.getName(), 'フォルダ', folder.getUrl(), '', '', '', '', 'Not Checked', '']);
    }
    const files = rootFolder.getFiles();
    while (files.hasNext()) {
      const file = files.next();
      topLevelFoldersAndFiles.push([file.getName(), 'ファイル', file.getUrl(), '', '', '', '', '', '']);
    }
    
    // バッチ処理でシートへ記載する
    writeBatchDataToSheet(sheet, topLevelFoldersAndFiles);
    
    // フォルダ内のフォルダ一覧、ファイル一覧を取得する
    let uncheckedFolders = existingData.filter(row => row[1] === 'フォルダ' && row[7] === 'Not Checked');
    while (uncheckedFolders.length > 0) {
      if (checkExecutionTime(startTime)) {
        return;
      }
      const folderBatch = uncheckedFolders.slice(0, 10); // 一度に10個のフォルダを処理
      folderBatch.forEach(folder => {
        processFolder(folder[2], sheet);
      });
      uncheckedFolders = existingData.filter(row => row[1] === 'フォルダ' && row[7] === 'Not Checked');
    }
    
    // 処理が完了したことを示すメッセージをシートに追加
    sheet.appendRow(["", "処理が完了しました", "", "", "", "", "", "", ""]);
    scriptProperties.deleteProperty('last_processed_folder');
  }
}

function getPermissionsForFilesAndFolders(sheet, startTime) {
  const dataRange = sheet.getDataRange();
  const data = dataRange.getValues();
  
  for (let i = 1; i < data.length; i++) {
    if (checkExecutionTime(startTime)) {
      return;
    }
    
    const [name, type, url, path, editUsers, viewUsers, publicStatus, folderItemCheckStatus, permissionCheckStatus] = data[i];
    
    if ((type === 'ファイル' || type === 'フォルダ') && permissionCheckStatus !== 'Checked') {
      try {

        const fileOrFolder = DriveApp.getFileById(url.split('/')[5]) || DriveApp.getFolderById(getFolderIdFromUrl(folderUrl)); 
        const editors = fileOrFolder.getEditors().map(user => user.getEmail()).join(', ');
        const viewers = fileOrFolder.getViewers().map(user => user.getEmail()).join(', ');
        const isPublic = fileOrFolder.getSharingAccess() === DriveApp.Access.ANYONE_WITH_LINK ? 'Public' : 'Private';
        
        sheet.getRange(i + 1, 5).setValue(editors);
        sheet.getRange(i + 1, 6).setValue(viewers);
        sheet.getRange(i + 1, 7).setValue(isPublic);
        sheet.getRange(i + 1, 9).setValue('Checked');
      } catch (error) {
        Logger.log(`Error processing file/folder: ${name} (${type}) at path: ${path}`);
        Logger.log(`Error message: ${error.message}`);
        sheet.getRange(i + 1, 9).setValue('Error');
      }
    }
  }
}

function processFolder(folderUrl, sheet) {
  const folderId = getFolderIdFromUrl(folderUrl);
  if (!folderId) {
    Logger.log('folderIdがみつかりません:'+folderUrl);
  }
    
  const folder = DriveApp.getFolderById(folderId);

  const folderPath = getFolderPath(folder);
  
  // フォルダ内のフォルダ一覧とファイル一覧を取得
  const subFoldersAndFiles = [];
  const folders = folder.getFolders();
  while (folders.hasNext()) {
    const subFolder = folders.next();
    subFoldersAndFiles.push([subFolder.getName(), 'フォルダ', subFolder.getUrl(), folderPath + '/' + subFolder.getName(), '', '', '', 'Not Checked', '']);
  }
  const files = folder.getFiles();
  while (files.hasNext()) {
    const file = files.next();
    subFoldersAndFiles.push([file.getName(), 'ファイル', file.getUrl(), folderPath + '/' + file.getName(), '', '', '', '', '']);
  }
  
  // バッチ処理でシートへ記載する
  writeBatchDataToSheet(sheet, subFoldersAndFiles);
  
  // フォルダのチェック状態を更新
  updateFolderCheckStatus(sheet, folderUrl, 'Checked');
}

function getOrCreateSheet() {
  const sharedDriveName = Drive.Drives.get(SHARED_DRIVE_ID).name;
  let sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(sharedDriveName);
  if (!sheet) {
    sheet = SpreadsheetApp.openById(SPREADSHEET_ID).insertSheet(sharedDriveName);
    initializeSheet(sheet);
  }
  return sheet;
}

function initializeSheet(sheet) {
  sheet.appendRow(['ファイル/フォルダ名', 'タイプ', 'URL', 'パス', '編集権限ユーザー', '閲覧権限ユーザー', '公開状態', 'FolderItem CheckStatus', 'PermissionCheck Status']);
}

function writeBatchDataToSheet(sheet, data) {
  const newData = data.filter(row => !existingData.some(existingRow => existingRow.join() === row.join()));
  if (newData.length > 0) {
    sheet.getRange(sheet.getLastRow() + 1, 1, newData.length, newData[0].length).setValues(newData);
    existingData = existingData.concat(newData); // 既存のデータに新しいデータを追加
  }
}


function checkExecutionTime(startTime) {
  return (new Date().getTime() - startTime) >= MAX_EXECUTION_TIME;
}

function resetProcessProperties() {
  scriptProperties.deleteProperty('last_processed_folder');
  scriptProperties.deleteAllProperties(); // Reset all last_processed_file_* properties
}

function getFolderPath(folder) {
  const folderPath = [];
  let currentFolder = folder;
  
  while (currentFolder.getId() !== SHARED_DRIVE_ID) {
    folderPath.unshift(currentFolder.getName());
    currentFolder = currentFolder.getParents().next();
  }
  
  return folderPath.join('/');
}

function updateFolderCheckStatus(sheet, folderUrl, status) {
  const rowIndex = existingData.findIndex(row => row[2] === folderUrl);
  
  if (rowIndex !== -1) {
    existingData[rowIndex][7] = status;
    const range = sheet.getRange(rowIndex + 1, 8);
    range.setValue(status);
  }
}

function getFolderIdFromUrl(url) {
  // URLからフォルダIDを抽出するための正規表現パターン
  // このパターンは、folders/の後の任意の文字列(フォルダID)を抽出し、
  // その後に?や終端が続く場合を考慮しています。
  const pattern = /drive\/folders\/([^\/?]+)(?:\?|$)/;
  
  // URLに対して正規表現を適用
  const match = url.match(pattern);
  
  // マッチした場合、フォルダIDを返す
  if (match && match[1]) {
    return match[1];
  } else {
    // マッチしない場合はnullを返す
    return null;
  }
}

4.スクリプトの設定

変数の値を変更する

1.const SPREADSHEET_ID = 'Sheet_ID'; //Sheet_ID をスプレッドシートのIDに変更

  • ID取得方法
    • スプレッドシートのURLから、d/[id]/edit のid部分を取得

2.const SHARED_DRIVE_ID = 'Drive_ID'; //Drive_ID を共有ドライブのIDに変更

  • ID取得方法
    • 一覧を取得したい共有ドライブのページから、URLの末尾文字列を取得

DriveAPIの有効化

  1. Google Apps Scriptエディタで、左部のサービス[+] をクリックする。
  2. Drive API を選択して[追加] をクリックする。

※2024/03/22 追記:DriveAPI v3が出てますが、v3では正しく動作しないのでv2を選択してください。

5. スクリプトの実行

  1. Google Apps Scriptエディタで、上部のプルダウンメニューから関数listFilesInSharedDriveを選択する。
  2. 左側の再生ボタン(▶️)をクリックして実行する。
  3. 必要に応じて、Google Apps Scriptに必要な権限を付与する。

※ファイル、フォルダ件数が大きい場合は時間がかかります。
参考として、ファイル+フォルダ合算件数:3000件で30分ほど。

6. 結果の確認

このGASは、ファイル/フォルダ一覧の出力→権限の出力 の流れで処理しています。

  1. Googleスプレッドシートを開いて、フォルダ/ファイル一覧が正しく書き込まれていることを確認する。
    • 最終行に処理が完了しましたと記載されていない場合、再度関数listFilesInSharedDriveを実行する。

GASの実行時間上限(6分 or 30分)を超える場合、処理途中の変数等を保存する仕組みにしています。
処理が中断されたら、再度listFilesInSharedDrive を実行してください。

おわりに

車輪の再発明、n番煎じっぽい記事ですが、特定共有ドライブの配下フォルダ/ファイルの全件取得してるGASを書いてる記事はググってもあまり見当たらなかったので書いてみました。

トップフォルダのみまたは複数の共有ドライブ一覧取得などについては、色々な方が記事を書いてくださっていますので以下の記事を参考にしてください。

GASでGoogle Driveの共有ドライブと所属ユーザ一の一覧を出力する – Qiita

Google共有ドライブの運用に便利なGAS|吉田航 (note.com)

【GAS】Googleドライブ:共有ドライブ(メンバー・権限)一覧出力 – Qiita

ではでは。

ばるす

パチンコ屋→焼き肉屋→情シスを経てクラウドネイティブへ入社。
趣味はギター,キーボード,アウトプット,散歩,読書など。
苦手なものは朝と事務作業。得意分野は眠ること。