feat(api): /api/icon-sets — list/serve/upload(zip,url)/delete
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
51
lib/api/routes/icon_sets.js
Normal file
51
lib/api/routes/icon_sets.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// lib/api/routes/icon_sets.js
|
||||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import { requireOwner } from '../cap.js';
|
||||
import { asyncWrap } from '../errors.js';
|
||||
import * as sets from '../../icons/sets.js';
|
||||
import { processFile, unpackZip, fetchUrl, isZip } from '../../icons/ingest.js';
|
||||
|
||||
export const router = Router();
|
||||
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'
|
||||
: req.params.file.endsWith('.png') ? 'image/png' : 'image/jpeg';
|
||||
res.set('Content-Type', ct).set('Cache-Control', 'public, max-age=86400').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 });
|
||||
}));
|
||||
@@ -7,6 +7,7 @@ import * as queue from './lib/jobs/queue.js';
|
||||
import { registerWorkers } from './lib/jobs/index.js';
|
||||
import { router as ingestRouter } from './lib/api/routes/ingest.js';
|
||||
import { router as iconsRouter } from './lib/api/routes/icons.js';
|
||||
import { router as iconSetsRouter } from './lib/api/routes/icon_sets.js';
|
||||
import { router as devicesRouter } from './lib/api/routes/devices.js';
|
||||
import { startCron } from './lib/cron/index.js';
|
||||
import { seedFromConfig } from './lib/health/registry.js';
|
||||
@@ -53,6 +54,10 @@ export function createApp() {
|
||||
// slugs are sanitized to [a-z0-9-] to prevent path traversal.
|
||||
app.use('/api/icons', iconsRouter);
|
||||
|
||||
// /api/icon-sets/* — GET routes are open (same <img> reason as above);
|
||||
// POST/DELETE are protected by requireOwner inside the router.
|
||||
app.use('/api/icon-sets', iconSetsRouter);
|
||||
|
||||
// /api/devices — band data is public (like the static devices.json it replaces);
|
||||
// discovered/edit/scan sub-routes use requireOwner (401/403) internally.
|
||||
app.use('/api/devices', devicesRouter);
|
||||
|
||||
Reference in New Issue
Block a user