life-seedロゴ

NEWS

Googleドライブの”リンクを知っている全員”の共有リンクを削除するプログラム

Googleドライブの”リンクを知っている全員”の共有リンクを削除するプログラム

Googleドライブを利用して、PPAP対策や大きなファイルを送る際に”リンクを知っている人全員”という共有設定を使うことがあるかと思います。

その設定をしたままだと、誰にでも見られてしまう可能性があります。

それで、期限を設定してリンク設定を解除するGoogleAppscriptプログラムを作りました。

 

ドメイン全体の委任 設定手順

ステップ 1: スクリプトのプロジェクト番号(Client ID)を取得する

まず、Google Admin Consoleで委任設定を行うために、GASスクリプトを一意に識別するID(クライアントID)が必要です。
  1. Apps Scriptエディタを開きます。
  2. 左側のメニューから 「⚙️ プロジェクトの設定」 (Settings) をクリックします。
  3. **「Google Cloud Platform(GCP)プロジェクト」**のセクションを探します。
  4. GCPプロジェクト番号のリンクをクリックし、Google Cloud Consoleに移動します。
    • (もしGASとGCPプロジェクトの連携がまだの場合、ここで「プロジェクトの変更」または「既存のプロジェクトにリンク」を選択する必要があります。)
  5. GCP Consoleの左側メニューから 「APIとサービス」 > 「認証情報」 を選択します。
  6. 「OAuth 2.0 クライアント ID」 のリストにある、GASプロジェクトに対応するクライアントID(長い文字列)をコピーします。これが委任設定に必要なIDです。
  7. Google Drive APIを有効化

    1. GCP Consoleの左側のナビゲーションメニューで 「APIとサービス」 > 「ライブラリ」 をクリックします。
    2. 検索バーで 「Google Drive API」 を検索します。
    3. 検索結果から「Google Drive API」を選択し、ページ内の 「有効にする」 ボタンをクリックします。
  8.  

ステップ 2: Google Workspace 管理コンソールで委任を設定する

次に、コピーしたクライアントIDと、必要な権限スコープを管理コンソールに登録します。
  1. Google Workspace 管理コンソールに特権管理者アカウントでログインします。
  2. 左側メニューから 「セキュリティ」 > 「アクセスとデータ管理」 > 「APIの制御」 へ進みます。
  3. ページの下部にある 「ドメイン全体の委任」 のセクションを探し、「新しいクライアント ID を追加」 をクリックします。
  4. 以下の情報を入力します。
  5. **「承認」**をクリックして保存します。
これで、スクリプトが組織全体にわたるファイル検索と共有解除を行うための最高レベルの権限が確立されました。この設定が完了すれば、GASのトリガーが組織全体のファイルを管理できるようになります。
 

Google Appscriptエディタにて

 

A. Advanced Drive Service の有効化

スクリプトが組織のファイルにアクセスするために、より強力な Google Drive API を利用できるように設定します。
  1. Google Apps Scriptエディタを開きます。
  2. 左側メニューの 「サービス」 (Services / + アイコンがあるかもしれません) をクリックします。
  3. 一覧から 「Drive API」 を探し、クリックして有効化します。
  4. 識別子(Identifier)が Drive となっていることを確認してください。
  5.  

スクリプトエディタの「⏰ トリガー」メニューから、以下の設定で2つのトリガーを作成してください。

 

1. 検索トリガーの設定

設定項目
設定値
実行する関数を選択
findPubliclySharedFiles
イベントのソースを選択
時間主導型
時間ベースのトリガーのタイプを選択
日タイマー
時刻を選択
毎日午前 6時〜7時 (推奨)

 

2. 解除トリガーの設定

設定項目
設定値
実行する関数を選択
checkAndRemovePublicAccess
イベントのソースを選択
時間主導型
時間ベースのトリガーのタイプを選択
日タイマー
時刻を選択
毎日午前 0時〜1時 (推奨)
				
					// ==========================================================
// 🚨 設定項目: ここを必ず変更してください 🚨
// ==========================================================
// 1. 管理用スプレッドシートのURLを貼り付けてください
const SPREADSHEET_URL = '【ここに管理用スプレッドシートのURLを貼り付けてください】'; 
const SHEET_NAME = 'シート1'; // シート名が異なる場合は変更してください

