From 37d51c3b587160efb8c492133d4d0b8566a339e6 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Mon, 28 Mar 2022 16:35:41 +0100 Subject: [PATCH] Plugins: Allow updating a resource via the data API --- packages/app-cli/app/command-apidoc.js | 9 +++++ packages/lib/services/rest/Api.test.ts | 35 ++++++++++++++++++- .../lib/services/rest/routes/resources.ts | 8 +++-- packages/lib/shim-init-node.js | 23 ++++++++++-- 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/packages/app-cli/app/command-apidoc.js b/packages/app-cli/app/command-apidoc.js index c66e45b58..28ff18aa8 100644 --- a/packages/app-cli/app/command-apidoc.js +++ b/packages/app-cli/app/command-apidoc.js @@ -313,6 +313,10 @@ async function fetchAllNotes() { lines.push(''); lines.push('\tcurl -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my resource title"}\' http://localhost:41184/resources'); lines.push(''); + lines.push('Or to **update** a resource:'); + lines.push(''); + lines.push('\tcurl -X PUT -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my modified title"}\' http://localhost:41184/resources/8fe1417d7b184324bf6b0122b76c4696'); + lines.push(''); lines.push('The "data" field is required, while the "props" one is not. If not specified, default values will be used.'); lines.push(''); lines.push('**From a plugin** the syntax to create a resource is also a bit special:'); @@ -368,6 +372,11 @@ async function fetchAllNotes() { lines.push(`Sets the properties of the ${singular} with ID :id`); lines.push(''); + if (model.type === BaseModel.TYPE_RESOURCE) { + lines.push('You may also update the file data by specifying a file (See `POST /resources` example).'); + lines.push(''); + } + lines.push(`## DELETE /${tableName}/:id`); lines.push(''); lines.push(`Deletes the ${singular} with ID :id`); diff --git a/packages/lib/services/rest/Api.test.ts b/packages/lib/services/rest/Api.test.ts index b213b111c..12a3ab4ee 100644 --- a/packages/lib/services/rest/Api.test.ts +++ b/packages/lib/services/rest/Api.test.ts @@ -325,7 +325,7 @@ describe('services_rest_Api', function() { expect(response.body.indexOf(resource.id) >= 0).toBe(true); })); - it('should not compress images uploaded through resource api', (async () => { + it('should not compress images uploaded through resource API', (async () => { const originalImagePath = `${supportDir}/photo-large.png`; await api.route(RequestMethod.POST, 'resources', null, JSON.stringify({ title: 'testing resource', @@ -345,6 +345,39 @@ describe('services_rest_Api', function() { expect(originalImageSize).toEqual(uploadedImageSize); })); + it('should update a resource', (async () => { + await api.route(RequestMethod.POST, 'resources', null, JSON.stringify({ + title: 'resource', + }), [ + { + path: `${supportDir}/photo.jpg`, + }, + ]); + + const resourceV1 = (await Resource.all())[0]; + + await msleep(1); + + await api.route(RequestMethod.PUT, `resources/${resourceV1.id}`, null, JSON.stringify({ + title: 'resource mod', + }), [ + { + path: `${supportDir}/photo-large.png`, + }, + ]); + + const resourceV2 = (await Resource.all())[0]; + + expect(resourceV2.title).toBe('resource mod'); + expect(resourceV2.mime).toBe('image/png'); + expect(resourceV2.file_extension).toBe('png'); + expect(resourceV2.updated_time).toBeGreaterThan(resourceV1.updated_time); + expect(resourceV2.created_time).toBe(resourceV1.created_time); + expect(resourceV2.size).toBeGreaterThan(resourceV1.size); + + expect(resourceV2.size).toBe((await shim.fsDriver().stat(Resource.fullPath(resourceV2))).size); + })); + it('should delete resources', (async () => { const f = await Folder.save({ title: 'mon carnet' }); diff --git a/packages/lib/services/rest/routes/resources.ts b/packages/lib/services/rest/routes/resources.ts index 841c4d003..86a059f5b 100644 --- a/packages/lib/services/rest/routes/resources.ts +++ b/packages/lib/services/rest/routes/resources.ts @@ -47,13 +47,17 @@ export default async function(request: Request, id: string = null, link: string if (link) throw new ErrorNotFound(); } - if (request.method === RequestMethod.POST) { + if (request.method === RequestMethod.POST || request.method === RequestMethod.PUT) { + const isUpdate = request.method === RequestMethod.PUT; + if (!request.files.length) throw new ErrorBadRequest('Resource cannot be created without a file'); + if (isUpdate && !id) throw new ErrorBadRequest('Missing resource ID'); const filePath = request.files[0].path; - const defaultProps = request.bodyJson(readonlyProperties('POST')); + const defaultProps = request.bodyJson(readonlyProperties(request.method)); return shim.createResourceFromPath(filePath, defaultProps, { userSideValidation: true, resizeLargeImages: 'never', + destinationResourceId: isUpdate ? id : '', }); } diff --git a/packages/lib/shim-init-node.js b/packages/lib/shim-init-node.js index 95aaf52cf..277871aa6 100644 --- a/packages/lib/shim-init-node.js +++ b/packages/lib/shim-init-node.js @@ -221,10 +221,15 @@ function shimInit(options = null) { return true; }; + // This is a bit of an ugly method that's used to both create a new resource + // from a file, and update one. To update a resource, pass the + // destinationResourceId option. This method is indirectly tested in + // Api.test.ts. shim.createResourceFromPath = async function(filePath, defaultProps = null, options = null) { options = Object.assign({ resizeLargeImages: 'always', // 'always', 'ask' or 'never' userSideValidation: false, + destinationResourceId: '', }, options); const readChunk = require('read-chunk'); @@ -236,9 +241,10 @@ function shimInit(options = null) { defaultProps = defaultProps ? defaultProps : {}; - const resourceId = defaultProps.id ? defaultProps.id : uuid.create(); + let resourceId = defaultProps.id ? defaultProps.id : uuid.create(); + if (options.destinationResourceId) resourceId = options.destinationResourceId; - const resource = Resource.new(); + let resource = options.destinationResourceId ? {} : Resource.new(); resource.id = resourceId; resource.mime = mimeUtils.fromFilename(filePath); resource.title = basename(filePath); @@ -281,7 +287,18 @@ function shimInit(options = null) { const saveOptions = { isNew: true }; if (options.userSideValidation) saveOptions.userSideValidation = true; - return Resource.save(resource, saveOptions); + + if (options.destinationResourceId) { + saveOptions.isNew = false; + const tempPath = `${targetPath}.tmp`; + await shim.fsDriver().move(targetPath, tempPath); + resource = await Resource.save(resource, saveOptions); + await Resource.updateResourceBlobContent(resource.id, tempPath); + await shim.fsDriver().remove(tempPath); + return resource; + } else { + return Resource.save(resource, saveOptions); + } }; shim.attachFileToNoteBody = async function(noteBody, filePath, position = null, options = null) {