mirror of
https://github.com/zadam/trilium.git
synced 2026-05-06 22:17:01 +02:00
chore(standalone): upload middleware with error handling
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface File {
|
||||
originalname: string;
|
||||
mimetype: string;
|
||||
buffer: string | Buffer;
|
||||
buffer: string | Buffer | Uint8Array;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user