From d4861c42cdbcbf163b4b8604f47097dcda8bf293 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 24 Jul 2023 01:31:17 +1000 Subject: [PATCH 1/3] feat: add file get triggers --- spec/CloudCode.spec.js | 102 +++++++++++++++++++++++++++++++++++++ src/Routers/FilesRouter.js | 75 +++++++++++++++++++-------- 2 files changed, 157 insertions(+), 20 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 90ab313826..116a2e528f 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3531,6 +3531,108 @@ describe('afterLogin hook', () => { }); describe('saveFile hooks', () => { + fit('find hooks should run', async () => { + await reconfigureServer({ silent: false }); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + const user = await Parse.User.signUp('username', 'password'); + const hooks = { + beforeFind(req) { + expect(req).toBeDefined(); + expect(req.file).toBeDefined(); + expect(req.triggerName).toBe('beforeFind'); + expect(req.master).toBeFalse(); + expect(req.log).toBeDefined(); + }, + afterFind(req) { + expect(req).toBeDefined(); + expect(req.file).toBeDefined(); + expect(req.triggerName).toBe('afterFind'); + expect(req.master).toBeFalse(); + expect(req.log).toBeDefined(); + }, + }; + for (const hook in hooks) { + spyOn(hooks, hook).and.callThrough(); + Parse.Cloud[hook](Parse.File, hooks[hook]); + } + await request({ + url: file.url(), + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': user.getSessionToken(), + }, + }); + for (const hook in hooks) { + expect(hooks[hook]).toHaveBeenCalled(); + } + }); + + fit('beforeFind can throw', async () => { + await reconfigureServer({ silent: false }); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + const user = await Parse.User.signUp('username', 'password'); + const hooks = { + beforeFind() { + throw 'unauthorized'; + }, + afterFind() {}, + }; + for (const hook in hooks) { + spyOn(hooks, hook).and.callThrough(); + Parse.Cloud[hook](Parse.File, hooks[hook]); + } + await expectAsync( + request({ + url: file.url(), + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': user.getSessionToken(), + }, + }).catch(e => { + throw new Parse.Error(e.data.code, e.data.error); + }) + ).toBeRejectedWith(new Parse.Error(Parse.Error.SCRIPT_FAILED, 'unauthorized')); + + expect(hooks.beforeFind).toHaveBeenCalled(); + expect(hooks.afterFind).not.toHaveBeenCalled(); + }); + + fit('afterFind can throw', async () => { + await reconfigureServer({ silent: false }); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + const user = await Parse.User.signUp('username', 'password'); + const hooks = { + beforeFind() {}, + afterFind() { + throw 'unauthorized'; + }, + }; + for (const hook in hooks) { + spyOn(hooks, hook).and.callThrough(); + Parse.Cloud[hook](Parse.File, hooks[hook]); + } + await expectAsync( + request({ + url: file.url(), + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': user.getSessionToken(), + }, + }).catch(e => { + throw new Parse.Error(e.data.code, e.data.error); + }) + ).toBeRejectedWith(new Parse.Error(Parse.Error.SCRIPT_FAILED, 'unauthorized')); + for (const hook in hooks) { + expect(hooks[hook]).toHaveBeenCalled(); + } + }); + it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => { await reconfigureServer({ filesAdapter: mockAdapter }); const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough(); diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index cbb59fdcdd..a94ff02a81 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -67,7 +67,7 @@ export class FilesRouter { return router; } - getHandler(req, res) { + async getHandler(req, res) { const config = Config.get(req.params.appId); if (!config) { res.status(403); @@ -75,29 +75,64 @@ export class FilesRouter { res.json({ code: err.code, error: err.message }); return; } - const filesController = config.filesController; - const filename = req.params.filename; - const contentType = mime.getType(filename); - if (isFileStreamable(req, filesController)) { - filesController.handleFileStream(config, filename, req, res, contentType).catch(() => { - res.status(404); - res.set('Content-Type', 'text/plain'); - res.end('File not found.'); - }); - } else { - filesController - .getFileData(config, filename) - .then(data => { - res.status(200); - res.set('Content-Type', contentType); - res.set('Content-Length', data.length); - res.end(data); - }) - .catch(() => { + + let filename = req.params.filename; + try { + const filesController = config.filesController; + let contentType = mime.getType(filename); + let file = new Parse.File(filename, { base64: '' }, contentType); + const triggerResult = await triggers.maybeRunFileTrigger( + triggers.Types.beforeFind, + { file }, + config, + req.auth + ); + if (triggerResult?.file?._name) { + filename = triggerResult?.file?._name; + contentType = mime.getType(filename); + } + + if (isFileStreamable(req, filesController)) { + filesController.handleFileStream(config, filename, req, res, contentType).catch(() => { res.status(404); res.set('Content-Type', 'text/plain'); res.end('File not found.'); }); + return; + } + + let data = await filesController.getFileData(config, filename).catch(() => { + res.status(404); + res.set('Content-Type', 'text/plain'); + res.end('File not found.'); + }); + if (!data) { + return; + } + file = new Parse.File(filename, { base64: data.toString('base64') }, contentType); + const afterFind = await triggers.maybeRunFileTrigger( + triggers.Types.afterFind, + { file }, + config, + req.auth + ); + + if (afterFind?.file) { + contentType = mime.getType(afterFind.file._name); + data = Buffer.from(afterFind.file._data, 'utf8'); + } + + res.status(200); + res.set('Content-Type', contentType); + res.set('Content-Length', data.length); + res.end(data); + } catch (e) { + const err = triggers.resolveError(e, { + code: Parse.Error.SCRIPT_FAILED, + message: `Could not find file: ${filename}.`, + }); + res.status(403); + res.json({ code: err.code, error: err.message }); } } From a8f0d1ead91a4b2a96b872f1671a673ab11817bf Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 24 Jul 2023 14:50:58 +1000 Subject: [PATCH 2/3] add download --- spec/CloudCode.spec.js | 28 ++++++++++++++++++++++------ src/Routers/FilesRouter.js | 5 ++++- src/triggers.js | 3 +++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 116a2e528f..99be084b57 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3531,8 +3531,7 @@ describe('afterLogin hook', () => { }); describe('saveFile hooks', () => { - fit('find hooks should run', async () => { - await reconfigureServer({ silent: false }); + it('find hooks should run', async () => { const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); await file.save({ useMasterKey: true }); const user = await Parse.User.signUp('username', 'password'); @@ -3550,6 +3549,7 @@ describe('saveFile hooks', () => { expect(req.triggerName).toBe('afterFind'); expect(req.master).toBeFalse(); expect(req.log).toBeDefined(); + expect(req.forceDownload).toBeFalse(); }, }; for (const hook in hooks) { @@ -3569,8 +3569,7 @@ describe('saveFile hooks', () => { } }); - fit('beforeFind can throw', async () => { - await reconfigureServer({ silent: false }); + it('beforeFind can throw', async () => { const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); await file.save({ useMasterKey: true }); const user = await Parse.User.signUp('username', 'password'); @@ -3601,8 +3600,7 @@ describe('saveFile hooks', () => { expect(hooks.afterFind).not.toHaveBeenCalled(); }); - fit('afterFind can throw', async () => { - await reconfigureServer({ silent: false }); + it('afterFind can throw', async () => { const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); await file.save({ useMasterKey: true }); const user = await Parse.User.signUp('username', 'password'); @@ -3633,6 +3631,24 @@ describe('saveFile hooks', () => { } }); + it('can force download', async () => { + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + const user = await Parse.User.signUp('username', 'password'); + Parse.Cloud.afterFind(Parse.File, req => { + req.forceDownload = true; + }); + const response = await request({ + url: file.url(), + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': user.getSessionToken(), + }, + }); + expect(response.headers['content-disposition']).toBe(`attachment;filename=${file._name}`); + }); + it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => { await reconfigureServer({ filesAdapter: mockAdapter }); const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough(); diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index a94ff02a81..dd8d3f997c 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -112,7 +112,7 @@ export class FilesRouter { file = new Parse.File(filename, { base64: data.toString('base64') }, contentType); const afterFind = await triggers.maybeRunFileTrigger( triggers.Types.afterFind, - { file }, + { file, forceDownload: false }, config, req.auth ); @@ -125,6 +125,9 @@ export class FilesRouter { res.status(200); res.set('Content-Type', contentType); res.set('Content-Length', data.length); + if (afterFind.forceDownload) { + res.set('Content-Disposition', `attachment;filename=${afterFind.file._name}`); + } res.end(data); } catch (e) { const err = triggers.resolveError(e, { diff --git a/src/triggers.js b/src/triggers.js index b5f11435df..42a91dcca3 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -983,6 +983,9 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) return fileObject; } const result = await fileTrigger(request); + if (request.forceDownload) { + fileObject.forceDownload = true; + } logTriggerSuccessBeforeHook( triggerType, 'Parse.File', From 0d99225d18946b7d9299b0b3f0e4f68bd39a6af6 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 24 Jul 2023 16:32:55 +1000 Subject: [PATCH 3/3] Update FilesRouter.js --- src/Routers/FilesRouter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index dd8d3f997c..0aa2732f93 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -119,7 +119,7 @@ export class FilesRouter { if (afterFind?.file) { contentType = mime.getType(afterFind.file._name); - data = Buffer.from(afterFind.file._data, 'utf8'); + data = Buffer.from(afterFind.file._data, 'base64'); } res.status(200);