Driveファイルの版番号が混ざる前に|GASで名前と更新日をdry-run監査

Driveファイルの版番号が混ざる前に|GASで名前と更新日をdry-run監査

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

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

「最終版」「修正版」「v2」が同じフォルダに増え、どれを残すべきか分からない。そんな状態で名前変更や削除を始めると、必要な版まで触ってしまいます。

この記事では、Google Driveのファイル一覧をまとめたシートを読み取り、版番号のない行、同じ版番号の重複、更新日時と版番号の逆転をGASで確認します。元シート、Drive上のファイル、共有権限は変更しません。

先に知っておきたいこと

  • できること: ファイル名の末尾にある v1、v1.2、v03 などを読み取り、行番号と問題種別だけをログへ出します。
  • できないこと: 正しい最新版の自動決定、ファイル名の変更、古い版の削除、フォルダ移動は行いません。
  • 必要なもの: Googleスプレッドシートと、その編集権限です。Driveの変更権限は不要です。
  • 安全設計: 初期値は dry-run 固定です。ファイル名、ファイルID、共有先メールアドレスはログへ出しません。

版番号は、ファイル名の末尾に「_v2」「-v2.1」「 v03」のように付いている前提です。拡張子がある場合も確認できます。

準備するシート

シート名を FileInventory にして、1行目へ次の3列を用意します。

  • file_id: ファイルを区別するためのID。空欄はHOLDになります。
  • file_name: 例「企画書_v2.1.pdf」。この値はログへ出しません。
  • updated_at: 更新日時。スプレッドシートの日時形式で入力します。

まずはコピーしたスプレッドシートへ5〜10件だけ入れて試してください。元の台帳を直接使う必要はありません。

この監査で見つけるもの

  • VERSION_MISSING: ファイル名から版番号を読み取れない
  • DUPLICATE_VERSION: 同じ資料名と版番号の組み合わせが複数ある
  • UPDATE_ORDER_MISMATCH: 大きい版番号の更新日時が、小さい版番号より古い
  • INPUT_INVALID: 必須列の空欄、日時不正、行数上限超過

監査結果は行番号と問題種別だけをログへ出します。ファイル名そのものをログへ残さないため、共有ログから案件名や顧客名が見えるリスクを抑えられます。

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

function auditDriveVersionNamesFree() {
  const config = {
    sheetName: 'FileInventory',
    dryRun: true,
    maxRows: 500,
    expectedHeaders: ['file_id', 'file_name', 'updated_at'],
  };

  if (config.dryRun !== true) {
    throw new Error('安全のため dryRun: true のときだけ実行できます。');
  }

  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = spreadsheet.getSheetByName(config.sheetName);
  if (!sheet) {
    throw new Error('FileInventory シートが見つかりません。');
  }

  const values = sheet.getDataRange().getValues();
  if (values.length < 2) {
    throw new Error('見出し行の下に確認用データを入れてください。');
  }

  const headers = values[0].map(function (value) {
    return String(value).trim().toLowerCase();
  });
  const missingHeaders = config.expectedHeaders.filter(function (header) {
    return headers.indexOf(header) === -1;
  });
  if (missingHeaders.length > 0) {
    throw new Error('不足している列: ' + missingHeaders.join(', '));
  }

  const body = values.slice(1);
  if (body.length > config.maxRows) {
    throw new Error('一度に確認できるのは ' + config.maxRows + ' 行までです。');
  }

  const column = {};
  config.expectedHeaders.forEach(function (header) {
    column[header] = headers.indexOf(header);
  });

  const issues = [];
  const versionGroups = {};

  body.forEach(function (row, offset) {
    const rowNumber = offset + 2;
    const fileId = String(row[column.file_id] || '').trim();
    const fileName = String(row[column.file_name] || '').trim();
    const updatedAt = toValidDate_(row[column.updated_at]);

    if (!fileId || !fileName || !updatedAt) {
      issues.push({ row: rowNumber, type: 'INPUT_INVALID' });
      return;
    }

    const parsed = parseVersionName_(fileName);
    if (!parsed) {
      issues.push({ row: rowNumber, type: 'VERSION_MISSING' });
      return;
    }

    const groupKey = normalizeStem_(parsed.stem);
    if (!versionGroups[groupKey]) {
      versionGroups[groupKey] = [];
    }
    versionGroups[groupKey].push({
      row: rowNumber,
      version: parsed.version,
      versionText: parsed.versionText,
      updatedAt: updatedAt,
    });
  });

  Object.keys(versionGroups).forEach(function (groupKey) {
    const records = versionGroups[groupKey];
    const seenVersions = {};

    records.forEach(function (record) {
      if (!seenVersions[record.versionText]) {
        seenVersions[record.versionText] = [];
      }
      seenVersions[record.versionText].push(record.row);
    });

    Object.keys(seenVersions).forEach(function (versionText) {
      const rows = seenVersions[versionText];
      if (rows.length > 1) {
        rows.forEach(function (rowNumber) {
          issues.push({ row: rowNumber, type: 'DUPLICATE_VERSION' });
        });
      }
    });

    const byVersion = records.slice().sort(function (a, b) {
      return a.version - b.version;
    });
    for (let index = 1; index < byVersion.length; index += 1) {
      const olderVersion = byVersion[index - 1];
      const newerVersion = byVersion[index];
      if (newerVersion.updatedAt.getTime() < olderVersion.updatedAt.getTime()) {
        issues.push({ row: newerVersion.row, type: 'UPDATE_ORDER_MISMATCH' });
      }
    }
  });

  const uniqueIssues = deduplicateIssues_(issues);
  const counts = uniqueIssues.reduce(function (result, issue) {
    result[issue.type] = (result[issue.type] || 0) + 1;
    return result;
  }, {});

  console.log(JSON.stringify({
    dryRun: true,
    checkedRows: body.length,
    issueCount: uniqueIssues.length,
    counts: counts,
    issues: uniqueIssues,
    privacy: 'ファイル名・ファイルID・メールアドレスはログへ出していません',
  }));
}

