Google Driveのフォルダを一括作成する前に確認|GASのdry-run付き安全テンプレート

Google Driveのフォルダを一括作成する前に確認|GASのdry-run付き安全テンプレート

Gasおじ|GoogleアカウントをAIエージェント化

Gasおじ|GoogleアカウントをAIエージェント化

案件ごとに「01_受領」「02_作業中」「03_納品」のような同じフォルダを作る作業は、数が増えるほど入力ミスや作成場所の間違いが起きやすくなります。

この記事では、スプレッドシートに作成予定の階層を書き、最初はdry-run(作成せず確認だけ)で検査する無料範囲のGASを紹介します。重複、空欄、深すぎる階層を先に止め、実行結果はログへ残します。

このコードでできること・しないこと

  • `FolderPlan` シートに書いた相対パスを検査する
  • dry-runではGoogle Driveへフォルダを作らず、作成予定だけを記録する
  • 明示的に `DRY_RUN` を `false` にした時だけフォルダを作る
  • 同名フォルダが既に1つある場合は再利用し、二重作成を避ける
  • 作成・再利用・停止理由を `FolderCreateLog` シートへ残す
  • ファイルや既存フォルダの削除、移動、名前変更、共有設定変更は行わない
  • 時間主導トリガーは作らず、手動実行だけを前提にする

Drive内に同名フォルダが複数見つかった場合は、どれを使うべきか自動判断せず停止します。

準備するシート

スプレッドシートに `FolderPlan` シートを作り、1行目を次の2列にします。

  • A列: `relative_path`
  • B列: `enabled`

入力例は次の通りです。

  • `2026_秋キャンペーン/01_受領`、TRUE
  • `2026_秋キャンペーン/02_作業中`、TRUE
  • `2026_秋キャンペーン/03_納品`、TRUE
  • `共通資料/参考`、FALSE

スラッシュは階層の区切りとして扱います。1回の実行は最大50行、深さは最大4階層に制限しています。

コピーして使える最小コード

const FOLDER_PLAN_CONFIG = {
  ROOT_FOLDER_ID: 'ここに作成先フォルダIDを入れる',
  PLAN_SHEET: 'FolderPlan',
  LOG_SHEET: 'FolderCreateLog',
  DRY_RUN: true,
  MAX_ROWS: 50,
  MAX_DEPTH: 4
};

function runFolderPlanFree() {
  const config = FOLDER_PLAN_CONFIG;
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  const planSheet = spreadsheet.getSheetByName(config.PLAN_SHEET);
  if (!planSheet) {
    throw new Error('FolderPlan シートがありません。');
  }

  const plans = readAndValidateFolderPlans_(planSheet, config);
  const rootFolder = getRootFolderSafely_(config.ROOT_FOLDER_ID);
  const logRows = [];

  plans.forEach(function(plan) {
    try {
      if (config.DRY_RUN) {
        logRows.push([new Date(), 'DRY_RUN', plan.path, '作成予定。Driveは変更していません。']);
        return;
      }

      const result = ensureFolderPath_(rootFolder, plan.parts);
      logRows.push([new Date(), result.status, plan.path, result.message]);
    } catch (error) {
      logRows.push([new Date(), 'HOLD', plan.path, String(error.message || error)]);
    }
  });

  appendFolderLogs_(spreadsheet, config.LOG_SHEET, logRows);
  const holdCount = logRows.filter(function(row) { return row[1] === 'HOLD'; }).length;
  const summary = 'mode=' + (config.DRY_RUN ? 'DRY_RUN' : 'CREATE')
    + ', plans=' + plans.length
    + ', hold=' + holdCount;
  console.log(summary);
  spreadsheet.toast(summary, 'フォルダ計画', 8);
  return summary;
}

