feat(icons): filesystem icon-set store (bundled read-only + uploads)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
52
lib/icons/sets.js
Normal file
52
lib/icons/sets.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// 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|jpg|jpeg)$/;
|
||||
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 });
|
||||
}
|
||||
Reference in New Issue
Block a user