TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import 'server-only';2import { mkdir, writeFile, readFile, unlink } from 'node:fs/promises';3import path from 'node:path';4import { newId } from '@rareindex/shared';5import { db, uploads } from '@/lib/db';67const MAX_BYTES: Record<string, number> = { avatar: 2 * 1024 * 1024, item_photo: 8 * 1024 * 1024 };8const ALLOWED: Record<string, string> = { 'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp' };910function uploadsRoot(): string {11 return path.resolve(process.env.RI_DATA_DIR ?? './data', 'uploads');12}1314/** Sniff the real type from magic bytes; never trust the declared MIME. */15function sniff(buf: Buffer): string | null {16 if (buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return 'image/jpeg';17 if (buf.length > 8 && buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) return 'image/png';18 if (buf.length > 12 && buf.subarray(0, 4).toString('ascii') === 'RIFF' && buf.subarray(8, 12).toString('ascii') === 'WEBP') return 'image/webp';19 return null;20}2122/** Cheap dimension probe for PNG/JPEG/WebP (no image library needed). */23function dimensions(buf: Buffer, mime: string): { width: number; height: number } | null {24 try {25 if (mime === 'image/png') return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };26 if (mime === 'image/jpeg') {27 let i = 2;28 while (i < buf.length) {29 if (buf[i] !== 0xff) return null;30 const marker = buf[i + 1]!;31 const len = buf.readUInt16BE(i + 2);32 if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) return { height: buf.readUInt16BE(i + 5), width: buf.readUInt16BE(i + 7) };33 i += 2 + len;34 }35 }36 if (mime === 'image/webp') {37 const chunk = buf.subarray(12, 16).toString('ascii');38 if (chunk === 'VP8X') return { width: 1 + buf.readUIntLE(24, 3), height: 1 + buf.readUIntLE(27, 3) };39 if (chunk === 'VP8 ') return { width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff };40 }41 } catch {42 return null;43 }44 return null;45}4647export async function storeUpload(userId: string, kind: 'avatar' | 'item_photo', file: File): Promise<{ id: string; url: string } | { error: string }> {48 const max = MAX_BYTES[kind]!;49 if (file.size === 0) return { error: 'Empty file.' };50 if (file.size > max) return { error: `File too large (max ${Math.round(max / 1024 / 1024)} MB).` };51 const buf = Buffer.from(await file.arrayBuffer());52 const mime = sniff(buf);53 if (!mime || !ALLOWED[mime]) return { error: 'Only JPEG, PNG and WebP images are accepted.' };54 const id = newId('image');55 const rel = path.join(userId, `${id}.${ALLOWED[mime]}`);56 const abs = path.join(uploadsRoot(), rel);57 await mkdir(path.dirname(abs), { recursive: true });58 await writeFile(abs, buf);59 const dim = dimensions(buf, mime);60 await db().insert(uploads).values({ id, userId, kind, path: rel, mime, bytes: buf.length, width: dim?.width ?? null, height: dim?.height ?? null });61 return { id, url: `/api/account/uploads/${id}` };62}6364export async function readUpload(id: string): Promise<{ buf: Buffer; mime: string; userId: string } | null> {65 const rows = await db().select().from(uploads).where((await import('@/lib/db')).eq(uploads.id, id)).limit(1);66 const row = rows[0];67 if (!row) return null;68 try {69 const buf = await readFile(path.join(uploadsRoot(), row.path));70 return { buf, mime: row.mime, userId: row.userId };71 } catch {72 return null;73 }74}7576export async function deleteUpload(userId: string, id: string): Promise<void> {77 const { and, eq } = await import('@/lib/db');78 const rows = await db().select().from(uploads).where(and(eq(uploads.id, id), eq(uploads.userId, userId))).limit(1);79 const row = rows[0];80 if (!row) return;81 await unlink(path.join(uploadsRoot(), row.path)).catch(() => {});82 await db().delete(uploads).where(eq(uploads.id, id));83}84