Files
Void-Homelab/lib/api/routes/icon_sets.js
root 16e324102e fix(icons): serve icons no-cache so updates propagate (2.5.2)
Icon route used Cache-Control: public, max-age=86400, so changed icons stayed
stuck in CF + browser caches for a day. Switch to no-cache (revalidate; Express
ETag => 304 when unchanged) so icon edits show up immediately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:25:54 +10:00

58 lines
2.6 KiB
JavaScript

// lib/api/routes/icon_sets.js
import { Router } from 'express';
import multer from 'multer';
import { requireOwner } from '../cap.js';
import { asyncWrap, errorMiddleware } from '../errors.js';
import { softAuth } from '../soft_auth.js';
import * as sets from '../../icons/sets.js';
import { processFile, unpackZip, fetchUrl, isZip } from '../../icons/ingest.js';
export const router = Router();
router.use(softAuth);
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 6 * 1024 * 1024, files: 50 } });
// GET /api/icon-sets — list sets + their icons (open; <img> can't send bearer).
router.get('/', asyncWrap(async (_req, res) => res.json(await sets.listSets())));
// GET /api/icon-sets/:set/:file — serve one icon.
router.get('/:set/:file', asyncWrap(async (req, res) => {
let buf;
try { buf = await sets.readIcon(req.params.set, req.params.file); }
catch (e) { return res.status(e.message === 'bad_slug' ? 400 : 404).end(); }
const ct = req.params.file.endsWith('.svg') ? 'image/svg+xml' : 'image/png';
// no-cache => browsers/CF revalidate (304 via Express's ETag when unchanged), so
// icon updates propagate immediately instead of being stuck for a day. Icons are
// tiny, so the revalidation cost is negligible.
res.set('Content-Type', ct).set('Cache-Control', 'no-cache').send(buf);
}));
// POST /api/icon-sets/:set — owner upload: multipart files (incl .zip) and/or { url }.
router.post('/:set', requireOwner, upload.array('files'), asyncWrap(async (req, res) => {
const set = req.params.set;
const items = []; // [{name, buffer}]
for (const f of req.files || []) {
if (isZip(f.buffer)) items.push(...unpackZip(f.buffer));
else items.push(processFile({ name: f.originalname, buffer: f.buffer }));
}
if (req.body?.url) {
const { buffer } = await fetchUrl(req.body.url);
if (isZip(buffer)) items.push(...unpackZip(buffer));
else {
const name = new URL(req.body.url).pathname.split('/').pop() || 'icon.png';
items.push(processFile({ name, buffer }));
}
}
if (!items.length) return res.status(400).json({ error: { code: 'no_icons' } });
for (const it of items) await sets.writeIcon(set, it.name, it.buffer);
res.json((await sets.listSets()).find(s => s.set === set) || { set, icons: [] });
}));
// DELETE /api/icon-sets/:set — owner remove an uploaded set.
router.delete('/:set', requireOwner, asyncWrap(async (req, res) => {
try { await sets.deleteSet(req.params.set); }
catch (e) { return res.status(e.message === 'reserved_set' ? 409 : 400).json({ error: { code: e.message } }); }
res.json({ ok: true });
}));
router.use(errorMiddleware);