mirror of
https://github.com/zadam/trilium.git
synced 2026-05-07 21:37:44 +02:00
feat(core): port image route
This commit is contained in:
@@ -30,6 +30,13 @@ interface Route {
|
||||
handler: RouteHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Symbol used to mark a result as an already-formatted response,
|
||||
* so that formatResult passes it through without JSON-serializing.
|
||||
* Must match the symbol exported from browser_routes.ts.
|
||||
*/
|
||||
const RAW_RESPONSE = Symbol.for('RAW_RESPONSE');
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
@@ -204,6 +211,26 @@ export class BrowserRouter {
|
||||
* Follows the same patterns as the server's apiResultHandler.
|
||||
*/
|
||||
private formatResult(result: unknown): BrowserResponse {
|
||||
// Handle raw responses (e.g. from image routes that write directly to res)
|
||||
if (result && typeof result === 'object' && RAW_RESPONSE in result) {
|
||||
const raw = result as unknown as { status: number; headers: Record<string, string>; body: unknown };
|
||||
let body: ArrayBuffer | null = null;
|
||||
|
||||
if (raw.body instanceof ArrayBuffer) {
|
||||
body = raw.body;
|
||||
} else if (raw.body instanceof Uint8Array) {
|
||||
body = raw.body.buffer as ArrayBuffer;
|
||||
} else if (typeof raw.body === 'string') {
|
||||
body = encoder.encode(raw.body).buffer as ArrayBuffer;
|
||||
}
|
||||
|
||||
return {
|
||||
status: raw.status,
|
||||
headers: raw.headers,
|
||||
body
|
||||
};
|
||||
}
|
||||
|
||||
// Handle [statusCode, response] format
|
||||
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
|
||||
const [statusCode, response] = result;
|
||||
|
||||
@@ -16,6 +16,13 @@ interface ResultHandlerResponse {
|
||||
setHeader(name: string, value: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Symbol used to mark a result as an already-formatted BrowserResponse,
|
||||
* so that BrowserRouter.formatResult passes it through without JSON-serializing.
|
||||
* Uses Symbol.for() so the same symbol is shared across modules.
|
||||
*/
|
||||
const RAW_RESPONSE = Symbol.for('RAW_RESPONSE');
|
||||
|
||||
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
||||
|
||||
/**
|
||||
@@ -82,12 +89,24 @@ function createApiRoute(router: BrowserRouter, transactional: boolean) {
|
||||
* - The resultHandler is applied to post-process the result (entity conversion, status codes).
|
||||
*/
|
||||
function createRoute(router: BrowserRouter) {
|
||||
return (method: HttpMethod, path: string, _middleware: any[], handler: (req: any) => unknown, resultHandler?: ((req: any, res: any, result: unknown) => unknown) | null) => {
|
||||
return (method: HttpMethod, path: string, _middleware: any[], handler: (req: any, res: any) => unknown, resultHandler?: ((req: any, res: any, result: unknown) => unknown) | null) => {
|
||||
router.register(method, path, (req: BrowserRequest) => {
|
||||
return getContext().init(() => {
|
||||
setContextFromHeaders(req);
|
||||
const expressLikeReq = toExpressLikeReq(req);
|
||||
const result = getSql().transactional(() => handler(expressLikeReq));
|
||||
const mockRes = createMockExpressResponse();
|
||||
const result = getSql().transactional(() => handler(expressLikeReq, mockRes));
|
||||
|
||||
// If the handler used the mock response (e.g. image routes that call res.send()),
|
||||
// return it as a raw response so BrowserRouter doesn't JSON-serialize it.
|
||||
if (mockRes._used) {
|
||||
return {
|
||||
[RAW_RESPONSE]: true as const,
|
||||
status: mockRes._status,
|
||||
headers: mockRes._headers,
|
||||
body: mockRes._body
|
||||
};
|
||||
}
|
||||
|
||||
if (resultHandler) {
|
||||
// Create a minimal response object that captures what apiResultHandler sets.
|
||||
@@ -102,6 +121,42 @@ function createRoute(router: BrowserRouter) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock Express response object that captures calls to set(), send(), sendStatus(), etc.
|
||||
* Used for route handlers (like image routes) that write directly to the response.
|
||||
*/
|
||||
function createMockExpressResponse() {
|
||||
const res = {
|
||||
_used: false,
|
||||
_status: 200,
|
||||
_headers: {} as Record<string, string>,
|
||||
_body: null as unknown,
|
||||
set(name: string, value: string) {
|
||||
res._headers[name] = value;
|
||||
return res;
|
||||
},
|
||||
setHeader(name: string, value: string) {
|
||||
res._headers[name] = value;
|
||||
return res;
|
||||
},
|
||||
status(code: number) {
|
||||
res._status = code;
|
||||
return res;
|
||||
},
|
||||
send(body: unknown) {
|
||||
res._used = true;
|
||||
res._body = body;
|
||||
return res;
|
||||
},
|
||||
sendStatus(code: number) {
|
||||
res._used = true;
|
||||
res._status = code;
|
||||
return res;
|
||||
}
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standalone apiResultHandler matching the server's behavior:
|
||||
* - Converts Becca entities to POJOs
|
||||
@@ -154,7 +209,8 @@ export function registerRoutes(router: BrowserRouter): void {
|
||||
apiRoute,
|
||||
asyncApiRoute: createApiRoute(router, false),
|
||||
apiResultHandler,
|
||||
checkApiAuth
|
||||
checkApiAuth,
|
||||
checkApiAuthOrElectron: checkApiAuth
|
||||
});
|
||||
apiRoute('get', '/bootstrap', bootstrapRoute);
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import etapiTokensApiRoutes from "./api/etapi_tokens.js";
|
||||
import exportRoute from "./api/export.js";
|
||||
import filesRoute from "./api/files.js";
|
||||
import fontsRoute from "./api/fonts.js";
|
||||
import imageRoute from "./api/image.js";
|
||||
import importRoute from "./api/import.js";
|
||||
import loginApiRoute from "./api/login.js";
|
||||
import metricsRoute from "./api/metrics.js";
|
||||
@@ -87,7 +86,8 @@ function register(app: express.Application) {
|
||||
apiRoute,
|
||||
asyncApiRoute,
|
||||
apiResultHandler,
|
||||
checkApiAuth: auth.checkApiAuth
|
||||
checkApiAuth: auth.checkApiAuth,
|
||||
checkApiAuthOrElectron: auth.checkApiAuthOrElectron
|
||||
});
|
||||
|
||||
route(PUT, "/api/notes/:noteId/file", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], filesRoute.updateFile, apiResultHandler);
|
||||
@@ -110,7 +110,6 @@ function register(app: express.Application) {
|
||||
|
||||
// TODO: Bring back attachment uploading
|
||||
// route(PST, "/api/notes/:noteId/attachments/upload", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], attachmentsApiRoute.uploadAttachment, apiResultHandler);
|
||||
route(GET, "/api/attachments/:attachmentId/image/:filename", [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage);
|
||||
route(GET, "/api/attachments/:attachmentId/open", [auth.checkApiAuthOrElectron], filesRoute.openAttachment);
|
||||
asyncRoute(
|
||||
GET,
|
||||
@@ -129,9 +128,10 @@ function register(app: express.Application) {
|
||||
apiRoute(PST, "/api/attachments/:attachmentId/upload-modified-file", filesRoute.uploadModifiedFileToAttachment);
|
||||
route(PUT, "/api/attachments/:attachmentId/file", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], filesRoute.updateAttachment, apiResultHandler);
|
||||
|
||||
route(GET, "/api/revisions/:revisionId/image/:filename", [auth.checkApiAuthOrElectron], imageRoute.returnImageFromRevision);
|
||||
// TODO: Re-enable once ported to core.
|
||||
// route(PUT, "/api/images/:noteId", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.updateImage, apiResultHandler);
|
||||
|
||||
// TODO: Re-enable once we suppourt route()
|
||||
// TODO: Re-enable once we support route()
|
||||
// route(GET, "/api/revisions/:revisionId/download", [auth.checkApiAuthOrElectron], revisionsApiRoute.downloadRevision);
|
||||
|
||||
route(GET, "/api/branches/:branchId/export/:type/:format/:version/:taskId", [auth.checkApiAuthOrElectron], exportRoute.exportBranch);
|
||||
@@ -139,8 +139,6 @@ function register(app: express.Application) {
|
||||
route(PST, "/api/notes/:parentNoteId/attachments-import", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], importRoute.importAttachmentsToNote, apiResultHandler);
|
||||
|
||||
// :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename
|
||||
route(GET, "/api/images/:noteId/:filename", [auth.checkApiAuthOrElectron], imageRoute.returnImageFromNote);
|
||||
route(PUT, "/api/images/:noteId", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.updateImage, apiResultHandler);
|
||||
|
||||
apiRoute(PST, "/api/password/change", passwordApiRoute.changePassword);
|
||||
apiRoute(PST, "/api/password/reset", passwordApiRoute.resetPassword);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs";
|
||||
|
||||
import becca from "../../becca/becca.js";
|
||||
import type BNote from "../../becca/entities/bnote.js";
|
||||
import type BRevision from "../../becca/entities/brevision.js";
|
||||
import imageService from "../../services/image.js";
|
||||
import { RESOURCE_DIR } from "../../services/resource_dir.js";
|
||||
|
||||
function returnImageFromNote(req: Request<{ noteId: string }>, res: Response) {
|
||||
const image = becca.getNote(req.params.noteId);
|
||||
@@ -22,7 +20,8 @@ function returnImageFromRevision(req: Request<{ revisionId: string }>, res: Resp
|
||||
function returnImageInt(image: BNote | BRevision | null, res: Response) {
|
||||
if (!image) {
|
||||
res.set("Content-Type", "image/png");
|
||||
return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`));
|
||||
// return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`));
|
||||
return res.sendStatus(404);
|
||||
} else if (!["image", "canvas", "mermaid", "mindMap", "spreadsheet"].includes(image.type)) {
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
@@ -79,7 +78,8 @@ function returnAttachedImage(req: Request<{ attachmentId: string }>, res: Respon
|
||||
|
||||
if (!attachment) {
|
||||
res.set("Content-Type", "image/png");
|
||||
return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`));
|
||||
// return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`));
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
if (!["image"].includes(attachment.role)) {
|
||||
@@ -22,6 +22,7 @@ import specialNotesRoute from "./api/special_notes";
|
||||
import syncApiRoute from "./api/sync";
|
||||
import autocompleteApiRoute from "./api/autocomplete";
|
||||
import similarNotesRoute from "./api/similar_notes";
|
||||
import imageRoute from "./api/image";
|
||||
|
||||
// TODO: Deduplicate with routes.ts
|
||||
const GET = "get",
|
||||
@@ -36,9 +37,10 @@ interface SharedApiRoutesContext {
|
||||
asyncApiRoute: any;
|
||||
checkApiAuth: any;
|
||||
apiResultHandler: any;
|
||||
checkApiAuthOrElectron: any;
|
||||
}
|
||||
|
||||
export function buildSharedApiRoutes({ route, apiRoute, asyncApiRoute, checkApiAuth, apiResultHandler }: SharedApiRoutesContext) {
|
||||
export function buildSharedApiRoutes({ route, apiRoute, asyncApiRoute, checkApiAuth, apiResultHandler, checkApiAuthOrElectron }: SharedApiRoutesContext) {
|
||||
apiRoute(GET, '/api/tree', treeApiRoute.getTree);
|
||||
apiRoute(PST, '/api/tree/load', treeApiRoute.load);
|
||||
|
||||
@@ -105,6 +107,10 @@ export function buildSharedApiRoutes({ route, apiRoute, asyncApiRoute, checkApiA
|
||||
apiRoute(PUT, "/api/branches/:branchId/set-prefix", branchesApiRoute.setPrefix);
|
||||
apiRoute(PUT, "/api/branches/set-prefix-batch", branchesApiRoute.setPrefixBatch);
|
||||
|
||||
route(GET, "/api/revisions/:revisionId/image/:filename", [checkApiAuthOrElectron], imageRoute.returnImageFromRevision);
|
||||
route(GET, "/api/attachments/:attachmentId/image/:filename", [checkApiAuthOrElectron], imageRoute.returnAttachedImage);
|
||||
route(GET, "/api/images/:noteId/:filename", [checkApiAuthOrElectron], imageRoute.returnImageFromNote);
|
||||
|
||||
asyncApiRoute(PST, "/api/sync/test", syncApiRoute.testSync);
|
||||
asyncApiRoute(PST, "/api/sync/now", syncApiRoute.syncNow);
|
||||
apiRoute(PST, "/api/sync/fill-entity-changes", syncApiRoute.fillEntityChanges);
|
||||
|
||||
Reference in New Issue
Block a user