GAS『Service Error: Drive』の原因と対処法

GAS『Service Error: Drive』の原因と対処法

Google Apps ScriptからDrive(DriveAppまたは高度なDrive API)を操作した際に、サーバ側で処理できず失敗したときに出る汎用エラー。権限・共有ドライブのロール不備、ID/URLの取り違え、ゴミ箱・削除、MIME不一致、サイズ超過、レート/同時実行超過、一時障害などが主因。入口(誰が、どのAPIで、何を、どの対象に)を確定し、権限整備・入力検証・チャンク処理・指数バックオフ・排他制御で安定化させる。

エラーの意味と発生条件(まず把握)

・DriveApp/Drive高度サービスいずれでも発生する“サーバ側失敗”の総称。

・発生しやすい操作:大容量コピー/エクスポート、共有設定変更、共有ドライブ間移動、短時間の大量呼び出し、アクセス権のないID参照、ゴミ箱/削除済み参照。

・「たまに失敗→再試行で通る」は一時障害/レート境界、「毎回同じ場所で失敗」は前提不一致(権限/ID/MIME等)の疑いが濃い。

最短復旧フロー(5手で切り分け)

1) どのAPIか:DriveAppか、高度なDrive API(Drive.*)かを判定。

2) 実行主体は誰か:Webアプリの「自分として実行」or「アクセスユーザーとして実行」、トリガー作成者の権限を確認。

3) 対象ファイルは存在/MIME一致/可視か:IDとMIME、ゴミ箱/共有ドライブの所在、ロールを確認。

4) I/Oサイズ/頻度は適正か:大きい操作は分割し、指数バックオフで再試行。

5) 再現性:毎回か時々かで恒久対策(検証/権限整備)と暫定対策(リトライ/スロットリング)を分ける。

DriveAppと高度なDrive APIを区別し、スコープと認可を確認

・DriveApp:簡易API。権限は「このアプリがGoogleドライブへのアクセスを…」の同意で付与。

・Drive(高度サービス):Resources→Advanced Google servicesで有効化+Cloud側API有効化とOAuthスコープ。

・途中で権限を広げたら“新バージョンとしてデプロイ”して再同意が必要(Webアプリ/実行API)。

ID/URL/MIMEの取り違えを除去(入力検証の基本)

function extractFileId(urlOrId) {
  const m = String(urlOrId).match(/\/file\/d\/([a-zA-Z0-9-_]+)/) ||
            String(urlOrId).match(/[-\w]{25,}/);
  return m ? (m[1] || m[0]) : String(urlOrId).trim();
}

function assertSpreadsheet(fileId) {
  const mime = DriveApp.getFileById(fileId).getMimeType();
  if (mime !== MimeType.GOOGLE_SHEETS) throw new Error('対象はスプレッドシートではありません: ' + mime);
}

共有ドライブと権限(ロール不足は即失敗)

・共有ドライブでは「コンテンツ管理者/編集者」以上が編集可。閲覧者/コメント可は大半の書込み不可。

・他人所有/組織外移動/ゴミ箱はDriveApp.getFileById時点や書込み時に失敗。

function whoAmI() {
  return {
    effective: Session.getEffectiveUser().getEmail(),
    active: Session.getActiveUser().getEmail()
  };
}

レート/同時実行/サイズの壁(分割+バックオフで通す)

function withRetry(fn, tries = 5, base = 400, max = 8000) {
  let wait = base;
  for (let i = 0; i < tries; i++) {
    try { return fn(); }
    catch (e) {
      const m = String(e && e.message || '');
      const transient = /service|internal|backend|rate|quota|timeout|unavailable|exceeded/i.test(m);
      if (!transient || i === tries - 1) throw e;
      Utilities.sleep(wait + Math.floor(Math.random() * 250)); // ジッタ
      wait = Math.min(wait * 2, max);
    }
  }
}

function withDriveLock(work, waitMs = 20000) {
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(waitMs)) throw new Error('ロック取得に失敗');
  try { return work(); } finally { lock.releaseLock(); }
}

フォルダ列挙/ページングの正攻法(巨大フォルダでのタイムアウト回避)

