【要検証】共有ドライブの期限付きファイル外部共有ツール(Google Apps Script)

組織外部にファイル共有をする際のPPAP対策として、Googleドライブを使えないかと検討すると以下の問題が出てきます。

  • 外部共有用の共有ドライブでは共有期限が設定できない。

  • Googleアカウントがない共有先にもビジター共有は可能だが、共有状態が放置される可能性を考えると、ビジター共有を禁止する方針になり、共有相手が制限される→ツールとして使えなくなる。

  • ユーザーに共有権限を付与すると、ルール通り運用されないため、権限付与は情シスになりがち。

上記問題対応できるツールが作れるのではと考えて、ChatGPTと相談しながら、以下機能のGoogle Apps Scriptを作成してみました。
ただし、個人契約の試用期間テナントでの動作検証しか行っていないため、検証可能環境で検証後、使用するようにしてください。

機能概要

以下のプロセスで、ファイルを期限付きで共有します。ユーザーには自分が共有したファイルの共有ドライブの閲覧権限のみ付与されます。

  • 指定したグループアドレスのメンバーのみ動作するGoogleフォーム

  • フォーム実行時トリガーは特権管理者など、外部共有用の共有ドライブの管理権限を持つユーザーで設定

  • Googleフォームで共有期間、共有先メールアドレス、添付ファイルを送信すると、添付ファイルを外部共有可能な共有ドライブに保存。

  • フォーム送信者、共有先メールアドレスに閲覧権限で添付ファイルを共有する

  • 管理者、フォーム申請者にメールで共有結果を送信

  • 共有期限の過ぎたファイルは、定期実行される関数により自動削除&シートに削除済記入

  • メールで届いたフォルダIDを申請することで、申請者は共有申請したファイルを削除&シートに削除済記入。

  • 動作状況をWebhookで送信

  • 「アップロード済みファイルの合計サイズの上限」について
    最大値1TBまでで設定可能。未検証ですが、アップロードフォルダ内のファイルを移動しているため、上限は無視できる可能性があります。

使い方

  • 外部共有用の共有ドライブの管理者権限を特権管理権限アカウントにつける。

  • そのアカウントでこちらのシートマイドライブにコピーして開く。※ファイル添付はマイドライブでないと使えないため。

  • 開くと「ファイルのアップロード先のフォルダが見つかりません」表示されるので、復元を押下

  • 回答タブでスプレッドシートリンク→新しいスプレッドシートを作成

  • 以下順番でスプレッドシートの項目ができているか確認

    • タイムスタンプ、メールアドレス、申請種別、共有期間、共有先メールアドレス(改行で複数指定)、ファイルのアップロード、削除する共有フォルダID

  • 「アップロード済みファイルの合計サイズの上限」を設定(最大1TB)

    • フォームの「ファイルのアップロード」の項目でも1つあたりのファイルの最大サイズなど設定可能。

  • フォームの編集メニューのスクリプトエディタを開き、ファイル共有.gsの以下変数を設定

    • destinationFolderId、groupEmails、WebhookURL

  • 共有期限切れフォルダ削除.gsのWebhookURLも設定

  • shareFilesWithExpiration関数のトリガーをフォーム送信時で設定(実行権限も承認)

  • checkShareExpiration関数のトリガー1日1回以上で設定

Google Apps Scriptコード

ファイル共有

