// lib/icons/sets.js import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; const BUNDLED_SET = 'devices'; // read-only, ships in public/icons/devices let setsDir = process.env.ICON_SETS_DIR || '/var/lib/void/icon-sets'; let bundledDir = path.resolve('public/icons/devices'); export function _setDirs({ setsDir: s, bundledDir: b }) { if (s) setsDir = s; if (b) bundledDir = b; } const SLUG = /^[a-z0-9-]+$/; const FILE = /^[a-z0-9-]+\.(svg|png)$/; function okSet(s) { return SLUG.test(s); } async function listDir(dir) { try { return (await readdir(dir)).filter(f => FILE.test(f)).sort(); } catch { return []; } } export async function listSets() { const out = [{ set: BUNDLED_SET, readonly: true, icons: await listDir(bundledDir) }]; let uploaded = []; try { uploaded = await readdir(setsDir, { withFileTypes: true }); } catch { /* none yet */ } for (const d of uploaded) { if (d.isDirectory() && okSet(d.name) && d.name !== BUNDLED_SET) { out.push({ set: d.name, readonly: false, icons: await listDir(path.join(setsDir, d.name)) }); } } return out; } // Resolve an on-disk path for serving. Throws on bad slugs. export function iconPath(set, file) { if (!okSet(set) || !FILE.test(file)) throw new Error('bad_slug'); return set === BUNDLED_SET ? path.join(bundledDir, file) : path.join(setsDir, set, file); } export async function readIcon(set, file) { return readFile(iconPath(set, file)); } export async function writeIcon(set, name, buffer) { if (set === BUNDLED_SET) throw new Error('reserved_set'); if (!okSet(set) || !FILE.test(name)) throw new Error('bad_slug'); const dir = path.join(setsDir, set); await mkdir(dir, { recursive: true }); await writeFile(path.join(dir, name), buffer); } export async function deleteSet(set) { if (set === BUNDLED_SET) throw new Error('reserved_set'); if (!okSet(set)) throw new Error('bad_slug'); await rm(path.join(setsDir, set), { recursive: true, force: true }); }