家計簿アプリを作る #74:領収書の差し替え・削除
家計簿アプリ作成シリーズの第74回です。前回スコープ外とした「編集画面での領収書の差し替え・削除」に対応します。
<input type="file">には既定値を設定できない
編集画面で「今アップロードされている画像のファイル名を、ファイル選択欄の既定値として表示したい」と考えましたが、これはブラウザの仕様上不可能でした。<input type="file"> はセキュリティ上の理由から value をプログラムから設定することができず、常に空(またはユーザーが選択したファイルのみ)を許可します。
代わりに、ファイル選択欄とは別に現在の画像をプレビュー表示する形にしました。
{#if data.transaction.receiptKey}
<img src={`/api/receipts/${data.transaction.receiptKey}`} alt="領収書" class="max-w-xs" />
{/if}
<Fileupload id="receipt" name="receipt" accept="image/*" />
元のファイル名(UUIDではない本来の名前)を表示したい場合は別途DBカラムやR2のcustomMetadataが必要になりますが、今回はスコープ外としました。
削除対象のキーはクライアントを信用せずサーバー側で取得する
「削除する既存のR2オブジェクトのキー」をどこから取得するか検討しました。hidden inputでクライアントから送る方法も考えましたが、これは改竄可能(DevToolsで値を書き換えられる)なため採用しませんでした。
// ✅ サーバー側でDBから正しい値を取得し直す
const currentTransaction = await db.select({ receiptKey: transactions.receiptKey })
.from(transactions)
.where(and(eq(transactions.userId, locals.user!.id), eq(transactions.id, params.id)))
.get();
const existingKey = currentTransaction?.receiptKey ?? '';
削除対象は「他人のファイルを誤って消しかねない」値なので、クライアントの入力を信用せず、サーバー側で権限確認込みで再取得する方が安全です。
差し替え・削除・現状維持の優先順位
新しいファイル・削除フラグの組み合わせを整理し、以下の優先順位で処理しました。
let receiptKey = existingKey;
if (file && file.size > 0) {
// 新しいファイルがあれば、既存を削除してから差し替え
if (existingKey) await bucket.delete(existingKey);
receiptKey = `receipts/${crypto.randomUUID()}`;
await bucket.put(receiptKey, await file.arrayBuffer(), {
httpMetadata: { contentType: file.type },
});
} else if (deleteReceipt && existingKey) {
// 新しいファイルが無く、削除フラグがあれば削除
await bucket.delete(existingKey);
receiptKey = '';
} else {
// どちらも無ければ現状維持(receiptKeyは初期値のまま)
}
ハマりポイント:ビルドキャッシュで新しいコードが反映されない
動作確認で「ファイルを指定して更新すると、サーバー側でfileがundefinedになる」現象がありました。原因は今回もビルドキャッシュで、npm run build した後のコードを修正しても再ビルドしない限り wrangler dev には反映されません。修正後に再ビルドしたところ、正しくファイルを受け取れるようになりました。
R2やD1などCloudflareのbindingsを試す開発サイクルでは、コード変更のたびに再ビルドが必要になる点を、これで何度目かの学びとして再確認しました。
まとめ
| ポイント | 内容 |
|---|---|
| ファイルinputの既定値 | ブラウザの仕様上不可能。プレビュー表示で代替する |
| 削除対象キーの取得元 | クライアントのhidden inputではなく、サーバー側でDBから再取得する |
| 差し替え・削除の優先順位 | 新規ファイル > 削除フラグ > 現状維持、の順で判定する |
| ビルドキャッシュ | wrangler devでCloudflare bindingsを試す際は再ビルドを忘れずに |