// 2. 新規発見ファイルにデフォルトで設定する共有解除期限日(例:検索日から7日後)
const DEFAULT_EXPIRATION_DAYS = 7; 

// 3. 検索対象から除外したいGoogleドライブフォルダのIDを配列で指定します。
const EXCLUDED_FOLDER_IDS = []; // 例: ['除外フォルダID_1', '除外フォルダID_2', ...]

// 4. スクリプトの実行制限時間(秒)。GASの制限に引っかからないように設定します。
// 推奨: 280秒 (約4分半)
const TIME_LIMIT_SECONDS = 280;
// ==========================================================


/**
 * スプレッドシートの最終行に新しいデータを追記します。
 * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet - 対象シート
 * @param {Array<any>} rowData - 追記するデータ(配列)
 */
function appendRow(sheet, rowData) {
  // 最終チェック: sheetが有効なオブジェクトであること、rowDataが配列であることを確認
  if (!sheet || typeof sheet.appendRow !== 'function') {
    Logger.log('FATAL ERROR: sheetオブジェクトが無効です。');
    throw new Error("appendRowに無効なsheetオブジェクトが渡されました。");
  }
  if (!Array.isArray(rowData)) {
    Logger.log('FATAL ERROR: rowDataは配列ではありません。rowData:' + rowData);
    throw new Error("appendRowに無効なrowDataが渡されました。");
  }
  sheet.appendRow(rowData);
}

/**
 * ファイルが除外対象フォルダのいずれかに含まれているかを確認します。
 * @param {object} file - Drive APIから取得したファイルオブジェクト (parentsプロパティを持つ)
 * @returns {boolean} - 除外対象であれば true
 */
function isExcluded(file) {
  if (EXCLUDED_FOLDER_IDS.length === 0) {
    return false; // 除外リストが空ならチェック不要
  }
  
  // 親フォルダ情報がない場合や、親フォルダが配列でない場合は除外しない
  if (!file.parents || !Array.isArray(file.parents)) {
      return false;
  }
  
  // 親フォルダIDの配列をチェックし、一つでも除外リストに含まれていれば true
  return file.parents.some(parentId => EXCLUDED_FOLDER_IDS.includes(parentId));
}

/**
 * Google Drive API (v3対応) を使用して、組織全体で「リンクを知っている全員」に公開されている
 * ファイルを検索し、スプレッドシートに記録します。
 */
