feat(attachments): R2 toggle, storage metrics, and demo R2 config

Add an ATTACHMENTS_ENABLED switch (default on when R2 is bound) via a
central getAttachmentBucket helper, surface R2 + estimated KV usage
against the free tier on the status page and /api/stats (refreshed by the
hourly cron), let setup.sh create and wire the R2 bucket, and bind the
demo bucket so the deployed demo has attachments.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-23 17:33:50 +02:00
parent 7226e718f7
commit f150d40c45
19 changed files with 387 additions and 23 deletions
+7 -4
View File
@@ -10,6 +10,7 @@ import { logger } from "../../lib/logger";
import { Layout, clampText } from "./ui";
import { deleteKeysWithConcurrency } from "./helpers";
import { feedRssUrl, feedAtomUrl, feedEmailAddress } from "../../utils/urls";
import { getAttachmentBucket } from "../../utils/attachments";
import { emailsPageScript } from "../../scripts/generated/emails-page";
type AppEnv = { Bindings: Env };
@@ -643,9 +644,10 @@ emailsRouter.post("/emails/:emailKey/delete", async (c) => {
await emailStorage.put(feedMetadataKey, JSON.stringify(feedMetadata));
}
if (env.ATTACHMENT_BUCKET && attachmentIds.length > 0) {
const attachmentBucket = getAttachmentBucket(env);
if (attachmentBucket && attachmentIds.length > 0) {
await Promise.allSettled(
attachmentIds.map((id) => env.ATTACHMENT_BUCKET!.delete(id)),
attachmentIds.map((id) => attachmentBucket.delete(id)),
);
}
@@ -726,9 +728,10 @@ emailsRouter.post("/feeds/:feedId/emails/bulk-delete", async (c) => {
);
await emailStorage.put(feedMetadataKey, JSON.stringify(feedMetadata));
if (env.ATTACHMENT_BUCKET && r2AttachmentIds.length > 0) {
const attachmentBucket = getAttachmentBucket(env);
if (attachmentBucket && r2AttachmentIds.length > 0) {
await Promise.allSettled(
r2AttachmentIds.map((id) => env.ATTACHMENT_BUCKET!.delete(id)),
r2AttachmentIds.map((id) => attachmentBucket.delete(id)),
);
}