Cloudflare R2の画像を差分アップロードに変え、第三者権利のある図を本番から下ろす
前の晩、本番のサイトで図の原本が開けないのを見ていた。ただ、そのときは手をつけないままにしていた。翌朝いちばんにやったのは、その件ではなく、前日の積み残しを並べさせることだった。
出てきたのは4系統。このリポジトリの未pushと未デプロイ、講義用テキストの図レビューの続き、QA教材のフェーズ残り、それとサイト本体側で公開記事の図3枚が未作成でリンク切れの疑い。最後のがいちばん痛いが、まず手前を片づけることにした。未コミット分をコミットしてデプロイする。学習ゲートはスキップでいいと伝えた。
デプロイは通ったのに、旧版の図が出ない
同じ作業ツリーを別のセッションが触っていた点だけ、先に確認させた。最初は講義用テキストの図レビュー側だと言われた。それだとビルドの途中でSVGが書き換わり、その途中状態が本番に乗ってしまう。実際に動いていたのはQA教材のほうで、触るのはコンテンツの元ファイルとメモだけだった。ビルド出力には入らない。混入の心配は消えた。
pushは通った。デプロイもバックグラウンドで走らせ、しばらくして exit 0 が返ってきた。ただ、このデプロイスクリプトはビルドに失敗しても「Status: built」を返すことがある。既知の落とし穴としてissueに残してあるので、成功と書いてあることは信用せず、実物を3点確かめさせた。ビルド出力の.mjsが0件でないこと、本番の図JSONが手元と一致すること、SVGのmd5が合うこと。8件は8件とも一致した。
そのうえでギャラリーを開いたら、旧版の枠が2つ空いていた。v2とv3が読み込めていない。数えさせると、旧版のSVGが23件リンク切れになっていた。デプロイのせいではなかった。前日にR2へ上げたとき、この23件が対象から漏れていた。前の晩に見た「原本が開けない」の正体がこれだった。
本当にコマンドで済むのか
上げ直すためのスクリプトはある。ただ、走らせる前に引っかかった。全部上げ直すのだとしたら、ほかのものまで書き潰さないだろうか。コミット済みなら戻せるとはいえ、アップロード先はGitの管理下にない。
実行させずに全文を読ませた。結論は、削除は一切しない、putしかしない、というものだった。ただ読ませたおかげで別のものが出てきた。第三者に権利のある図が1枚だけあり、それがスクリプトの使用例で除外対象として名指しされていた。手動の-Excludeで外す運用になっている。
書き込みなしのモードで対象を数えさせ、全件ではなく欠けている23件だけを上げる案に絞った。権利のある図が混ざっていないことも確かめてから実行させた。23件すべて成功と出た。
上げたのに404が返ってくる
ところが、上げたはずのものが本番ではまだ404を返した。
切り分けさせたら、R2には正しい実体があった。md5は手元と完全に一致している。404を返していたのはCloudflareのエッジで、さっきまでの404をキャッシュしていた。cf-cache-statusはHIT、max-ageは14400秒。4時間は自力で消えない。キャッシュを迂回すれば23件とも200で返ることまで確認させた。
パージ用のスクリプトは無い。APIキーを取りに行こうとして.envに触れたところで弾かれた。読まないルールにしてあるので、そこは正しい挙動だった。
ブラウザでパージさせる
コマンドで押し切るより、自分がログイン済みのブラウザでやらせたほうが早い。Chrome DevTools経由でダッシュボードを操作させることにした。
アカウントIDを取らせてからキャッシュの設定ページへ移動し、カスタムパージを開かせた(すべてパージは押させない)。URL指定の上限は30件で、こちらは23件。貼り付けてから、押す前に中身を検証させた。23行、想定外のURLは0、重複も0。
ここでボタンが有効にならなかった。テキストは入っているのに、React側の状態に伝わっていない。正規のinputイベントを発火させて同期させたら押せるようになった。画面を目視で確かめてから実行させた。
確認は旧版51件をまとめて行い、すべて200で返った。v2とv3の枠に図が出て、画像の読み込み失敗は0件になった。
1枚だけ直したときに何が起きるのか
ここで気になったことを聞いた。図を作り直したら、そのスクリプトを回すのは全件再アップロードになるのか。1枚だけ修正しても。
そのとおりだった。両方のスクリプトとも差分検知を持っていない。しかも意図的で、upload-images-to-r2.ps1の7行目にこう書いてある。
すでに R2 上に同じ key が存在しても上書きする(差分検知はしない=シンプル運用)
思いつきをそのまま言った。変更したらGitの履歴に入るのだから、履歴からファイル名を拾って上げ直せばいいのではないか。そんなに難しいことをやろうとしているわけではないはずだ。無料枠が100万回なら費用は問題にならないけれど、安く作れるならそのほうがいい。
Gitの履歴では捕まらないもの
返ってきた反論で、自分の案がこの日のバグを捕まえられないことがわかった。
404だった図の1枚は8月16日にコミット済みだった。Gitから見れば「とっくに変更済み」で、その後に何も起きていない。落ちていた理由は、変更されたのに上げ忘れたことだった。Gitが答えるのは「何が変わったか」であって、「R2に何が無いか」ではない。
ETagを比較すれば両方まとめて拾えて、コード量も同じくらいになる。Git由来のリストも作れるが、二重管理になるぶんかえって複雑になる。ETag比較でいくことにした。
弱いETagで51件の誤検出
実装させて書き込みなしで回したら、差分51件と出た。合っているはずのものが全部ずれている。
原因は圧縮配信だった。SVGは圧縮して配られるので、ETagがW/"..."という弱い形になる。値そのものは同じmd5で、前後の飾りが違うだけだった。パースを直したら差分ゼロになった。ターミナルの文字化けのほうは、表示だけの問題だった。
差分ゼロは正常だが、それだけでは検出できることの証明にならない。権利つきの1枚はR2に無いはずなので、除外を外せば1件だけ拾える。そういうテストをさせた。
出なかった。その1枚はR2にあって、公開URLで200を返していた。
検出が働くこと自体は別のやり方で確かめ、もう一方のスクリプトにも同じ仕組みを入れた。どちらも差分ゼロで落ち着いた。残ったのは、200を返していた1枚のほうだった。
権利のある図を本番から下ろす
除外は手作業のリストに頼っていて、書き忘れればそのまま通る。実際に本番から配信されていた。
その図はウェブから表示しないことにした。誰が作った図なのか自分でも思い出せず、国が出しているものではないかと口に出した。デプロイを待つあいだに説明は返ってきたが、判断は変えなかった。どのみち後で消す予定だったので、前倒しにした。R2の実体も削除していいと伝えた。
直させたのは3か所。
- 生成側は、台帳の
rightsがok以外なら原図そのものを出力しない - アップロード側は、台帳の
rightsを見て自動で弾く。手動の-Excludeに頼らない - R2の実体は消す。復元元が手元にある(244KB)ことを確かめてから実行させた
自動除外は効いた。-Excludeを付けなくてもその1枚は対象外になり、件数452も一致した。教材JSONの差分もその1件だけで、チェックコマンドにも差分は出なかった。
R2から消したのに、まだ200が返ってきた。またパージさせた。R2からも本番経由でも404になり、ほかの図は200のままだった。コミットとデプロイをやり直させ、3.9分で通った。
最後に、次のセッションで図レビューの続きに入るためのプロンプトを出させて終わりにした。
残ったもの
- Gitが答えられるのは「何が変わったか」まで。「向こうに何が無いか」は別の問いで、答えを持っていたのはR2側のETagだけだった
- 手動の除外リストは、書いた本人が忘れる。台帳のフラグで自動的に弾く形にしないと、いつか通る
- R2に上げただけでは直らない。エッジの404は4時間居座る。実体を消したときも同じで、消したのに200が返る
- 「本当にコマンドで済むのか」と引っかかったのは正解だった。削除しないスクリプトだったが、走らせる前に全文を読ませたおかげで権利つきの1枚が浮かんだ