public
anord
read
Ledger
Why work hard when you can work easier?
Languages
Repository composition by tracked source files.
TypeScript
86%
CSS
10%
SQL
3%
Shell
1%
HTML
0%
Trace
apps/api/src/http/routes/attachments.ts
Trace helps you understand code history line by line. See who changed each line, when it changed, and which commit introduced it.
Author
Date
Commit
Line
Code
1
import { mkdir, writeFile } from "node:fs/promises";
2
import path from "node:path";
3
import crypto from "node:crypto";
4
import { Router } from "express";
5
import { z } from "zod";
6
import { requireEditor } from "../middleware/auth.js";
7
import { env } from "../../config/env.js";
8
import { pool } from "../../db/pool.js";
9
import { getPageMetadata } from "../../services/pages.js";
11
const attachmentSchema = z.object({
12
pageId: z.string().uuid(),
13
fileName: z.string().min(1),
14
contentType: z.string().min(3),
15
base64Data: z.string().min(1)
16
});
18
export const attachmentsRouter = Router();
20
attachmentsRouter.get("/", async (req, res) => {
21
const pageId = String(req.query.pageId ?? "");
22
const page = await getPageMetadata(pageId, req.user ?? null);
23
if (!page) {
24
return res.status(404).json({ error: "Page not found" });
25
}
27
const result = await pool.query(
28
`SELECT id, page_id, file_name, content_type, storage_path, size_bytes, created_at
29
FROM attachments
30
WHERE page_id = $1
31
ORDER BY created_at DESC`,
32
[pageId]
33
);
35
return res.json({ attachments: result.rows });
36
});
38
attachmentsRouter.post("/", requireEditor, async (req, res) => {
39
const input = attachmentSchema.parse(req.body);
40
const page = await getPageMetadata(input.pageId, req.user ?? null);
41
if (!page) {
42
return res.status(404).json({ error: "Page not found" });
43
}
45
if (env.STORAGE_PROVIDER !== "local") {
46
return res.status(400).json({ error: "This Ledger deployment is configured for local storage only." });
47
}
49
const buffer = Buffer.from(input.base64Data, "base64");
50
const fileId = crypto.randomUUID();
51
const ext = path.extname(input.fileName);
52
const fileName = `${fileId}${ext}`;
54
await mkdir(env.LOCAL_STORAGE_ROOT, { recursive: true });
55
const storagePath = path.join(env.LOCAL_STORAGE_ROOT, fileName);
56
await writeFile(storagePath, buffer);
58
const created = await pool.query(
59
`
60
INSERT INTO attachments (page_id, file_name, content_type, storage_path, size_bytes, created_by_user_id)
61
VALUES ($1, $2, $3, $4, $5, $6)
62
RETURNING id, file_name, storage_path, size_bytes
63
`,
64
[input.pageId, input.fileName, input.contentType, storagePath, buffer.length, req.user!.id]
65
);
67
return res.status(201).json(created.rows[0]);
68
});