File Uploads
ZiWound accepts images, videos, and documents through multipart uploads routed into a static directory and served back under /uploads. This page shows the full round trip. The framework side is covered in Server and Body Parsing.
Serving uploadsโ
The server is configured with a static path in back/mod.ts:
coreApp.runServer({
port: Number(APP_PORT),
staticPath: ["/uploads"], // files under back/uploads/ are served at /uploads/...
});
Uploaded files land on disk under back/uploads/{image|video|docs}/, and become publicly reachable at http://<host>:1406/uploads/{image}/<filename>.
The upload actโ
Uploading is a multipart request, not JSON. Lesan parses the form and places the file in details.set.formData (see parsBody in checkWants). The act validates type + size, routes to the right sub-directory, and stores the metadata row:
// src/file/uploadFile/uploadFile.fn.ts (simplified)
export const uploadFileFn: ActFn = async (body) => {
const { set } = body.details;
const file = set.formData; // the parsed multipart file
const fileType = set.type; // "image" | "video" | "docs"
const maxSize = fileType === "docs" ? 50_000_000 : 15_000_000;
if (file.size > maxSize) throwError("file is too large");
const name = `${crypto.randomUUID()}.${extension}`;
await writeFile(`${UPLOAD_DIR}/${fileType}/${name}`, file.content);
return await files.insertOne({
doc: {
name, mimeType: file.mimeType, size: file.size,
type: fileType, alt_text: set.alt_text,
uploader: contextFns.getUser()._id, // owner
},
projection: get,
});
};
Validator:
export const uploadFileValidator = () =>
object({
set: object({ type: enums(["image", "video", "docs"]), alt_text: optional(string()) }),
get: coreApp.schemas.selectStruct("file", { ... }),
});
The act itself is protected by the auth chain: preAct: [setUser, grantAccess(["Reporter", "Editor", "Manager", "Admin"])].
The frontend sideโ
The Next.js frontend uploads with FormData and stores the returned file _id:
// front/src/actions/upload.ts (simplified)
const form = new FormData();
form.append("file", blob, filename);
form.append("type", "image");
const { body } = await fetch(`${LESAN_URL}/lesan`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: form,
});
const file = body.body; // { _id, name, mimeType, size, type, ... }
Then the file _id is attached where needed โ as user.avatar, report.documents[].documentFiles, or a report's cover image โ via the owning model's updateRelations act:
await callLesan("updateUserRelations", {
set: { _id: userId, avatar: fileId }, // Lesan wires the reverse list too
get: { avatar: true },
});
Checklist for multipart in Lesanโ
- Set
staticPath: ["/uploads"]inrunServerso files are served. - Accept the raw file through
details.set.formDatain the act. - Validate
type(enum) andsizein the act โ the JSON validator can't see the binary. - Persist the file to disk with a unique name (UUID).
- Insert a
filemetadata row, tagged withuploader. - Reference the file
_idfrom the owning model'supdateRelationsact โ Lesan keeps the reverse list in sync.
Next: Act Structure.