From 5821c77caa80304224862f22aaee524870bc0d3a Mon Sep 17 00:00:00 2001 From: pallaoro Date: Thu, 3 Sep 2026 07:51:30 +0200 Subject: [PATCH] fix(uploads): store images in R2 so the Worker can boot src/server/uploads.ts wrote to the local filesystem via node:fs and resolved its uploads directory with fileURLToPath(import.meta.url) at module scope. Neither works on Workers: import.meta.url is undefined there, so the call threw while the module was still being imported and the Worker never started. Every route was dead, not just uploads. Swap the module for the R2 implementation the other templates already use, bind the bucket once in middleware, and declare the binding in wrangler.toml. Verified against a local `wrangler dev`: the Worker boots, GET /api/templates returns the seeded rows, design create/get/list/delete round-trip, and an uploaded PNG comes back byte-identical from GET /api/uploads/:filename. --- README.md | 2 +- src/server/index.ts | 11 +++++-- src/server/uploads.ts | 67 +++++++++++++++++-------------------------- wrangler.toml | 4 +++ 4 files changed, 40 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index ef3a147a..80cab5df 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ pnpm install pnpm run dev ``` -Open `http://localhost:5178` in your browser. Data persists in `data.db`, uploads in `uploads/`. +Open `http://localhost:5173` in your browser. Designs and uploads persist in the local D1 and R2 simulators under `.wrangler/`. ### Agent Mode (for OpenClaw / Claude Code) diff --git a/src/server/index.ts b/src/server/index.ts index 030b34af..f3da21f2 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,11 +1,18 @@ import { createApp, createRoute, z } from "@clawnify/app"; import { query, get, run } from "./db.js"; -import { putUpload, getUpload } from "./uploads.js"; +import { initUploads, putUpload, getUpload } from "./uploads.js"; -type Env = { Bindings: { DB: D1Database } }; +type Env = { Bindings: { DB: D1Database; UPLOADS: R2Bucket } }; const app = createApp({ title: "OpenDesign API", version: "1.0.0" }); +// Bind the R2 bucket before any handler runs. Registered ahead of every route +// because Hono runs middleware in registration order. +app.use("*", async (c, next) => { + initUploads(c.env.UPLOADS); + await next(); +}); + // ── Schemas ────────────────────────────────────────────────────────── const DesignSchema = z.object({ diff --git a/src/server/uploads.ts b/src/server/uploads.ts index 05707e7f..505f50b8 100644 --- a/src/server/uploads.ts +++ b/src/server/uploads.ts @@ -1,55 +1,40 @@ -import { mkdirSync, writeFileSync, readFileSync, existsSync, unlinkSync } from "fs"; -import { join, dirname } from "path"; -import { fileURLToPath } from "url"; +let _bucket: R2Bucket; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const UPLOADS_DIR = join(__dirname, "..", "..", "uploads"); -mkdirSync(UPLOADS_DIR, { recursive: true }); - -const MIME: Record = { - png: "image/png", - jpg: "image/jpeg", - jpeg: "image/jpeg", - gif: "image/gif", - webp: "image/webp", - svg: "image/svg+xml", -}; - -function mime(filename: string): string { - return MIME[filename.split(".").pop()?.toLowerCase() || "png"] || "application/octet-stream"; -} - -function sanitize(filename: string): string { - return filename.replace(/[^a-zA-Z0-9._-]/g, ""); +export function initUploads(bucket: R2Bucket) { + _bucket = bucket; } -export async function putUpload(filename: string, data: ArrayBuffer | Uint8Array, contentType: string): Promise { - writeFileSync(join(UPLOADS_DIR, filename), Buffer.from(data as ArrayBuffer)); +export async function putUpload( + filename: string, + data: ArrayBuffer | Uint8Array, + contentType: string, +): Promise { + await _bucket.put(filename, data, { httpMetadata: { contentType } }); return `/api/uploads/${filename}`; } -export async function getUpload(filename: string): Promise<{ data: ArrayBuffer; contentType: string } | null> { - const safe = sanitize(filename); - const filePath = join(UPLOADS_DIR, safe); - if (!existsSync(filePath)) return null; - const buf = readFileSync(filePath); +export async function getUpload( + filename: string, +): Promise<{ data: ArrayBuffer; contentType: string } | null> { + const obj = await _bucket.get(filename); + if (!obj) return null; return { - data: buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength), - contentType: mime(safe), + data: await obj.arrayBuffer(), + contentType: obj.httpMetadata?.contentType || "application/octet-stream", }; } export async function deleteUpload(filename: string): Promise { - const safe = sanitize(filename); - const filePath = join(UPLOADS_DIR, safe); - if (existsSync(filePath)) unlinkSync(filePath); + await _bucket.delete(filename); } -export async function readUploadAsBase64DataUrl(filename: string): Promise { - const safe = sanitize(filename); - const filePath = join(UPLOADS_DIR, safe); - if (!existsSync(filePath)) return null; - const buf = readFileSync(filePath); - const contentType = mime(safe); - return `data:${contentType};base64,${buf.toString("base64")}`; +export async function readUploadAsBase64DataUrl( + filename: string, +): Promise { + const result = await getUpload(filename); + if (!result) return null; + const bytes = new Uint8Array(result.data); + let binary = ""; + for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]); + return `data:${result.contentType};base64,${btoa(binary)}`; } diff --git a/wrangler.toml b/wrangler.toml index 83b1f99d..33b93aeb 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -7,3 +7,7 @@ compatibility_flags = ["nodejs_compat"] binding = "DB" database_name = "open-design-db" database_id = "local" + +[[r2_buckets]] +binding = "UPLOADS" +bucket_name = "open-design-uploads"