Add PDF file attachments to components
- Upload PDFs, rename them (conflict-checked), delete them - Link/unlink files per component (many components can share a file) - File picker dialog: browse existing files, rename inline, upload new - Component detail shows linked files as clickable links - Files stored in data/pdfs/, served at /pdf/:filename - KV prefix: pdf: Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
70
server.mjs
70
server.mjs
@@ -16,9 +16,11 @@ import {
|
||||
list_source_images, get_source_image, add_source_image, delete_source_image,
|
||||
list_grid_images, get_grid_image, set_grid_image, delete_grid_image,
|
||||
list_component_templates, get_component_template, set_component_template, delete_component_template,
|
||||
list_pdfs, get_pdf, set_pdf, delete_pdf,
|
||||
} from './lib/storage.mjs';
|
||||
|
||||
mkdirSync('./data/images', { recursive: true });
|
||||
mkdirSync('./data/pdfs', { recursive: true });
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
@@ -39,6 +41,23 @@ const upload = multer({
|
||||
limits: { fileSize: 20 * 1024 * 1024 },
|
||||
});
|
||||
|
||||
const pdf_upload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: './data/pdfs',
|
||||
filename: (req, file, cb) => cb(null, generate_id() + '.pdf'),
|
||||
}),
|
||||
limits: { fileSize: 50 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.mimetype === 'application/pdf' || extname(file.originalname).toLowerCase() === '.pdf') {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only PDF files are allowed'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
app.use('/pdf', express.static('./data/pdfs'));
|
||||
|
||||
function remove_image_file(img_id) {
|
||||
try { unlinkSync(join('./data/images', img_id)); } catch {}
|
||||
}
|
||||
@@ -116,11 +135,12 @@ app.get('/api/components/:id', (req, res) => {
|
||||
app.put('/api/components/:id', (req, res) => {
|
||||
const existing = get_component(req.params.id);
|
||||
if (!existing) return fail(res, 'not found', 404);
|
||||
const { name, description, fields } = req.body;
|
||||
const { name, description, fields, file_ids } = req.body;
|
||||
const updated = { ...existing, updated_at: Date.now() };
|
||||
if (name !== undefined) updated.name = name.trim();
|
||||
if (description !== undefined) updated.description = description.trim();
|
||||
if (fields !== undefined) updated.fields = fields;
|
||||
if (file_ids !== undefined) updated.file_ids = file_ids;
|
||||
set_component(updated);
|
||||
ok(res, { component: updated });
|
||||
});
|
||||
@@ -456,6 +476,54 @@ app.delete('/api/component-templates/:id', (req, res) => {
|
||||
ok(res);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PDF files
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
app.get('/api/pdfs', (req, res) => {
|
||||
ok(res, { pdfs: list_pdfs() });
|
||||
});
|
||||
|
||||
app.post('/api/pdfs', pdf_upload.single('file'), (req, res) => {
|
||||
if (!req.file) return fail(res, 'no file uploaded');
|
||||
const display_name = (req.body.display_name?.trim() || req.file.originalname).trim();
|
||||
if (list_pdfs().some(p => p.display_name === display_name)) {
|
||||
try { unlinkSync(join('./data/pdfs', req.file.filename)); } catch {}
|
||||
return fail(res, 'a file with that name already exists');
|
||||
}
|
||||
const pdf = {
|
||||
id: generate_id(),
|
||||
filename: req.file.filename,
|
||||
display_name,
|
||||
original_name: req.file.originalname,
|
||||
size: req.file.size,
|
||||
uploaded_at: Date.now(),
|
||||
};
|
||||
set_pdf(pdf);
|
||||
ok(res, { pdf });
|
||||
});
|
||||
|
||||
app.put('/api/pdfs/:id', (req, res) => {
|
||||
const pdf = get_pdf(req.params.id);
|
||||
if (!pdf) return fail(res, 'not found', 404);
|
||||
const display_name = req.body.display_name?.trim();
|
||||
if (!display_name) return fail(res, 'display_name is required');
|
||||
if (list_pdfs().some(p => p.display_name === display_name && p.id !== pdf.id)) {
|
||||
return fail(res, 'a file with that name already exists');
|
||||
}
|
||||
const updated = { ...pdf, display_name };
|
||||
set_pdf(updated);
|
||||
ok(res, { pdf: updated });
|
||||
});
|
||||
|
||||
app.delete('/api/pdfs/:id', (req, res) => {
|
||||
const pdf = get_pdf(req.params.id);
|
||||
if (!pdf) return fail(res, 'not found', 404);
|
||||
try { unlinkSync(join('./data/pdfs', pdf.filename)); } catch {}
|
||||
delete_pdf(req.params.id);
|
||||
ok(res);
|
||||
});
|
||||
|
||||
// SPA fallback — serve index.html for any non-API, non-asset path
|
||||
const INDEX_HTML = new URL('./public/index.html', import.meta.url).pathname;
|
||||
app.get('/{*path}', (req, res) => res.sendFile(INDEX_HTML));
|
||||
|
||||
Reference in New Issue
Block a user