function findPubliclySharedFiles() {
  const startTime = new Date().getTime(); // 開始時刻を記録

  // 🚨 0. Advanced Drive Service (Drive) が有効化されているかチェック 🚨
  if (typeof Drive === 'undefined') {
    Logger.log('FATAL ERROR: Advanced Drive Service (Drive API) が有効化されていません。GASエディタの「サービス」から有効化してください。');
    return;
  }

  // 1. スプレッドシートを開く処理
  let ss;
  try {
    ss = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
  } catch (e) {
    Logger.log(`致命的なエラー: SPREADSHEET_URLの設定が不正か、アクセス権がありません。URL: ${SPREADSHEET_URL}。エラー: ${e.message}`);
    return;
  }
  
  // 2. シート名でシートを取得する処理
  const sheet = ss.getSheetByName(SHEET_NAME);
  
  if (!sheet) {
    Logger.log(`エラー: シート名が見つかりません。設定を確認してください: ${SHEET_NAME}。URLは正常: ${SPREADSHEET_URL}`);
    return;
  }
  
  // 既に記録されているファイルIDのセットを作成 (重複登録防止のため)
  const existingIds = new Set();
  const values = sheet.getDataRange().getValues();
  // 1行目のヘッダーを除外
  for (let i = 1; i < values.length; i++) {
    if (values[i][0]) {
      existingIds.add(values[i][0]);
    }
  }

  // Drive API検索クエリ: 
  // 🚨 クエリを空にし、APIにすべてのファイルメタデータを要求します。
  const searchQuery = ''; // 空のクエリで、全てのファイルを要求します。

  let pageToken = null;
  let newFilesFound = 0;
  let totalProcessed = 0; // 処理した総ファイル数
  const today = new Date();
  let timeOutOccurred = false;

  Logger.log('検索を開始します...');
  Logger.log(`検索クエリ: ${searchQuery === '' ? '(全ファイル/フォルダ)' : searchQuery}`);


  do {
    // ⏰ 時間制限チェック
    const currentTime = new Date().getTime();
    if ((currentTime - startTime) / 1000 > TIME_LIMIT_SECONDS) {
      Logger.log('⏳ 実行時間の制限に達しました。処理を中断します。');
      timeOutOccurred = true;
      break;
    }

    let response;
    try {
      // 🚨 Drive API v3 用のリクエスト設定
      const requestOptions = {
        q: searchQuery, // 空クエリ
        // マイドライブと共有ドライブの両方を対象とします。
        corpora: 'allDrives', 
        // v3では items ではなく files を使用。permissionsも明示的に取得が必要。
        fields: 'nextPageToken, files(id, name, mimeType, trashed, parents, permissions)', 
        // 組織全体検索のための必須オプション (v3)
        // 共有ドライブ内のファイル検索を有効化
        includeItemsFromAllDrives: true,
        supportsAllDrives: true,
        pageSize: 1000, // maxResults ではなく pageSize
        pageToken: pageToken
      };
      
      response = Drive.Files.list(requestOptions);
      
    } catch (e) {
      Logger.log(`Drive API検索エラー (詳細): ${e.message}. Stack: ${e.stack}`);
      // API検索中にエラーが発生した場合、ログに出力して処理を終了
      return;
    }

    // v3では response.files にデータが入ります (v2は response.items)
    const files = response.files || response.items;

    if (!files || files.length === 0) {
        break; 
    }
    
    // 取得したファイルのループ処理
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      totalProcessed++;

      // 🚨 フィルタリングの追加
      // ゴミ箱に入っている、またはフォルダはスキップします。
      if (file.trashed === true || file.mimeType === 'application/vnd.google-apps.folder') {
        continue;
      }
      
      const fileId = file.id;

      // 共有権限のチェック
      let anyonePermission = null;
      if (file.permissions) {
        // file.permissionsは Drive v3 のレスポンスに正しく含まれているはず
        anyonePermission = file.permissions.find(p => p.type === 'anyone');
      }

      // 公開共有されていないファイルはスキップ
      if (!anyonePermission) {
        continue; 
      }
      
      if (existingIds.has(fileId)) {
        continue; // 既に記録されている場合はスキップ
      }
      
      // フォルダ除外チェック
      if (isExcluded(file)) {
        Logger.log(`ファイル ${fileId} は除外対象のためスキップされました。`);
        continue;
      }
      
      let permissionType = anyonePermission.role.toUpperCase();
      
      const defaultExpirationDate = new Date();
      defaultExpirationDate.setDate(today.getDate() + DEFAULT_EXPIRATION_DAYS);
      
      const newRow = [
        fileId,
        Utilities.formatDate(defaultExpirationDate, Session.getScriptTimeZone(), 'yyyy/MM/dd'),
        '期限日設定待ち', 
        permissionType
      ];
      
      if (sheet && Array.isArray(newRow)) {
          appendRow(sheet, newRow);
          newFilesFound++;
      }
    }

    pageToken = response.nextPageToken;
    Logger.log(`現在 ${totalProcessed} 件のアイテムをスキャン完了... (新規発見: ${newFilesFound}件)`);

  } while (pageToken);
  
  // 結果の出力
  const timeMessage = timeOutOccurred ? ' (時間制限により途中終了)' : '';
  Logger.log('----------------------------------------------------');
  Logger.log(`処理結果: 共有ファイル検索が完了しました。${timeMessage}`);
  Logger.log(`総スキャン数: ${totalProcessed} 件`);
  Logger.log(`新規追加ファイル数: ${newFilesFound} 件`);
  Logger.log('----------------------------------------------------');
}

/**
 * スプレッドシートに記録されたファイルをチェックし、
 * 期限切れであれば「リンクを知っている全員」の共有を解除します。
 */
