ken_nogi/PleasanterSystem/dbbackup_XXX/SPEC.md
Kenichiro NOGI ed33892f08 chore: 作業中の変更を整理しコミット(複数プロジェクト分)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 11:09:50 +09:00

21 KiB

PostgreSQL → LINE WORKS 共有ドライブ 自動バックアップ (Node.js)

Context

現行の backup.shpg_dump でローカルにダンプを作成するところまでは動いているが、転送処理(sftp.sh 経由)はコメントアウトされて使われていない。転送先を独自VPN上のSFTPサーバーから LINE WORKS ドライブの共有ドライブに変更したいというのが今回の要望。

認証方式について: 当初 get-token.js / 姉妹プロジェクトの lineworks-anythingllm.js(LINE WORKS 掲示板連携)と同じ「Service AccountのJWT自己署名 → OAuth2トークン取得」を共有ドライブAPIにも流用する想定だったが、実装・検証の結果、Drive/共有ドライブAPIはService Account認証(JWT)に対応しておらず、User Account認証(OAuth 2.0 認可コードフロー)が必須であることが判明した(公式ドキュメント https://developers.worksmobile.com/jp/docs/drive に明記、かつ実機検証でも「スコープ不足」とは異なるエラーNot allowed apiを確認)。そのため:

  • 掲示板API等、既存のService Account認証で動くもの → 従来通り get-token.js / lib/lineworksAuth.js
  • Drive/共有ドライブAPI(backup.jsが使うもの) → 新設の lib/lineworksUserAuth.js(User Account認証、OAuth認可コードフロー)

を使い分ける。User Account認証は実ユーザー(kenichiro.nogi@next-hd.co.jp)による一度きりのブラウザ認可が必要なため、lineworks-authorize.js という一回限りのセットアップスクリプトを用意し、そこで取得した refresh_token.env に保存、以降 backup.js は無人で refresh_token から access_token を再発行する。

保存先は 共有ドライブ「システムバックアップ」 > Pleasanter_DB_Backup > 日付フォルダ(YYYYMMDD) で、フォルダが無ければ都度作成。ファイル名側に秒単位まで含むタイムスタンプを付与し、同日複数回実行しても上書きされないようにする。アップロード後は日付フォルダの個数を数え、保持回数(既定100・.envで変更可)を超えた古いフォルダをLINE WORKS側から削除してローテーションする。

さらに、バックアップ開始・終了、アップロード開始・終了、LINE WORKSログイン(トークン取得)からディレクトリ取得・作成までの各過程を秒単位のタイムスタンプ付きで詳細ログに記録する。ログは同じ共有ドライブの Pleasanter_DB_Backup/logs/ 配下に月1回ローテーション(月ごとに1ファイル)で保存し、実行のたびに上書きアップロードする。通常の経過はINFO、リトライ等の異常系はWARN/ERRORで記録する。

sftp.sh は「以前の手段」なので使用しない・参照もしない。

任意にパイプライン全体(pg_dump→分割→アップロード→ローテーション→ログ)を試せるよう、本番DBに触れずに使えるテスト用DBの作成・削除コマンドも backup.js のサブコマンドとして用意する。

参照した公式ドキュメント(エンドポイント確定分)

操作 Method URL
共有ドライブ一覧 GET /v1.0/sharedrives{ sharedrives: [{ sharedriveId, name, ... }] }
ルート直下 一覧 GET /v1.0/sharedrives/{sharedriveId}/files{ files: [...], responseMetaData: { nextCursor } }
フォルダ配下 一覧 GET /v1.0/sharedrives/{sharedriveId}/files/{fileId}/children (同形式)
ルート直下 フォルダ作成 POST /v1.0/sharedrives/{sharedriveId}/files/createfolder body { fileName } → 201 { fileId, fileType:"FOLDER", ... }
フォルダ配下 フォルダ作成 POST /v1.0/sharedrives/{sharedriveId}/files/{fileId}/createfolder body { fileName }
アップロードURL発行(段階1) POST /v1.0/sharedrives/{sharedriveId}/files/{fileId} body { fileName, fileSize, modifiedTime, overwrite, resume, suffixOnDuplicate }{ uploadUrl, offset }
実バイト送信(段階2) POST uploadUrl (別ホスト apis-storage.worksmobile.com) — multipart/form-data, フィールド名 Filedata, Authorization: Bearer 同梱 → 201 { fileId, fileName, fileSize }
削除(ファイル/フォルダ) DELETE /v1.0/sharedrives/{sharedriveId}/files/{fileId} → 204

必要スコープ: file(読み書き全般)。.envLW_SCOPEget-token.js(Service Account認証)専用に残し、バックアップ用(User Account認証)には別変数 LW_SCOPE_DRIVE を新設して混同しないようにする。

認可コードフロー / リフレッシュ関連エンドポイント

操作 Method URL
認可リクエスト GET(ブラウザ) https://auth.worksmobile.com/oauth2/v2.0/authorize?client_id=...&redirect_uri=...&response_type=code&scope=file&state=...
認可コード→トークン交換 POST https://auth.worksmobile.com/oauth2/v2.0/token (grant_type=authorization_code)
リフレッシュ POST 同上 (grant_type=refresh_token)

Refresh Token Rotation が ON のため、リフレッシュのたびに新しい refresh_token が発行され古い方は無効化される。取得の都度 .envLW_USER_REFRESH_TOKEN を書き換えて保存する。

新規ファイル構成 (dbbackup/ 配下)

dbbackup/
  backup.js                 … エントリポイント (node backup.js)
  lineworks-authorize.js     … User Account認証の一回限りのセットアップ(ブラウザ認可→refresh_token取得・.env保存)
  lib/
    env.js                  … .env の読み込み(process.envへ反映。.envが常に正、実行環境変数より優先)・LW_SHAREDRIVE_ID/LW_USER_REFRESH_TOKEN書き戻し
    logger.js                … 月次ローテーションするローカルログファイルへの書き込み(INFO/WARN/ERROR、時分秒付き)
    lineworksAuth.js         … get-token.js が使うService Account認証(JWT)。コード内にデフォルト値は持たず、.env未設定なら例外
    lineworksUserAuth.js     … backup.jsが使うUser Account認証(OAuth認可コードフロー)。認可URL組立て・コード交換・refresh_tokenによる再発行(rotation対応)
    lineworksDrive.js        … 共有ドライブAPI一覧のラッパー(共有ドライブID解決・フォルダ確保・アップロード・一覧・削除)、429リトライ付きfetch。logger連携で各API呼び出しをINFO/WARNログに記録
    pgDump.js                … pg_dump をspawnし、標準出力を3072MB単位でローカルファイルに分割書き出し
    testDb.js                … createdb/dropdb をspawnしてテスト用DBを作成・削除するヘルパー
  logs/                      … ログのローカル置き場(月次ファイル、既定 backup-YYYYMM.log)
  package.json               … 依存ゼロ(fetch/FormData/Blob/fs.openAsBlob等 Node標準のみ)。engines: node>=18.13
  get-token.js               … Service Account認証の動作確認用CLI。内部実装は lib/lineworksAuth.js を呼ぶ薄いラッパー。コード内にデフォルト値を持たず、必要な環境変数が.envに無ければ実行不可
  .env                       … 下記キーを追記
  SPEC.md                    … 本仕様書(このplanの内容)をそのまま保存。実装の最初のステップとしてコミットする

sftp.sh は変更・参照しない。

.env 追記内容

# --- Drive(共有ドライブ)バックアップ関連 (User Account認証) ---
LW_SCOPE_DRIVE=file
LW_REDIRECT_URI=https://example.com/callback
LW_USER_REFRESH_TOKEN=
LW_SHAREDRIVE_NAME=システムバックアップ
LW_SHAREDRIVE_ID=
LW_BACKUP_ROOT_FOLDER_NAME=Pleasanter_DB_Backup

# --- PostgreSQL接続 (backup.sh のデフォルトを踏襲) ---
PG_DUMP_BIN=/usr/bin/pg_dump
PGHOST=localhost
PGPORT=5432
PGUSER=postgres
PGPASSWORD=
PGDATABASE=Implem.Pleasanter

# --- ローカル一時ファイル・分割・ローテーション ---
BACKUP_LOCAL_DIR=/var/lib/pgsql/backup/dmp/
PG_DUMP_SPLIT_SIZE_MB=3072
BACKUP_RETENTION_COUNT=100

# --- ログ ---
LOG_LOCAL_DIR=./logs
LW_LOG_FOLDER_NAME=logs

# --- テスト用DB(create-test-db / drop-test-db) ---
PG_CREATEDB_BIN=createdb
PG_DROPDB_BIN=dropdb
TEST_DB_NAME=pleasanter_backup_test

LW_SHAREDRIVE_ID は初回実行時に名前(LW_SHAREDRIVE_NAME)から解決し、lib/env.js.env ファイルの当該行を書き換えて永続化する。2回目以降はID解決APIを呼ばずキャッシュ値を使う。

処理フロー (backup.js)

全体を try { ... } catch { ...ERRORログ... } finally { ...ログファイルのアップロード... } で包む。accessToken / sharedriveId / backupFolderId / logsFolderId は関数スコープの変数に保持し、finally 内でログアップロード時に(まだ未取得なら)再利用・再取得する。

  1. .env 読み込み(lib/env.jsprocess.env に無いキーだけ反映=実環境変数を優先)。logger.info("===== バックアップ開始 =====")
  2. lib/pgDump.js: logger.info("pg_dump開始: host=... db=...")PG_DUMP_BIN -h $PGHOST -p $PGPORT -U $PGUSER -Fc $PGDATABASE を spawn(PGPASSWORD 設定時のみ子プロセスのenvに渡す)。標準出力を BACKUP_LOCAL_DIR 配下に postgres-<YYYYMMDD-HHMMSS>.dmp000, .dmp001, … として PG_DUMP_SPLIT_SIZE_MB ごとに書き出す(split -d -a 3 と同じ3桁ゼロ埋め連番)。完了時 logger.info("pg_dump終了: 合計サイズ=... 所要時間=...")。exit code非0またはstderrで失敗時は logger.error(...) を出して例外を投げここで中断(アップロード・ローテーションは行わない)。
  3. logger.info("LINE WORKSアクセストークン取得開始 (User Account認証)")lib/lineworksUserAuth.jsgetUserAccessToken()LW_USER_REFRESH_TOKEN から access_token を再発行(rotationで返る新しいrefresh_tokenは即座に.envへ保存) → 成功で logger.info("アクセストークン取得成功")、失敗で logger.error(...)(LW_USER_REFRESH_TOKEN未設定時は「先にlineworks-authorize.jsを実行してください」という内容の例外)。
  4. lib/lineworksDrive.js(各呼び出し前後でINFOログ、429リトライはWARNログをlogger経由で出力):
    • resolveSharedDriveId(): .envLW_SHAREDRIVE_ID があれば logger.info("共有ドライブID: キャッシュ値を使用 (...)")。無ければ一覧取得→LW_SHAREDRIVE_NAMEと一致するものを検索→.envへ書き戻し、logger.info("共有ドライブID解決: name=... id=...")。見つからなければlogger.errorしてエラー終了。
    • ensureFolder(parentFileId, name): 一覧を見て同名のFOLDERがあれば logger.info("フォルダ確認: 既存を使用 name=... id=...")、無ければ作成し logger.info("フォルダ確認: 新規作成 name=... id=...")。これを Pleasanter_DB_Backup(ルート直下)→ 日付フォルダYYYYMMDD(その配下)の2階層分呼ぶ。
    • 分割されたダンプファイルを順番に uploadFile(sharedriveId, dateFolderId, filePath) でアップロード。各ファイルについて logger.info("アップロード開始: ファイル名=... サイズ=...") → 段階1でuploadUrl取得→段階2でPOST → 成功で logger.info("アップロード成功: ファイル名=... fileId=... 所要時間=...")、失敗で logger.error(...) して例外を投げ中断(ローカルファイルは削除しない=再実行できるように残す)。大きいファイルは fs.openAsBlob() でメモリに全展開せず送信。
  5. 全パートのアップロードに成功したら BACKUP_LOCAL_DIR 上の当該一時ファイルを削除し logger.info("ローカル一時ファイル削除完了")
  6. ローテーション: logger.info("ローテーション開始 (保持件数=...)")Pleasanter_DB_Backup 配下のフォルダ一覧を取得し、フォルダ名(=日付文字列なので辞書順=時系列順)でソート。BACKUP_RETENTION_COUNT を超えた古い方から deleteFile(sharedriveId, fileId) で削除しつつ logger.info("削除: フォルダ名=... fileId=...")logger.info("ローテーション終了: 削除件数=...")
  7. logger.info("===== バックアップ終了(成功) ===== 所要時間=...")。失敗時は catch 節で logger.error("===== バックアップ終了(失敗): ...") を出し process.exitCode = 1
  8. finally 節: logger.info("ログファイルアップロード開始") → 4.で解決済みの sharedriveId を使って(未解決なら改めて解決を試み、それも失敗したらログアップロードは諦めローカルログのみ残す)Pleasanter_DB_Backup/logs フォルダを確保し、当月ログファイル(backup-YYYYMM.log)を overwrite: true でアップロード(段階1リクエストのoverwriteフラグをtrueにして同名ファイルを上書き)。成功/失敗いずれも最後にコンソールへも出力。

初回セットアップ (lineworks-authorize.js)

Drive API利用に必須のUser Account認証を一度だけ行うスクリプト。

  1. 事前にDeveloper Consoleの「User Account認証」欄のRedirect URLに .envLW_REDIRECT_URI と完全一致する値を登録しておく。
  2. node lineworks-authorize.js を実行すると認可URLが表示されるので、ブラウザで開き対象アカウント(例: kenichiro.nogi@next-hd.co.jp)でログイン・同意する。パスワードなど認証情報はこちらのスクリプトや対話には一切入力しない(ブラウザ上のLINE WORKSログイン画面でのみ入力する)。
  3. 同意後リダイレクトされた画面のURL(またはcodeパラメータの値)をコンソールに貼り付ける。
  4. codeaccess_token/refresh_tokenに交換し、refresh_token.envLW_USER_REFRESH_TOKENに保存する。
  5. 以降 backup.js は無人でこのrefresh_tokenからaccess_tokenを再発行する。Refresh Token Rotationにより毎回新しいrefresh_tokenが発行されるため、lib/lineworksUserAuth.jsgetUserAccessToken()は取得の都度.envへ書き戻す。

CLIサブコマンド (backup.js)

process.argv[2] をコマンド名として単純にディスパッチする(追加npm依存なし)。

コマンド 内容
node backup.js または node backup.js run [--database=<dbName>] 通常のバックアップフロー(上記1〜8)を実行。--database指定時は同プロセス内でPGDATABASEをその値に上書きしてから実行(.envは書き換えない=使い捨ての一時上書き)。
node backup.js create-test-db [dbName] テスト用DBを作成(lib/testDb.js)。省略時は.envTEST_DB_NAMEを使用。
node backup.js drop-test-db [dbName] テスト用DBを削除(lib/testDb.js)。省略時は.envTEST_DB_NAMEを使用。

これにより、本番DBに触れず以下のようなセルフテストが可能になる:

node backup.js create-test-db
node backup.js run --database=pleasanter_backup_test
node backup.js drop-test-db

lib/testDb.js

  • createTestDb(dbName): PG_CREATEDB_BIN -h $PGHOST -p $PGPORT -U $PGUSER <dbName> を spawn(PGPASSWORD設定時は子プロセスのenvへ渡す)。既に存在する場合はcreatedbのエラーをそのまま表示して終了(先にdrop-test-dbを促すメッセージを添える)。
  • dropTestDb(dbName): PG_DROPDB_BIN -h $PGHOST -p $PGPORT -U $PGUSER --if-exists <dbName> を spawn。--if-existsにより未作成でもエラーにしない。
  • どちらも接続先ホスト/ポート/ユーザーは通常バックアップと同じ.envPGHOST/PGPORT/PGUSER/PGPASSWORDを再利用する(接続先サーバーは同じ、対象DB名だけが違う)。
  • 実行結果はconsole.log/console.errorのみ(通常バックアップの月次ログ・LINE WORKSへのログアップロードとは無関係な開発用ユーティリティのため、logger/Driveアップロードには連携しない)。

ログ設計 (lib/logger.js)

  • 出力先はローカルの LOG_LOCAL_DIR/backup-YYYYMM.log(月が変わったら自動的に別ファイルに切り替わる=月1回ローテーション)。プロセス内でファイルハンドルを使い回さず、書き込みの都度対象月のパスを計算して追記(fs.appendFileSync)することで日付またぎにも自然に対応する。
  • 1行フォーマット: YYYY-MM-DD HH:mm:ss [LEVEL] message(時分秒まで必須)。
  • logger.info(msg) / logger.warn(msg) / logger.error(msg) の3レベル。すべて console.log/console.warn/console.error にも同時出力し、標準出力からも経過が追えるようにする。
  • 記録対象(すべて時分秒付きINFO、異常系はWARN/ERROR):
    • バックアップ処理全体の開始・終了(成功/失敗、所要時間)
    • pg_dumpの開始・終了(失敗時はERRORでstderr内容も記録)
    • LINE WORKSアクセストークン取得(scope、開始・成功・失敗)
    • 共有ドライブID解決(キャッシュ使用/新規検索の別)
    • Pleasanter_DB_Backup・日付フォルダ・logsフォルダそれぞれの取得/作成(既存流用か新規作成か、fileId)
    • 各ダンプファイルのアップロード開始・成功・失敗
    • 429レートリミット時のリトライ(WARN、待機秒数)
    • ローカル一時ファイル削除
    • ローテーションで削除したフォルダ一覧
    • ログファイル自体のアップロード開始・成功・失敗
  • lib/lineworksDrive.js の各関数は logger を受け取って(または直接requireして)上記のログを出す。lwFetch内の429リトライ箇所も console.warn ではなく logger.warn に統一する。

再利用・共通化する既存ロジック

  • JWT組立て・トークン取得: get-token.js / lineworks-anythingllm.jscreateJwtAssertion / getAccessToken をほぼそのまま lib/lineworksAuth.js に切り出す(scopeを引数化するだけで、get-token.jsの board向けデフォルトはCLI側に残す)。
  • 429リトライ付きfetch: lineworks-anythingllm.jslwFetch(スロットリング+Retry-After対応)と同じ実装を lib/lineworksDrive.js 内に置く(ロジックはコピーだが依存関係を増やしたくないため別ファイル化はしない)。
  • ファイル名の日時フォーマットは backup.shdate '+%Y%m%d-%H%M%S' 相当をJSで再現。

動作確認方法

  1. node dbbackup/get-token.js でService Account認証のトークン取得自体が引き続き動くことを確認(リファクタ後の非破壊確認。.envLW_CLIENT_ID等が無ければ実行不可になることも確認)。
  2. node dbbackup/lineworks-authorize.js を一度実行し、User Account認証(kenichiro.nogi@next-hd.co.jp)でrefresh_tokenが.envLW_USER_REFRESH_TOKENに保存されることを確認。
  3. node dbbackup/backup.js create-test-db でテスト用DBを作成し、node dbbackup/backup.js run --database=pleasanter_backup_test(本番PGDATABASEには触れない)を一度実行して、
    • LINE WORKSの「システムバックアップ」共有ドライブに Pleasanter_DB_Backup/<today>/ が作成され、ダンプファイルが格納されること
    • Pleasanter_DB_Backup/logs/backup-<今月>.log がアップロードされ、開始〜終了までの各ステップが時分秒付き・INFOで記録されていること
    • .envLW_SHAREDRIVE_ID が書き込まれること
    • ローカル dbbackup/logs/backup-<今月>.log にも同内容が残っていること を目視確認。
  4. 同日にもう一度 run --database=pleasanter_backup_test を実行し、同じ日付フォルダ・同じ月次ログファイルが上書き(ログは追記)されること、ダンプファイル名は秒まで異なるため衝突しないことを確認。
  5. BACKUP_RETENTION_COUNT を小さい値(例: 2)に一時変更して連続実行し、古い日付フォルダが削除されローテーションが機能すること、ログに削除対象フォルダがINFOで記録されることを確認。
  6. 異常系: DB接続情報を意図的に間違え、アップロード・ローテーションに進まず失敗終了すること、ローカルログにERRORが記録されつつfinallyでログファイル自体はアップロードされることを確認。
  7. node dbbackup/backup.js drop-test-db でテスト用DBを削除し、後始末できることを確認。