1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Server: Removed controller dependency from route

This commit is contained in:
Laurent Cozic 2021-01-13 21:50:43 +00:00
parent 66a09e5068
commit fc58db5d1a
4 changed files with 84 additions and 29 deletions

View File

@ -7,16 +7,6 @@ import { ChangePagination, PaginatedChanges } from '../../models/ChangeModel';
export default class FileController extends BaseController { export default class FileController extends BaseController {
// Note: this is only used in tests. To create files with no content
// or directories, use postChild()
public async postFile_(sessionId: string, file: File): Promise<File> {
const user = await this.initSession(sessionId);
const fileModel = this.models.file({ userId: user.id });
let newFile = fileModel.fromApiInput(file);
newFile = await fileModel.save(file);
return fileModel.toApiOutput(newFile);
}
public async getFile(sessionId: string, fileId: string): Promise<File> { public async getFile(sessionId: string, fileId: string): Promise<File> {
const user = await this.initSession(sessionId); const user = await this.initSession(sessionId);
const fileModel = this.models.file({ userId: user.id }); const fileModel = this.models.file({ userId: user.id });

View File

@ -1,6 +1,6 @@
import { ErrorNotFound, ErrorMethodNotAllowed, ErrorBadRequest } from '../../utils/errors'; import { ErrorNotFound, ErrorMethodNotAllowed, ErrorBadRequest } from '../../utils/errors';
import { File } from '../../db'; import { File } from '../../db';
import { bodyFields, formParse, headerSessionId } from '../../utils/requestUtils'; import { bodyFields, formParse } from '../../utils/requestUtils';
import { SubPath, Route, respondWithFileContent } from '../../utils/routeUtils'; import { SubPath, Route, respondWithFileContent } from '../../utils/routeUtils';
import { AppContext } from '../../utils/types'; import { AppContext } from '../../utils/types';
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
@ -9,29 +9,61 @@ import { requestChangePagination, requestPagination } from '../../models/utils/p
const route: Route = { const route: Route = {
exec: async function(path: SubPath, ctx: AppContext) { exec: async function(path: SubPath, ctx: AppContext) {
const fileController = ctx.controllers.apiFile();
// console.info(`${ctx.method} ${path.id}${path.link ? `/${path.link}` : ''}`); // console.info(`${ctx.method} ${path.id}${path.link ? `/${path.link}` : ''}`);
// -------------------------------------------
// ROUTE api/files/:id
// -------------------------------------------
if (!path.link) { if (!path.link) {
const fileModel = ctx.models.file({ userId: ctx.owner.id });
const fileId = path.id;
if (ctx.method === 'GET') { if (ctx.method === 'GET') {
return fileController.getFile(headerSessionId(ctx.headers), path.id); const file: File = await fileModel.entityFromItemId(fileId);
const loadedFile = await fileModel.load(file.id);
if (!loadedFile) throw new ErrorNotFound();
return fileModel.toApiOutput(loadedFile);
} }
if (ctx.method === 'PATCH') { if (ctx.method === 'PATCH') {
return fileController.patchFile(headerSessionId(ctx.headers), path.id, await bodyFields(ctx.req)); const inputFile: File = await bodyFields(ctx.req);
const existingFile: File = await fileModel.entityFromItemId(fileId);
const newFile = fileModel.fromApiInput(inputFile);
newFile.id = existingFile.id;
return fileModel.toApiOutput(await fileModel.save(newFile));
} }
if (ctx.method === 'DELETE') { if (ctx.method === 'DELETE') {
return fileController.deleteFile(headerSessionId(ctx.headers), path.id); try {
const file: File = await fileModel.entityFromItemId(fileId, { mustExist: false });
if (!file.id) return;
await fileModel.delete(file.id);
} catch (error) {
if (error instanceof ErrorNotFound) {
// That's ok - a no-op
} else {
throw error;
}
}
return;
} }
throw new ErrorMethodNotAllowed(); throw new ErrorMethodNotAllowed();
} }
// -------------------------------------------
// ROUTE api/files/:id/content
// -------------------------------------------
if (path.link === 'content') { if (path.link === 'content') {
const fileModel = ctx.models.file({ userId: ctx.owner.id });
const fileId = path.id;
if (ctx.method === 'GET') { if (ctx.method === 'GET') {
const file: File = await fileController.getFileContent(headerSessionId(ctx.headers), path.id); let file: File = await fileModel.entityFromItemId(fileId);
file = await fileModel.loadWithContent(file.id);
if (!file) throw new ErrorNotFound();
return respondWithFileContent(ctx.response, file); return respondWithFileContent(ctx.response, file);
} }
@ -39,35 +71,55 @@ const route: Route = {
const result = await formParse(ctx.req); const result = await formParse(ctx.req);
if (!result?.files?.file) throw new ErrorBadRequest('File data is missing'); if (!result?.files?.file) throw new ErrorBadRequest('File data is missing');
const buffer = await fs.readFile(result.files.file.path); const buffer = await fs.readFile(result.files.file.path);
return fileController.putFileContent(headerSessionId(ctx.headers), path.id, buffer);
const file: File = await fileModel.entityFromItemId(fileId, { mustExist: false });
file.content = buffer;
return fileModel.toApiOutput(await fileModel.save(file, { validationRules: { mustBeFile: true } }));
} }
if (ctx.method === 'DELETE') { if (ctx.method === 'DELETE') {
return fileController.deleteFileContent(headerSessionId(ctx.headers), path.id); const file: File = await fileModel.entityFromItemId(fileId, { mustExist: false });
if (!file) return;
file.content = Buffer.alloc(0);
await fileModel.save(file, { validationRules: { mustBeFile: true } });
return;
} }
throw new ErrorMethodNotAllowed(); throw new ErrorMethodNotAllowed();
} }
// -------------------------------------------
// ROUTE api/files/:id/delta
// -------------------------------------------
if (path.link === 'delta') { if (path.link === 'delta') {
if (ctx.method === 'GET') { if (ctx.method === 'GET') {
return fileController.getDelta( const fileModel = ctx.models.file({ userId: ctx.owner.id });
headerSessionId(ctx.headers), const dir: File = await fileModel.entityFromItemId(path.id, { mustExist: true });
path.id, const changeModel = ctx.models.change({ userId: ctx.owner.id });
requestChangePagination(ctx.query) return changeModel.byDirectoryId(dir.id, requestChangePagination(ctx.query));
);
} }
throw new ErrorMethodNotAllowed(); throw new ErrorMethodNotAllowed();
} }
// -------------------------------------------
// ROUTE api/files/:id/children
// -------------------------------------------
if (path.link === 'children') { if (path.link === 'children') {
const fileModel = ctx.models.file({ userId: ctx.owner.id });
if (ctx.method === 'GET') { if (ctx.method === 'GET') {
return fileController.getChildren(headerSessionId(ctx.headers), path.id, requestPagination(ctx.query)); const parent: File = await fileModel.entityFromItemId(path.id);
return fileModel.toApiOutput(await fileModel.childrens(parent.id, requestPagination(ctx.query)));
} }
if (ctx.method === 'POST') { if (ctx.method === 'POST') {
return fileController.postChild(headerSessionId(ctx.headers), path.id, await bodyFields(ctx.req)); const child: File = fileModel.fromApiInput(await bodyFields(ctx.req));
const parent: File = await fileModel.entityFromItemId(path.id);
child.parent_id = parent.id;
return fileModel.toApiOutput(await fileModel.save(child));
} }
throw new ErrorMethodNotAllowed(); throw new ErrorMethodNotAllowed();

View File

@ -153,13 +153,25 @@ export function routeResponseFormat(match: MatchedRoute, rawPath: string): Route
return path.indexOf('api') === 0 || path.indexOf('/api') === 0 ? RouteResponseFormat.Json : RouteResponseFormat.Html; return path.indexOf('api') === 0 || path.indexOf('/api') === 0 ? RouteResponseFormat.Json : RouteResponseFormat.Html;
} }
// In a path such as "/api/files/SOME_ID/content" we want to find:
// - The base path: "api/files"
// - The ID: "SOME_ID"
// - The link: "content"
export function findMatchingRoute(path: string, routes: Routes): MatchedRoute { export function findMatchingRoute(path: string, routes: Routes): MatchedRoute {
const splittedPath = path.split('/'); const splittedPath = path.split('/');
// Because the path starts with "/", we remove the first element, which is
// an empty string. So for example we now have ['api', 'files', 'SOME_ID', 'content'].
splittedPath.splice(0, 1); splittedPath.splice(0, 1);
if (splittedPath.length >= 2) { if (splittedPath.length >= 2) {
// Create the base path, eg. "api/files", to match it to one of the
// routes.s
const basePath = `${splittedPath[0]}/${splittedPath[1]}`; const basePath = `${splittedPath[0]}/${splittedPath[1]}`;
if (routes[basePath]) { if (routes[basePath]) {
// Remove the base path from the array so that parseSubPath() can
// extract the ID and link from the URL. So the array will contain
// at this point: ['SOME_ID', 'content'].
splittedPath.splice(0, 2); splittedPath.splice(0, 2);
return { return {
route: routes[basePath], route: routes[basePath],

View File

@ -65,7 +65,7 @@ export async function beforeEachDb() {
} }
interface AppContextTestOptions { interface AppContextTestOptions {
owner?: User; // owner?: User;
sessionId?: string; sessionId?: string;
request?: any; request?: any;
} }
@ -100,6 +100,8 @@ export async function koaAppContext(options: AppContextTestOptions = null): Prom
const req = httpMocks.createRequest(reqOptions); const req = httpMocks.createRequest(reqOptions);
req.__isMocked = true; req.__isMocked = true;
const owner = options.sessionId ? await models().session().sessionUser(options.sessionId) : null;
const appLogger = Logger.create('AppTest'); const appLogger = Logger.create('AppTest');
// Set type to "any" because the Koa context has many properties and we // Set type to "any" because the Koa context has many properties and we
@ -111,9 +113,8 @@ export async function koaAppContext(options: AppContextTestOptions = null): Prom
appContext.models = models(); appContext.models = models();
appContext.controllers = controllers(); appContext.controllers = controllers();
appContext.appLogger = () => appLogger; appContext.appLogger = () => appLogger;
appContext.path = req.url; appContext.path = req.url;
appContext.owner = options.owner; appContext.owner = owner;
appContext.cookies = new FakeCookies(); appContext.cookies = new FakeCookies();
appContext.request = new FakeRequest(req); appContext.request = new FakeRequest(req);
appContext.response = new FakeResponse(); appContext.response = new FakeResponse();