function parseVersionName_(fileName) {
  const match = fileName.match(/^(.*?)[_\-\s]+v(\d+)(?:\.(\d+))?(\.[^.]+)?$/i);
  if (!match) return null;

  const major = Number(match[2]);
  const minor = Number(match[3] || 0);
  if (!Number.isInteger(major) || !Number.isInteger(minor)) return null;

  return {
    stem: match[1],
    version: major * 1000 + minor,
    versionText: major + '.' + minor,
  };
}

function normalizeStem_(stem) {
  return String(stem)
    .normalize('NFKC')
    .trim()
    .replace(/\s+/g, ' ')
    .toLowerCase();
}

function toValidDate_(value) {
  const date = value instanceof Date ? value : new Date(value);
  return Number.isNaN(date.getTime()) ? null : date;
}

function deduplicateIssues_(issues) {
  const seen = {};
  return issues.filter(function (issue) {
    const key = issue.row + ':' + issue.type;
    if (seen[key]) return false;
    seen[key] = true;
    return true;
  }).sort(function (a, b) {
    return a.row - b.row || a.type.localeCompare(b.type);
  });
}

実行手順

  1. 確認対象とは別のスプレッドシートを作り、FileInventory シートと3列を用意します。
  2. 5〜10件のサンプルを入力します。版番号あり、版番号なし、同じ版番号を混ぜると挙動を確認しやすくなります。
  3. 拡張機能から Apps Script を開き、コードを貼り付けます。
  4. auditDriveVersionNamesFree を選んで実行します。
  5. 実行ログの issueCount、counts、issues を確認します。
  6. 問題行は元データ側で人が確認し、必要なファイルだけを手動で整理します。

初回はスプレッドシートを読み取る権限確認が表示されます。Drive API、Gmail、外部サービスへの接続はありません。

ログの読み方

たとえば issues に row 6、type DUPLICATE_VERSION と出たら、FileInventory の6行目を確認します。ログだけを見て削除や名前変更を決めず、同じ資料名の前後版と内容を開いて比較してください。

issueCount が0でも、版番号の付け方がチーム内で統一されていることまでは保証できません。v2、改訂版、最終版が混在する場合は、先に命名ルールを決める必要があります。

失敗しやすい点と制限

  • 「最終版」「確定」「修正版」だけの名前は版番号として扱いません。
  • v1.2.3 のような3段階の番号には未対応です。
  • 同じ資料かどうかは、版番号を除いたファイル名で判断します。表記ゆれが大きい場合は別グループになります。
  • 更新日時は編集や再アップロードでも変わるため、版の正しさを断定する材料にはできません。
  • 500行を超える場合は、対象フォルダや期間を分けてください。
  • 共有ドライブやショートカット固有の権限状態は確認しません。
  • 自動削除、自動移動、自動リネームは実装していません。

公開前チェックリスト

  • 元のDriveファイルや台帳を変更していない
  • ファイル名、ID、共有先メールアドレスをログへ出していない
  • 問題行を人が確認してから整理する手順になっている
  • dryRun が true のままになっている
  • 少量のコピー環境で先に試している

まとめ

版管理で最初に必要なのは、自動削除ではなく「どの行を人が確認するか」を安全に絞ることです。このコードは、版番号の不足・重複・更新順の逆転を、元データ非変更で確認します。

Drive内のファイル一覧を作るところから効率化したい場合は、次の有料Tipsが関連します。

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

GasおじのTips商品一覧を見る

※学習・検証用の例です。実データへ適用する前に、コピー環境と少量データで確認してください。Google公式・Google公認の商品ではありません。


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

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

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

この記事の販売者

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

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

この販売者が書いた他の記事

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)
  • Gmailの自動ラベルが競合する前に確認|GASで仕分けルールをdry-run監査

  • 毎朝のToDoを区分・優先度・期限つきでまとめる Morning ToDo Mail Pro

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

関連のおすすめ記事

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

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

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

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

    SmartSenior

  • 毎朝のToDoを区分・優先度・期限つきでまとめる Morning ToDo Mail Pro

    ¥2,980
    1 %獲得
    (29 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

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