function checkAndRemovePublicAccess() {
  // 1. スプレッドシートを開く処理
  let ss;
  try {
    ss = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
  } catch (e) {
    Logger.log(`致命的なエラー: SPREADSHEET_URLの設定が不正か、アクセス権がありません。URL: ${SPREADSHEET_URL}。エラー: ${e.message}`);
    return;
  }
  
  // 2. シート名でシートを取得する処理
  const sheet = ss.getSheetByName(SHEET_NAME);
  
  if (!sheet) {
    Logger.log(`エラー: シート名が見つかりません。設定を確認してください: ${SHEET_NAME}。URLは正常: ${SPREADSHEET_URL}`);
    return;
  }
  
  const dataRange = sheet.getDataRange();
  const values = dataRange.getValues();
  const today = new Date();
  today.setHours(0, 0, 0, 0); // 日付のみを比較するための基準日
  
  Logger.log('----------------------------------------------------');
  Logger.log('期限切れ公開共有ファイルの解除処理を開始します。');
  let removedCount = 0;

  // ヘッダー行をスキップして、2行目から処理を開始
  for (let i = 1; i < values.length; i++) {
    const fileId = values[i][0]; // A列: ファイルID
    const expirationDate = values[i][1]; // B列: 解除期限日
    let status = values[i][2]; // C列: 処理状況
    const row = i + 1; // スプレッドシートの行番号

    // 既に「処理完了」になっている場合はスキップ
    if (status.toString().includes('処理完了')) {
      continue;
    }

    // ファイルIDが空、または期限日が有効な日付オブジェクトではない場合はスキップ
    if (!fileId || !(expirationDate instanceof Date)) {
      if (fileId && !expirationDate) {
        // 期限日がないがファイルIDはある場合
        sheet.getRange(row, 3).setValue('期限日未設定');
      }
      continue;
    }

    // 期限日のチェック (期限日 <= 今日)
    if (expirationDate.getTime() <= today.getTime()) {
      Logger.log(`期限切れのファイルを発見 (行 ${row}): ${fileId}`);
      
      try {
        // Drive API v3対応: パーミッション取得
        const permissions = Drive.Permissions.list(fileId).permissions || Drive.Permissions.list(fileId).items;
        let publicPermissionId = null;
        
        if (permissions) {
          // 2. 「anyone」タイプのパーミッションIDを特定
          permissions.forEach(permission => {
            if (permission.type === 'anyone') {
              publicPermissionId = permission.id;
            }
          });
        }
        
        let publicPermissionRemoved = false;

        if (publicPermissionId) {
          // 3. Drive APIを使用して、そのパーミッションを削除
          Drive.Permissions.remove(fileId, publicPermissionId);
          publicPermissionRemoved = true;
          removedCount++;
          Logger.log(`ファイルID: ${fileId} の共有を解除しました。パーミッションID: ${publicPermissionId}`);
        }
        
        if (publicPermissionRemoved) {
          sheet.getRange(row, 3).setValue('処理完了');
        } else {
          sheet.getRange(row, 3).setValue('処理完了 (リンクは既に解除済)');
          Logger.log(`ファイルID: ${fileId} の公開リンクは既に解除されていました。`);
        }

      } catch (e) {
        // ファイルが見つからない、アクセス権がないなどのエラー処理
        sheet.getRange(row, 3).setValue(`エラー: ${e.toString()}`);
        Logger.log(`ファイルID: ${fileId} の処理中にエラーが発生: ${e.toString()}`);
      }
    }
  }
  Logger.log(`処理完了: ${removedCount} 件のファイルの公開共有を解除しました。`);
  Logger.log('----------------------------------------------------');
}

/**
 * [参考] ファイルIDを取得しやすくするための補助関数
 */
function getFileId() {
  const ui = SpreadsheetApp.getUi();
  const result = ui.prompt(
      'ファイルIDの取得',
      '処理したいGoogleドライブファイルのURLを貼り付けてください。',
      ui.ButtonSet.OK_CANCEL);

  if (result.getSelectedButton() === ui.Button.OK) {
    const url = result.getResponseText();
    const match = url.match(/id=([^&]+)/) || url.match(/\/d\/([^/]+)/);
    
    if (match && match[1]) {
      ui.alert('抽出されたファイルID', match[1], ui.ButtonSet.OK);
    } else {
      ui.alert('エラー', '有効なGoogleドライブのURL形式ではありません。', ui.ButtonSet.OK);
    }