function shareFilesWithExpiration(e) {
  const destinationFolderId = ''; // 外部共有ファイル用共有ドライブのフォルダID
  const groupEmails = ''; // このコードの実行権限をつけるグループアドレスをカンマ区切りで指定。空欄の場合チェックなし。
  const WebhookURL = ""; // SlackかGoogleChatのWebhook URLを設定。空欄の場合送信なし

  let itemResponse;
  let uploadedFileIds;
  let respondentEmail;
  const respons_ss = SpreadsheetApp.openById(FormApp.getActiveForm().getDestinationId());


  const responsesLength = FormApp.getActiveForm().getResponses().length;
  const formResponse = (e !== undefined) ? e.response : FormApp.getActiveForm().getResponses()[responsesLength - 1]; 
  
  // 回答のオブジェクトを取得
  const itemResponses = formResponse.getItemResponses();

  // フォームの1つ目の回答を取得
  const firstAnswer = itemResponses[0].getResponse();

  // フォームの1つ目の回答が「共有削除申請」の場合
  if (firstAnswer === "共有削除申請") {
    deleteFolderRequest(formResponse, WebhookURL); // deleteFolderRequest 関数を実行
    return; // 動作を終了
  }

  // ファイルのアップロードの回答結果と送信者のメールアドレスを取得する
  respondentEmail = formResponse.getRespondentEmail(); 
  itemResponse = itemResponses[3];  // 
  uploadedFileIds = itemResponse.getResponse();

  // 送信者のメールアドレスから@マークまでの部分を取得し、それを含むフォルダ名を作成
  const atIndex = respondentEmail.indexOf('@');
  const folderName = atIndex !== -1 ? respondentEmail.substring(0, atIndex) : respondentEmail;

  // 外部共有権限の確認
  if (checkMembershipInGroups(groupEmails, respondentEmail)) {
    const destinationFolder = DriveApp.getFolderById(destinationFolderId); // 移動先のフォルダ

    // 新しいフォルダを作成し、そのフォルダ内にアップロードされたファイルを移動する
    const newSubFolder = destinationFolder.createFolder(folderName + '_' + responsesLength);
    const fileURLs = []; // 移動後のファイルのURLを保持する配列

    for (let i = 0; i < uploadedFileIds.length; i++) {
      const uploadedFile = DriveApp.getFileById(uploadedFileIds[i]);
      const movedFile = uploadedFile.moveTo(newSubFolder);
      const fileURL = movedFile.getUrl(); // 移動後のファイルのURLを取得
      fileURLs.push(fileURL); // URLを配列に追加
    }

    // 新しいフォルダに対して閲覧権限を追加
    const emailList = itemResponses[2].getResponse().split(/\s+/); // 改行文字や空白文字で分割する
    for (let i = 0; i < emailList.length; i++) {
      const email = emailList[i].trim();
      if (email !== '') {
        newSubFolder.addViewer(email);
      }
    }    
    newSubFolder.addViewer(respondentEmail);


    // 共有期限の日付を計算
    const sharedExpiryText = itemResponses[1].getResponse(); // 共有期限をフォームの回答2つ目から取得
    const expiryDate = calculateExpiryDate(sharedExpiryText); // 共有期限の日付を計算

    // 共有期限の日付を "yyyy/MM/dd" の形式で表示
    const formattedExpiryDate = formatDate(expiryDate);

    // メッセージ内容を作成
    let message = `以下の通りファイルを共有しました。共有先アドレスにシステムからメールが送信されています。\n\n■共有先アドレス:\n${emailList}\n\n■共有期限:\n${formattedExpiryDate}\n\n■外部共有用フォルダURL:\n${newSubFolder.getUrl()}\n\n■外部共有用フォルダID:\n${newSubFolder.getId()}`;
    // メールを送信
    const subject = "ファイル共有通知";
    const recipient = respondentEmail;
    sendEmailAndWebhook(recipient, subject, message,WebhookURL);

    // スプレッドシートに共有期限とフォルダIDを記入
    const respons_ssid=FormApp.getActiveForm().getDestinationId()
    const responseSheet = respons_ss.getSheetByName("フォームの回答 1");
    const lastRow = responseSheet.getLastRow();
    responseSheet.getRange('H' + lastRow).setValue(expiryDate);
    responseSheet.getRange('I' + lastRow).setValue(newSubFolder.getId());
  } else {
    // グループに含まれない場合のエラーメッセージ
    const errorMessage = "権限のあるグループへの追加をシステム管理者に依頼してください。";
    sendEmailAndWebhook(respondentEmail, "外部共有権限が確認できませんでした。", errorMessage,WebhookURL);
  }

}