function readAndValidateFolderPlans_(sheet, config) {
  const lastRow = sheet.getLastRow();
  if (lastRow < 2) {
    throw new Error('作成予定がありません。');
  }

  const rowCount = lastRow - 1;
  if (rowCount > config.MAX_ROWS) {
    throw new Error('一度に処理できるのは' + config.MAX_ROWS + '行までです。');
  }

  const values = sheet.getRange(2, 1, rowCount, 2).getValues();
  const seen = {};
  const plans = [];

  values.forEach(function(row, index) {
    const enabledText = String(row[1]).trim().toUpperCase();
    if (![true, false].includes(row[1]) && enabledText !== 'TRUE' && enabledText !== 'FALSE') {
      throw new Error((index + 2) + '行目: enabled はTRUEまたはFALSEで指定してください。');
    }
    const enabled = row[1] === true || enabledText === 'TRUE';
    if (!enabled) return;

    const path = String(row[0] || '').trim();
    if (!path) {
      throw new Error((index + 2) + '行目: relative_path が空です。');
    }

    const parts = path.split('/').map(function(name) { return name.trim(); });
    if (parts.length > config.MAX_DEPTH) {
      throw new Error((index + 2) + '行目: 最大' + config.MAX_DEPTH + '階層までです。');
    }

    parts.forEach(function(name) {
      if (!name || name === '.' || name === '..') {
        throw new Error((index + 2) + '行目: 空または相対指定の階層名は使えません。');
      }
      if (name.length > 100 || /[\u0000-\u001f]/.test(name)) {
        throw new Error((index + 2) + '行目: 階層名が長すぎるか制御文字を含みます。');
      }
    });

    const normalized = parts.join('/');
    if (seen[normalized]) {
      throw new Error((index + 2) + '行目: 同じパスが重複しています。');
    }
    seen[normalized] = true;
    plans.push({ path: normalized, parts: parts });
  });

  if (!plans.length) {
    throw new Error('enabled がTRUEの作成予定がありません。');
  }
  return plans;
}

function getRootFolderSafely_(folderId) {
  const id = String(folderId || '').trim();
  if (!id || id === 'ここに作成先フォルダIDを入れる') {
    throw new Error('ROOT_FOLDER_IDを設定してください。');
  }
  try {
    return DriveApp.getFolderById(id);
  } catch (error) {
    throw new Error('作成先フォルダを確認できません。IDと権限を確認してください。');
  }
}

function ensureFolderPath_(rootFolder, parts) {
  let current = rootFolder;
  const actions = [];

  parts.forEach(function(name) {
    const matches = current.getFoldersByName(name);
    if (!matches.hasNext()) {
      current = current.createFolder(name);
      actions.push('作成:' + name);
      return;
    }

    const found = matches.next();
    if (matches.hasNext()) {
      throw new Error('同名フォルダが複数あります: ' + name);
    }
    current = found;
    actions.push('再利用:' + name);
  });

  return {
    status: actions.some(function(action) { return action.indexOf('作成:') === 0; }) ? 'CREATED' : 'EXISTS',
    message: actions.join(' / ')
  };
}

function appendFolderLogs_(spreadsheet, sheetName, rows) {
  let logSheet = spreadsheet.getSheetByName(sheetName);
  if (!logSheet) {
    logSheet = spreadsheet.insertSheet(sheetName);
    logSheet.appendRow(['executed_at', 'status', 'relative_path', 'message']);
  }
  if (rows.length) {
    logSheet.getRange(logSheet.getLastRow() + 1, 1, rows.length, 4).setValues(rows);
  }
}

実行手順

1. 対象スプレッドシートの「拡張機能」からApps Scriptを開き、コードを貼り付けます。

2. `ROOT_FOLDER_ID` に作成先フォルダのIDを入れます。IDはDriveでフォルダを開いた時のURL末尾にある文字列です。

3. `DRY_RUN: true` のまま `runFolderPlanFree` を手動実行します。

4. 初回の権限確認を読み、対象フォルダへアクセスしてよい場合だけ許可します。

5. `FolderCreateLog` の全行が `DRY_RUN` で、パスと件数が意図通りか確認します。

6. 問題がなければ `DRY_RUN: false` に変更し、もう一度だけ手動実行します。

7. ログの `CREATED`、`EXISTS`、`HOLD` を確認し、終わったら `DRY_RUN: true` に戻します。

少量のテスト用フォルダで確認してから実データへ適用してください。

