feat(api): add multipart file upload, workspace management, and download routes

- Register @fastify/multipart plugin with size limits from env config
- Workspace manager: createWorkspace, getWorkspacePath, cleanupWorkspace
- File validation: magic byte detection, format check, megapixel limit
- POST /api/v1/upload: multipart upload with validation, returns jobId + file metadata
- GET /api/v1/download/:jobId/:filename: serve files with Content-Disposition
- Path traversal guards on all file-serving endpoints
- Add @stirling-image/image-engine and sharp as API dependencies
- Add apiUpload, getDownloadUrl, apiDownloadBlob to web client
This commit is contained in:
Siddharth Kumar Sah
2026-03-22 03:47:54 +08:00
parent ccac5b885c
commit ece341e8c4
8 changed files with 957 additions and 0 deletions
+34
View File
@@ -32,3 +32,37 @@ export function setToken(token: string) {
export function clearToken() {
localStorage.removeItem("stirling-token");
}
// ── File Upload / Download ──────────────────────────────────────
export async function apiUpload(
files: File[],
): Promise<{
jobId: string;
files: Array<{ name: string; size: number; format: string }>;
}> {
const formData = new FormData();
files.forEach((f) => formData.append("files", f));
const res = await fetch("/api/v1/upload", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
body: formData,
});
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
return res.json();
}
export function getDownloadUrl(jobId: string, filename: string): string {
return `/api/v1/download/${jobId}/${filename}`;
}
export async function apiDownloadBlob(
jobId: string,
filename: string,
): Promise<Blob> {
const res = await fetch(getDownloadUrl(jobId, filename), {
headers: { Authorization: `Bearer ${getToken()}` },
});
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
return res.blob();
}