// 共有期限のテキストから共有期限の日付を計算する関数
function calculateExpiryDate(sharedExpiryText) {
  const currentDate = new Date(); // 現在の日付
  const expiryDate = new Date(currentDate); // 共有期限の日付

  if (sharedExpiryText === '3日') {
    expiryDate.setDate(expiryDate.getDate() + 3); // 3日後
  } else if (sharedExpiryText === '7日') {
    expiryDate.setDate(expiryDate.getDate() + 7); // 7日後
  } else if (sharedExpiryText === '14日') {
    expiryDate.setDate(expiryDate.getDate() + 14); // 14日後
  }

  return expiryDate;
}

function checkMembershipInGroups(groupEmails, targetEmail) {
  // groupEmailsが空の場合は真を返す
  if (!groupEmails || groupEmails.trim() === "") {
    return true;
  }

  // カンマで区切られたテキストを配列に変換
  var groupEmailList = groupEmails.split(",");

  for (var i = 0; i < groupEmailList.length; i++) {
    var groupEmail = groupEmailList[i].trim();
    var members = AdminDirectory.Members.list(groupEmail).members;

    if (members) {
      for (var j = 0; j < members.length; j++) {
        var member = members[j];
        if (member.email == targetEmail) {
          return true; // メールアドレスが見つかったら真を返す
        }
      }
    }
  }

  return false; // メールアドレスが見つからなかったら偽を返す
}





function formatDate(date) {
  const year = date.getFullYear();
  const month = (date.getMonth() + 1).toString().padStart(2, '0');
  const day = date.getDate().toString().padStart(2, '0');
  return `${year}/${month}/${day}`;
}

function sendEmailAndWebhook(mailAddress, subject, body, WebhookURL) {
    try {
        // メール送信
        MailApp.sendEmail(mailAddress, subject, body);
    } catch (e) {
        // エラーがあればコンソールに出力
        console.error(e);
        // Webhookにエラーを送信
        if (WebhookURL !== null) {
            sendWebhookMessage(WebhookURL, `エラーが発生しました: ${e}`);
        }
    }
    // Webhookにメール内容を送信
    if (WebhookURL !== null) {
        sendWebhookMessage(WebhookURL, `メール内容: ${subject}\n\n${body}`);
    }
}

function sendWebhookMessage(webhookURL, message) {
    const payload = {
        text: message
    };
    const options = {
        method: "post",
        contentType: "application/json",
        payload: JSON.stringify(payload)
    };
    UrlFetchApp.fetch(webhookURL, options);
}

フォーム送信者にわかりやすい通知をするのが大事そうかと思い、メールで通知を送るようにしています。
共有先には、共有ドライブのシステムによるメールが飛ぶだけなので、共有期限含めて通知するメールは飛ばせるといいかもしれませんね。
より長期で、編集可能な権限をつけるフォームへのアレンジも可能かと思います。

共有期限切れフォルダ削除

function checkShareExpiration() {
  const WebhookURL = ""; // SlackかGoogleChatのWebhook URLを設定。空欄の場合送信なし

  const respons_ss = SpreadsheetApp.openById(FormApp.getActiveForm().getDestinationId());
  const sheet = respons_ss.getSheetByName('フォームの回答 1');
  const lastRow = sheet.getLastRow();
  const currentDate = new Date();
  const executingUserEmail = Session.getActiveUser().getEmail();



  for (let i = 2; i <= lastRow; i++) {
    const folderID = sheet.getRange(`I${i}`).getValue(); // フォルダのIDを取得
    const shareDeadline = new Date(sheet.getRange(`H${i}`).getValue());
    const shareEmails = sheet.getRange(`E${i}`).getValue().split('\n');
    const ownerEmails = sheet.getRange(`B${i}`).getValue();

    if (shareDeadline < currentDate) {
      // 共有期日が過ぎた場合
      const folder = DriveApp.getFolderById(folderID);

      // 共有フォルダを削除
      folder.setTrashed(true);

      // 削除済みをマーク
      sheet.getRange(`H${i}`).setValue('削除済'); // 共有期限欄に削除済を記入

      // 所有者と実行ユーザーにメールを送信
      const folderName = DriveApp.getFolderById(folderID).getName(); // フォルダ名を取得
      const subject = `共有フォルダ削除(${shareEmails.join(', ')})`;
      const body = `共有期限を過ぎたため、以下ユーザーに共有した共有フォルダを削除しました。\n\n` +
                   `フォルダ名:${folderName}\n` +
                   `フォルダID:${folderID}\n\n` +
                   `共有ユーザー:\n${shareEmails.join('\n')}`;
      sendEmailAndWebhook(ownerEmails, subject, body,WebhookURL);
      sendEmailAndWebhook(executingUserEmails, subject, body,WebhookURL);      
    }
  }
}