失敗しやすい点と停止条件

  • フォルダIDが違う、または閲覧・作成権限がない場合は開始前に停止します。
  • TRUEの行が50件を超える場合は、分割して確認してください。
  • 同じ相対パスが2行ある場合は、Driveへ触る前に停止します。
  • 同じ親の中に同名フォルダが複数ある場合は、自動選択せずそのパスを `HOLD` にします。
  • 作成モードで途中停止した場合、それまでに新規作成した親フォルダは残ります。ログを確認し、同じ計画をdry-runへ戻してから再確認してください。
  • 実行時間やDriveの利用上限に達する場合があります。最初から大量の階層へ広げないでください。
  • 共有ドライブや組織アカウントでは、管理者設定によって作成できない場合があります。

この例は既存フォルダを再利用しますが、削除・移動・名前変更・共有設定変更は行いません。作成後の整理や権限設計は人が確認してください。

公開前チェックリスト

  • [ ] `DRY_RUN` が `true` になっている
  • [ ] 作成先フォルダIDを別の人にも確認してもらった
  • [ ] `FolderPlan` に空欄、重複、不要なTRUEがない
  • [ ] dry-runログの件数と階層が想定通り
  • [ ] 個人情報や機密のフォルダ名をログへ残してよいか確認した
  • [ ] 初回は少量のテスト用フォルダで実行する

まとめ

フォルダの一括作成は、コードを動かすことより「どこに何を作るか」を先に確認できることが重要です。計画表、dry-run、入力検証、ログの4点をそろえると、作成場所や階層名の間違いに気づきやすくなります。

複数フォルダやサブフォルダにある既存ファイルを一覧化し、更新日や種類で確認したい場合は、次のDriveFileList Proが関連します。

DriveFileList Pro|Google Driveのファイル一覧化GASテンプレ

GasおじのTips商品一覧も確認できます。

GasおじのTips商品一覧

※この記事は学習・検証用の例です。Google公式・Google公認の商品ではありません。Google Apps ScriptとDriveには権限設定、実行時間、各種利用上限があります。実データへ適用する前に、コピー環境と少量データで確認してください。


あなたも記事の投稿・販売を
始めてみませんか?

Tipsなら簡単に記事を販売できます!
登録無料で始められます!

Tipsなら、無料ですぐに記事の販売をはじめることができます Tipsの詳細はこちら
 

この記事のライター

Gasおじ|GoogleアカウントをAIエージェント化

おう、Gasおじだ。 AIツールとSaaSに散々突っ込んで遠回りした結果、最後に強かったのは「今あるGoogleアカウント」だった。 今ではAIエージェントであらゆるタスクを自動化して無双してるぞ。 GAS、スプレッドシート、フォーム、Gmail、AIエージェントを組み合わせれば、個人でも小さな会社でも、手作業だらけの業務はかなり自動化できる。 ここでは机上のAI論じゃなく、現場で動くテンプレ、コード、手順だけを置いていくぞ。 高いツールを増やす前に、まずお前さんのGoogleアカウントを仕事するエージェントに変えようじゃないか。

このライターが書いた他の記事

  • GASのtry-catch入門|元データを守るエラー処理とログの実装例

  • Gasおじ式 X投稿キュー自動実行GAS Pro|危ない時だけ止まる

    ¥2,980
    1 %獲得
    (29 円相当)
  • 個人クリエイター公開準備チェックGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)

関連のおすすめ記事

  • USBメモリで持ち運べるLinux Mint 22.1と永続的な書き込み領域の作成法【Rufus だけで作成する方法を初心者向けに徹底解説】

    ¥500
    1 %獲得
    (5 円相当)
    SmartSenior

    SmartSenior

  •  Let's noteにChromeOS 135を搭載!内蔵メモリ8GBのChromebookを構築 Windows10との共存環境

    ¥500
    1 %獲得
    (5 円相当)
    SmartSenior

    SmartSenior

  • AI時代のX運用 基礎設計マニュアル|1日15分で回すアカウント運用の土台づくり

    ¥980
    1 %獲得
    (9 円相当)
    OfficeBuddy

    OfficeBuddy