import { del, get, put } from "@vercel/blob"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; export type BoardCard = { id: string; title: string; comment: string; imageUrl: string; published: boolean; createdAt: string; updatedAt: string; }; const dataBlobPath = "pozor/cards.json"; const localDataDir = path.resolve(".data"); const localUploadsDir = path.join(localDataDir, "uploads"); const localCardsPath = path.join(localDataDir, "cards.json"); const hasBlobToken = () => Boolean(process.env.BLOB_READ_WRITE_TOKEN?.trim()); const isVercel = () => process.env.VERCEL === "1"; export const storageMode = hasBlobToken() ? "Vercel Blob" : "Local file"; export async function readCards(): Promise { if (hasBlobToken()) { try { const result = await get(dataBlobPath, { access: "private" }); if (!result?.stream) { return []; } const text = await streamToText(result.stream); const parsed = JSON.parse(text) as BoardCard[]; return Array.isArray(parsed) ? parsed : []; } catch { return []; } } try { const text = await readFile(localCardsPath, "utf8"); const parsed = JSON.parse(text) as BoardCard[]; return Array.isArray(parsed) ? parsed : []; } catch { return []; } } export async function writeCards(cards: BoardCard[]) { const content = JSON.stringify(cards, null, 2); if (hasBlobToken()) { await put(dataBlobPath, content, { access: "private", contentType: "application/json", allowOverwrite: true }); return; } assertWritableLocalStorage(); await mkdir(localDataDir, { recursive: true }); await writeFile(localCardsPath, content, "utf8"); } export async function saveImage(file: File) { const safeName = safeFileName(file.name || "image"); const pathname = `pozor/images/${Date.now()}-${safeName}`; if (hasBlobToken()) { const blob = await put(pathname, file, { access: "public", contentType: file.type || "application/octet-stream" }); return blob.url; } assertWritableLocalStorage(); await mkdir(localUploadsDir, { recursive: true }); const localName = `${Date.now()}-${safeName}`; const localPath = path.join(localUploadsDir, localName); await writeFile(localPath, Buffer.from(await file.arrayBuffer())); return `/uploads/${localName}`; } export async function deleteRemoteImage(imageUrl: string) { if (!hasBlobToken() || !imageUrl.includes(".blob.vercel-storage.com/")) { return; } try { await del(imageUrl); } catch { // Image deletion should not block card edits. } } export function localUploadPath(filename: string) { return path.join(localUploadsDir, safeFileName(filename)); } function assertWritableLocalStorage() { if (!isVercel()) { return; } throw new Error( "BLOB_READ_WRITE_TOKEN is not set for the API deployment. Vercel functions cannot write to .data; connect Vercel Blob to the API project and redeploy." ); } export function publicCards(cards: BoardCard[]) { return cards .filter((card) => card.published) .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt)); } function safeFileName(name: string) { return name.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-"); } async function streamToText(stream: ReadableStream) { const reader = stream.getReader(); const chunks: Uint8Array[] = []; while (true) { const { done, value } = await reader.read(); if (done) { break; } chunks.push(value); } return Buffer.concat(chunks).toString("utf8"); }