GASは実行失敗することもよくあるので、一日2回以上はトリガー実行されるようにしておくといいかと思います。


削除申請

function deleteFolderRequest(formResponse, WebhookURL) {
  const respons_ss = SpreadsheetApp.openById(FormApp.getActiveForm().getDestinationId());
  const sheet = respons_ss.getSheetByName('フォームの回答 1');
  const lastRow = sheet.getLastRow();
  const currentDate = new Date();
  const executingUserEmail = Session.getActiveUser().getEmail();

  // 回答のオブジェクトを取得
  const itemResponses = formResponse.getItemResponses();

  // 削除する共有フォルダID、申請者アドレスをフォーム結果から取得
  const folderID = itemResponses[1].getResponse(); // 削除申請の場合の1番目の回答から取得
  const respondentEmail = formResponse.getRespondentEmail();

  // フォルダIDを検索して行を特定し、該当行のownerEmailと共有先メールアドレスを取得
  let ownerEmail = "";
  let sharedEmail = "";
  let rowWithFolderID = -1; // フォルダIDが見つかった行のインデックスを保持する変数
  const dataRange = sheet.getRange("I:I"); // I列の範囲を指定
  const folderIDValues = dataRange.getValues();
  for (let i = 0; i < folderIDValues.length; i++) {
    if (folderIDValues[i][0] === folderID) {
      ownerEmail = sheet.getRange("B" + (i + 1)).getValue(); // ownerEmailはB列にあるため、該当行のB列から取得
      sharedEmail = sheet.getRange("E" + (i + 1)).getValue(); // 共有先メールアドレスはE列にあるため、該当行のE列から取得
      rowWithFolderID = i + 1; // インデックスではなく行番号を保持するため、+1して格納
      break;
    }
  }

  // ownerEmailとrespondentEmailが一致しない場合の処理
  if (ownerEmail !== respondentEmail) {
    // 権限がない旨のメッセージを送信
    const subject = "権限がありません";
    const body = "削除を実行する権限がありません。";
    sendEmailAndWebhook(respondentEmail + "," + executingUserEmail, subject, body, WebhookURL);
    return; // 処理を終了
  }

  // フォルダが見つかった場合の処理
  if (rowWithFolderID !== -1) {
    // フォルダを削除
    const folderToDelete = DriveApp.getFolderById(folderID);
    folderToDelete.setTrashed(true);

    // H列に"削除済"を記入
    sheet.getRange("H" + rowWithFolderID).setValue("削除済");

    // 削除したことをrespondentEmailとトリガー実行アカウントに通知
    const subject = "共有フォルダを削除しました";
    const body = "以下の共有フォルダを削除しました。\n\n" +
      "■外部共有用フォルダURL\n" +
      folderToDelete.getUrl() + "\n" +
      "■外部共有用フォルダID\n" +
      folderID +
      "\n" +
      "■共有先メールアドレス: \n" + sharedEmail;
    sendEmailAndWebhook(respondentEmail + "," + executingUserEmail, subject, body, WebhookURL);
  } else {
    // フォルダが見つからなかった場合、削除しなかった理由をrespondentEmailに通知
    const subject = "共有フォルダが見つかりませんでした";
    const body = "以下のフォルダIDに対応する共有フォルダが見つかりませんでした。\n\n" +
      "フォルダID: " + folderID + "\n\n" +
      "詳細を確認してください。";
    sendEmailAndWebhook(respondentEmail + "," + executingUserEmail, subject, body, WebhookURL);
  }
  
}

誤ったファイルや、誤った共有先に送った場合はこちらで削除を。

この記事が気に入ったらサポートをしてみませんか?