[{"data":1,"prerenderedAt":267},["ShallowReactive",2],{"content-/image-r2-diff-upload-and-rights":3,"all-pages-for-dir":246,"related-/image-r2-diff-upload-and-rights":247,"og-image-/image-r2-diff-upload-and-rights":266},{"id":4,"title":5,"body":6,"category":226,"concepts":227,"description":228,"extension":229,"meta":230,"navigation":231,"ogImage":227,"path":232,"project_name":233,"published":234,"publishedAt":235,"seo":236,"source":237,"stem":238,"tags":239,"todo":227,"unpublished":234,"updatedAt":227,"__hash__":245},"pages/2026-08/2026-08-19/image-r2-diff-upload-and-rights.md","Cloudflare R2の画像を差分アップロードに変え、第三者権利のある図を本番から下ろす",{"type":7,"value":8,"toc":213},"minimark",[9,13,16,21,24,27,30,33,36,44,47,50,53,64,71,74,77,80,83,86,90,93,100,106,109,113,116,119,122,126,129,136,139,142,145,148,151,154,157,184,190,193,196,199],[10,11,12],"p",{},"前の晩、本番のサイトで図の原本が開けないのを見ていた。ただ、そのときは手をつけないままにしていた。翌朝いちばんにやったのは、その件ではなく、前日の積み残しを並べさせることだった。",[10,14,15],{},"出てきたのは4系統。このリポジトリの未pushと未デプロイ、講義用テキストの図レビューの続き、QA教材のフェーズ残り、それとサイト本体側で公開記事の図3枚が未作成でリンク切れの疑い。最後のがいちばん痛いが、まず手前を片づけることにした。未コミット分をコミットしてデプロイする。学習ゲートはスキップでいいと伝えた。",[17,18,20],"h2",{"id":19},"デプロイは通ったのに旧版の図が出ない","デプロイは通ったのに、旧版の図が出ない",[10,22,23],{},"同じ作業ツリーを別のセッションが触っていた点だけ、先に確認させた。最初は講義用テキストの図レビュー側だと言われた。それだとビルドの途中でSVGが書き換わり、その途中状態が本番に乗ってしまう。実際に動いていたのはQA教材のほうで、触るのはコンテンツの元ファイルとメモだけだった。ビルド出力には入らない。混入の心配は消えた。",[10,25,26],{},"pushは通った。デプロイもバックグラウンドで走らせ、しばらくして exit 0 が返ってきた。ただ、このデプロイスクリプトはビルドに失敗しても「Status: built」を返すことがある。既知の落とし穴としてissueに残してあるので、成功と書いてあることは信用せず、実物を3点確かめさせた。ビルド出力の.mjsが0件でないこと、本番の図JSONが手元と一致すること、SVGのmd5が合うこと。8件は8件とも一致した。",[10,28,29],{},"そのうえでギャラリーを開いたら、旧版の枠が2つ空いていた。v2とv3が読み込めていない。数えさせると、旧版のSVGが23件リンク切れになっていた。デプロイのせいではなかった。前日にR2へ上げたとき、この23件が対象から漏れていた。前の晩に見た「原本が開けない」の正体がこれだった。",[17,31,32],{"id":32},"本当にコマンドで済むのか",[10,34,35],{},"上げ直すためのスクリプトはある。ただ、走らせる前に引っかかった。全部上げ直すのだとしたら、ほかのものまで書き潰さないだろうか。コミット済みなら戻せるとはいえ、アップロード先はGitの管理下にない。",[10,37,38,39,43],{},"実行させずに全文を読ませた。結論は、削除は一切しない、putしかしない、というものだった。ただ読ませたおかげで別のものが出てきた。第三者に権利のある図が1枚だけあり、それがスクリプトの使用例で除外対象として名指しされていた。手動の",[40,41,42],"code",{},"-Exclude","で外す運用になっている。",[10,45,46],{},"書き込みなしのモードで対象を数えさせ、全件ではなく欠けている23件だけを上げる案に絞った。権利のある図が混ざっていないことも確かめてから実行させた。23件すべて成功と出た。",[17,48,49],{"id":49},"上げたのに404が返ってくる",[10,51,52],{},"ところが、上げたはずのものが本番ではまだ404を返した。",[10,54,55,56,59,60,63],{},"切り分けさせたら、R2には正しい実体があった。md5は手元と完全に一致している。404を返していたのはCloudflareのエッジで、さっきまでの404をキャッシュしていた。",[40,57,58],{},"cf-cache-status","はHIT、",[40,61,62],{},"max-age","は14400秒。4時間は自力で消えない。キャッシュを迂回すれば23件とも200で返ることまで確認させた。",[10,65,66,67,70],{},"パージ用のスクリプトは無い。APIキーを取りに行こうとして",[40,68,69],{},".env","に触れたところで弾かれた。読まないルールにしてあるので、そこは正しい挙動だった。",[17,72,73],{"id":73},"ブラウザでパージさせる",[10,75,76],{},"コマンドで押し切るより、自分がログイン済みのブラウザでやらせたほうが早い。Chrome DevTools経由でダッシュボードを操作させることにした。",[10,78,79],{},"アカウントIDを取らせてからキャッシュの設定ページへ移動し、カスタムパージを開かせた（すべてパージは押させない）。URL指定の上限は30件で、こちらは23件。貼り付けてから、押す前に中身を検証させた。23行、想定外のURLは0、重複も0。",[10,81,82],{},"ここでボタンが有効にならなかった。テキストは入っているのに、React側の状態に伝わっていない。正規のinputイベントを発火させて同期させたら押せるようになった。画面を目視で確かめてから実行させた。",[10,84,85],{},"確認は旧版51件をまとめて行い、すべて200で返った。v2とv3の枠に図が出て、画像の読み込み失敗は0件になった。",[17,87,89],{"id":88},"_1枚だけ直したときに何が起きるのか","1枚だけ直したときに何が起きるのか",[10,91,92],{},"ここで気になったことを聞いた。図を作り直したら、そのスクリプトを回すのは全件再アップロードになるのか。1枚だけ修正しても。",[10,94,95,96,99],{},"そのとおりだった。両方のスクリプトとも差分検知を持っていない。しかも意図的で、",[40,97,98],{},"upload-images-to-r2.ps1","の7行目にこう書いてある。",[101,102,103],"blockquote",{},[10,104,105],{},"すでに R2 上に同じ key が存在しても上書きする（差分検知はしない＝シンプル運用）",[10,107,108],{},"思いつきをそのまま言った。変更したらGitの履歴に入るのだから、履歴からファイル名を拾って上げ直せばいいのではないか。そんなに難しいことをやろうとしているわけではないはずだ。無料枠が100万回なら費用は問題にならないけれど、安く作れるならそのほうがいい。",[17,110,112],{"id":111},"gitの履歴では捕まらないもの","Gitの履歴では捕まらないもの",[10,114,115],{},"返ってきた反論で、自分の案がこの日のバグを捕まえられないことがわかった。",[10,117,118],{},"404だった図の1枚は8月16日にコミット済みだった。Gitから見れば「とっくに変更済み」で、その後に何も起きていない。落ちていた理由は、変更されたのに上げ忘れたことだった。Gitが答えるのは「何が変わったか」であって、「R2に何が無いか」ではない。",[10,120,121],{},"ETagを比較すれば両方まとめて拾えて、コード量も同じくらいになる。Git由来のリストも作れるが、二重管理になるぶんかえって複雑になる。ETag比較でいくことにした。",[17,123,125],{"id":124},"弱いetagで51件の誤検出","弱いETagで51件の誤検出",[10,127,128],{},"実装させて書き込みなしで回したら、差分51件と出た。合っているはずのものが全部ずれている。",[10,130,131,132,135],{},"原因は圧縮配信だった。SVGは圧縮して配られるので、ETagが",[40,133,134],{},"W/\"...\"","という弱い形になる。値そのものは同じmd5で、前後の飾りが違うだけだった。パースを直したら差分ゼロになった。ターミナルの文字化けのほうは、表示だけの問題だった。",[10,137,138],{},"差分ゼロは正常だが、それだけでは検出できることの証明にならない。権利つきの1枚はR2に無いはずなので、除外を外せば1件だけ拾える。そういうテストをさせた。",[10,140,141],{},"出なかった。その1枚はR2にあって、公開URLで200を返していた。",[10,143,144],{},"検出が働くこと自体は別のやり方で確かめ、もう一方のスクリプトにも同じ仕組みを入れた。どちらも差分ゼロで落ち着いた。残ったのは、200を返していた1枚のほうだった。",[17,146,147],{"id":147},"権利のある図を本番から下ろす",[10,149,150],{},"除外は手作業のリストに頼っていて、書き忘れればそのまま通る。実際に本番から配信されていた。",[10,152,153],{},"その図はウェブから表示しないことにした。誰が作った図なのか自分でも思い出せず、国が出しているものではないかと口に出した。デプロイを待つあいだに説明は返ってきたが、判断は変えなかった。どのみち後で消す予定だったので、前倒しにした。R2の実体も削除していいと伝えた。",[10,155,156],{},"直させたのは3か所。",[158,159,160,172,181],"ul",{},[161,162,163,164,167,168,171],"li",{},"生成側は、台帳の",[40,165,166],{},"rights","が",[40,169,170],{},"ok","以外なら原図そのものを出力しない",[161,173,174,175,177,178,180],{},"アップロード側は、台帳の",[40,176,166],{},"を見て自動で弾く。手動の",[40,179,42],{},"に頼らない",[161,182,183],{},"R2の実体は消す。復元元が手元にある（244KB）ことを確かめてから実行させた",[10,185,186,187,189],{},"自動除外は効いた。",[40,188,42],{},"を付けなくてもその1枚は対象外になり、件数452も一致した。教材JSONの差分もその1件だけで、チェックコマンドにも差分は出なかった。",[10,191,192],{},"R2から消したのに、まだ200が返ってきた。またパージさせた。R2からも本番経由でも404になり、ほかの図は200のままだった。コミットとデプロイをやり直させ、3.9分で通った。",[10,194,195],{},"最後に、次のセッションで図レビューの続きに入るためのプロンプトを出させて終わりにした。",[17,197,198],{"id":198},"残ったもの",[158,200,201,204,207,210],{},[161,202,203],{},"Gitが答えられるのは「何が変わったか」まで。「向こうに何が無いか」は別の問いで、答えを持っていたのはR2側のETagだけだった",[161,205,206],{},"手動の除外リストは、書いた本人が忘れる。台帳のフラグで自動的に弾く形にしないと、いつか通る",[161,208,209],{},"R2に上げただけでは直らない。エッジの404は4時間居座る。実体を消したときも同じで、消したのに200が返る",[161,211,212],{},"「本当にコマンドで済むのか」と引っかかったのは正解だった。削除しないスクリプトだったが、走らせる前に全文を読ませたおかげで権利つきの1枚が浮かんだ",{"title":214,"searchDepth":215,"depth":215,"links":216},"",2,[217,218,219,220,221,222,223,224,225],{"id":19,"depth":215,"text":20},{"id":32,"depth":215,"text":32},{"id":49,"depth":215,"text":49},{"id":73,"depth":215,"text":73},{"id":88,"depth":215,"text":89},{"id":111,"depth":215,"text":112},{"id":124,"depth":215,"text":125},{"id":147,"depth":215,"text":147},{"id":198,"depth":215,"text":198},"dev",null,"本番で旧版の図がリンク切れしていた原因は、前日のアップロード漏れとエッジの404キャッシュだった。1枚直すたびに全件を上げ直す運用を疑い、ETag比較の差分アップロードへ変えた記録","md",{},true,"/image-r2-diff-upload-and-rights","eurekapu-nuxt4",false,"2026-08-19T00:00:00.000Z",{"title":5,"description":228},"make-diary","2026-08/2026-08-19/image-r2-diff-upload-and-rights",[240,241,242,243,244],"Cloudflare R2","差分アップロード","ETag","キャッシュ","デプロイ","9t41snEYCz1eJYjNWvQHVL5iy6dE0zWi8m17F6C61U0",[],[248,252,255,259,263],{"title":249,"path":250,"publishedAt":251},"eurekapu-nuxt4のデプロイ復旧と、画像・音声をR2配信に寄せ切った一日","/gogen-assets-r2-offload","2026-07-20T00:00:00.000Z",{"title":253,"path":254,"publishedAt":235},"2026年8月19日の開発日記 - やり直しの単位を小さくした一日","/2026-08-19-diary",{"title":256,"path":257,"publishedAt":258},"縦型ショート動画のレイアウトを専用実装せず、既存のレスポンシブ縦積みで撮った","/vertical-short-video-layout","2026-08-18T00:00:00.000Z",{"title":260,"path":261,"publishedAt":262},"2026年8月14日の開発日記 - 積み残しに番号を振って潰し、デプロイ25分の内訳を割った","/2026-08-14-diary","2026-08-14T00:00:00.000Z",{"title":264,"path":265,"publishedAt":262},"デプロイ25分の内訳を計測して、削れる場所と削れない場所を分けた","/deploy-time-breakdown-measurement","https://log.eurekapu.com/og/blog/image-r2-diff-upload-and-rights.png?v=2026-08-19T00%3A00%3A00.000Z&title=Cloudflare%20R2%E3%81%AE%E7%94%BB%E5%83%8F%E3%82%92%E5%B7%AE%E5%88%86%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%AB%E5%A4%89%E3%81%88%E3%80%81%E7%AC%AC%E4%B8%89%E8%80%85%E6%A8%A9%E5%88%A9%E3%81%AE%E3%81%82%E3%82%8B%E5%9B%B3%E3%82%92%E6%9C%AC%E7%95%AA%E3%81%8B%E3%82%89%E4%B8%8B%E3%82%8D%E3%81%99&author=Kei%20Komatsu&sig=a2baea214ff24a00",1787187401295]