Plugins: Allow updating a resource via the data API

pull/6342/head
Laurent Cozic 2022-03-28 16:35:41 +01:00
parent d5dfecc19f
commit 37d51c3b58
4 changed files with 69 additions and 6 deletions

View File

@ -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`);

View File

@ -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' });

View File

@ -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 : '',
});
}

View File

@ -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;
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) {