diff --git a/README.md b/README.md index ef3a147..80cab5d 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 030b34a..f3da21f 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 05707e7..505f50b 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 83b1f99..33b93ae 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"