chore(standalone): upload middleware with error handling

This commit is contained in:
Elian Doran
2026-03-27 16:40:23 +02:00
parent f81369d643
commit f069b41df6
7 changed files with 54 additions and 11 deletions

View File

@@ -5,6 +5,12 @@
import { getContext, routes } from "@triliumnext/core";
export interface UploadedFile {
originalname: string;
mimetype: string;
buffer: Uint8Array;
}
export interface BrowserRequest {
method: string;
url: string;
@@ -13,6 +19,7 @@ export interface BrowserRequest {
query: Record<string, string | undefined>;
headers?: Record<string, string>;
body?: unknown;
file?: UploadedFile;
}
export interface BrowserResponse {
@@ -154,8 +161,9 @@ export class BrowserRouter {
const query = parseQuery(url.search);
const upperMethod = method.toUpperCase();
// Parse JSON body if it's an ArrayBuffer and content-type suggests JSON
// Parse body based on content-type
let parsedBody = body;
let uploadedFile: UploadedFile | undefined;
if (body instanceof ArrayBuffer && headers) {
const contentType = headers['content-type'] || headers['Content-Type'] || '';
if (contentType.includes('application/json')) {
@@ -166,9 +174,31 @@ export class BrowserRouter {
}
} catch (e) {
console.warn('[Router] Failed to parse JSON body:', e);
// Keep original body if JSON parsing fails
parsedBody = body;
}
} else if (contentType.includes('multipart/form-data')) {
try {
// Reconstruct a Response so we can use the native FormData parser
const response = new Response(body, { headers: { 'content-type': contentType } });
const formData = await response.formData();
const formFields: Record<string, string> = {};
for (const [key, value] of formData.entries()) {
if (typeof value === 'string') {
formFields[key] = value;
} else {
// File field (Blob) — multer uses the field name "upload"
const fileBuffer = new Uint8Array(await value.arrayBuffer());
uploadedFile = {
originalname: value.name,
mimetype: value.type || 'application/octet-stream',
buffer: fileBuffer
};
}
}
parsedBody = formFields;
} catch (e) {
console.warn('[Router] Failed to parse multipart body:', e);
}
}
}
// Find matching route
@@ -191,7 +221,8 @@ export class BrowserRouter {
params,
query,
headers: headers ?? {},
body: parsedBody
body: parsedBody,
file: uploadedFile
};
try {

View File

@@ -35,6 +35,7 @@ function toExpressLikeReq(req: BrowserRequest) {
body: req.body,
headers: req.headers ?? {},
method: req.method,
file: req.file,
get originalUrl() { return req.url; }
};
}
@@ -227,7 +228,9 @@ export function registerRoutes(router: BrowserRouter): void {
checkApiAuthOrElectron: noopMiddleware,
checkAppNotInitialized,
checkCredentials: noopMiddleware,
loginRateLimiter: noopMiddleware
loginRateLimiter: noopMiddleware,
uploadMiddlewareWithErrorHandling: noopMiddleware,
csrfMiddleware: noopMiddleware
});
apiRoute('get', '/bootstrap', bootstrapRoute);

View File

@@ -88,7 +88,9 @@ function register(app: express.Application) {
checkApiAuthOrElectron: auth.checkApiAuthOrElectron,
checkAppNotInitialized: auth.checkAppNotInitialized,
checkCredentials: auth.checkCredentials,
loginRateLimiter
loginRateLimiter,
uploadMiddlewareWithErrorHandling,
csrfMiddleware
});
route(PUT, "/api/notes/:noteId/file", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], filesRoute.updateFile, apiResultHandler);

View File

@@ -1,5 +1,10 @@
import type { Request } from "express";
import path from "path";
import type { File } from "../../services/import/common.js";
interface ImportRequest<P> extends Request<P> {
file?: File;
}
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
@@ -14,7 +19,7 @@ import * as cls from "../../services/context.js";
import { ValidationError } from "../../errors.js";
import becca_loader from "../../becca/becca_loader.js";
async function importNotesToBranch(req: Request<{ parentNoteId: string }>) {
async function importNotesToBranch(req: ImportRequest<{ parentNoteId: string }>) {
const { parentNoteId } = req.params;
const { taskId, last } = req.body;
@@ -66,7 +71,7 @@ async function importNotesToBranch(req: Request<{ parentNoteId: string }>) {
return importResult;
}
} else {
note = await singleImportService.importSingleFile(taskContext, file, parentNote);
note = singleImportService.importSingleFile(taskContext, file, parentNote);
}
} catch (e: unknown) {
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
@@ -100,7 +105,7 @@ async function importNotesToBranch(req: Request<{ parentNoteId: string }>) {
return note.getPojo();
}
function importAttachmentsToNote(req: Request<{ parentNoteId: string }>) {
function importAttachmentsToNote(req: ImportRequest<{ parentNoteId: string }>) {
const { parentNoteId } = req.params;
const { taskId, last } = req.body;

View File

@@ -45,9 +45,11 @@ interface SharedApiRoutesContext {
checkAppNotInitialized: any;
loginRateLimiter: any;
checkCredentials: any;
uploadMiddlewareWithErrorHandling: any;
csrfMiddleware: any;
}
export function buildSharedApiRoutes({ route, asyncRoute, apiRoute, asyncApiRoute, checkApiAuth, apiResultHandler, checkApiAuthOrElectron, checkAppNotInitialized, checkCredentials, loginRateLimiter }: SharedApiRoutesContext) {
export function buildSharedApiRoutes({ route, asyncRoute, apiRoute, asyncApiRoute, checkApiAuth, apiResultHandler, checkApiAuthOrElectron, checkAppNotInitialized, checkCredentials, loginRateLimiter, uploadMiddlewareWithErrorHandling, csrfMiddleware }: SharedApiRoutesContext) {
apiRoute(GET, '/api/tree', treeApiRoute.getTree);
apiRoute(PST, '/api/tree/load', treeApiRoute.load);

View File

@@ -1,5 +1,5 @@
export interface File {
originalname: string;
mimetype: string;
buffer: string | Buffer;
buffer: string | Buffer | Uint8Array;
}

View File

@@ -27,7 +27,7 @@ interface OpmlOutline {
outline: OpmlOutline[];
}
async function importOpml(taskContext: TaskContext<"importNotes">, fileBuffer: string | Buffer, parentNote: BNote) {
async function importOpml(taskContext: TaskContext<"importNotes">, fileBuffer: string | Uint8Array, parentNote: BNote) {
const xml = await new Promise<OpmlXml>((resolve, reject) => {
parseString(fileBuffer, (err: any, result: OpmlXml) => {
if (err) {