function listFilesPaged(folderId, limit = 500) {
  const files = DriveApp.getFolderById(folderId).getFiles();
  const out = [];
  while (files.hasNext() && out.length < limit) {
    const f = withRetry(() => files.next());
    out.push([f.getName(), f.getId(), f.getUrl(), f.getSize()]);
  }
  return out; // 大量ならlimitを刻んで複数回に分ける
}

コピー/エクスポート/変換はサイズ依存(チャンクや分割を前提)

// シート→CSVエクスポート(大きい場合はシート分割や範囲指定を検討)
function exportCsv(fileId, gid) {
  const url = 'https://docs.google.com/spreadsheets/export?exportFormat=csv' +
              '&id=' + encodeURIComponent(fileId) +
              (gid != null ? '&gid=' + encodeURIComponent(gid) : '');
  const res = withRetry(() => UrlFetchApp.fetch(url, { muteHttpExceptions: true, followRedirects: true, timeout: 15000 }));
  if (res.getResponseCode() >= 400) throw new Error('Export failed ' + res.getResponseCode());
  return res.getBlob().setName('sheet.csv');
}

ゴミ箱/削除/所在の確認(“存在するが取れない”を特定)

function ensureAvailable(fileId) {
  try {
    const f = DriveApp.getFileById(fileId); // 不可視/削除でここが落ちる
    return { name: f.getName(), url: f.getUrl(), size: f.getSize() };
  } catch (e) {
    throw new Error('ファイルにアクセスできません(ID/共有/ゴミ箱/所有者を確認): ' + e.message);
  }
}

Webアプリ/トリガー:実行主体を合わせて失敗を減らす

// Webアプリ(自分として実行)で、オーナー権限でDrive操作する例
function doPost(e) {
  const id = extractFileId(e?.parameter?.id || '');
  const meta = ensureAvailable(id);
  return ContentService.createTextOutput(JSON.stringify({ ok: true, meta }))
    .setMimeType(ContentService.MimeType.JSON);
}

// 時間トリガー(インストール型)で作成者の認可を使う
function installHourly() {
  ScriptApp.newTrigger('batchDrive').timeBased().everyHours(1).create();
}

コピーや書込みは冪等化(再試行で重複しない)

function copyOnce(srcId, dstFolderId, nameKey) {
  const folder = DriveApp.getFolderById(dstFolderId);
  const it = folder.getFilesByName(nameKey);
  if (it.hasNext()) return it.next().getId(); // 既存なら再利用
  return withRetry(() => DriveApp.getFileById(srcId).makeCopy(nameKey, folder).getId());
}

NG→OK早見(クイック修正)

・NG:openByIdにURLを渡す/IDに空白混入 → OK:ID抽出と正規化で厳格に扱う。

・NG:共有ドライブ閲覧者でコピー/書込み → OK:コンテンツ管理者以上へロール昇格 or 「自分として実行」。

・NG:巨大フォルダを一括列挙/コピー → OK:ページング・件数制限・分割実行。

・NG:高頻度連続呼び出し → OK:指数バックオフ+軽いスリープ+排他ロック。

・NG:すべて単発関数で直列実行 → OK:withRetry/withDriveLockの共通化と集中管理。

・NG:毎回同じIDで失敗するが原因不明 → OK:MIME/所在/ゴミ箱/所有者をログに出す診断関数で特定。

チェックリスト(上から順に潰す)

・どのAPI(DriveApp/高度Drive)を使い、必要スコープ/再同意は済んでいるか。

・実行主体(Webアプリ/トリガー/ユーザー)は対象に十分な権限があるか。

・ID/URLの混同はないか、MIMEは期待通りか、ゴミ箱/共有ドライブの所在は正しいか。

・操作対象のサイズ/件数は分割しているか、レート/同時実行対策はあるか。

・一時障害に備えて指数バックオフとスロットリングを実装済みか。

・冪等化して再試行しても重複や二重処理にならないか。

・失敗時のログ(実行主体・ID・MIME・所在・レスポンスコード)を残して後追いできるか。