diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index b65d8f3492..7287dd14db 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -33,6 +33,95 @@ describe('Parse.File testing', () => { }); }); + it('supports REST end-to-end file create, read, delete, read', done => { + var headers = { + 'Content-Type': 'image/jpeg', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/testfile.txt', + body: 'check one two', + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.name).toMatch(/_testfile.txt$/); + expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*testfile.txt$/); + request.get(b.url, (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('check one two'); + request.del({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Master-Key': 'test' + }, + url: 'http://localhost:8378/1/files/' + b.name + }, (error, response, body) => { + expect(error).toBe(null); + expect(response.statusCode).toEqual(200); + request.get({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }, + url: b.url + }, (error, response, body) => { + expect(error).toBe(null); + expect(response.statusCode).toEqual(404); + done(); + }); + }); + }); + }); + }); + + it('blocks file deletions with missing or incorrect master-key header', done => { + var headers = { + 'Content-Type': 'image/jpeg', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/thefile.jpg', + body: 'the file body' + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*thefile.jpg$/); + // missing X-Parse-Master-Key header + request.del({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }, + url: 'http://localhost:8378/1/files/' + b.name + }, (error, response, body) => { + expect(error).toBe(null); + var del_b = JSON.parse(body); + expect(response.statusCode).toEqual(400); + expect(del_b.code).toEqual(119); + // incorrect X-Parse-Master-Key header + request.del({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Master-Key': 'tryagain' + }, + url: 'http://localhost:8378/1/files/' + b.name + }, (error, response, body) => { + expect(error).toBe(null); + var del_b2 = JSON.parse(body); + expect(response.statusCode).toEqual(400); + expect(del_b2.code).toEqual(119); + done(); + }); + }); + }); + }); + it('handles other filetypes', done => { var headers = { 'Content-Type': 'image/jpeg', diff --git a/src/Adapters/Files/FilesAdapter.js b/src/Adapters/Files/FilesAdapter.js index 9daed5177a..a1d5955ff4 100644 --- a/src/Adapters/Files/FilesAdapter.js +++ b/src/Adapters/Files/FilesAdapter.js @@ -14,6 +14,8 @@ export class FilesAdapter { createFile(config, filename, data) { } + deleteFile(config, filename) { } + getFileData(config, filename) { } getFileLocation(config, filename) { } diff --git a/src/Adapters/Files/GridStoreAdapter.js b/src/Adapters/Files/GridStoreAdapter.js index 8c95319dea..21934c9a5a 100644 --- a/src/Adapters/Files/GridStoreAdapter.js +++ b/src/Adapters/Files/GridStoreAdapter.js @@ -20,6 +20,17 @@ export class GridStoreAdapter extends FilesAdapter { }); } + deleteFile(config, filename) { + return config.database.connect().then(() => { + let gridStore = new GridStore(config.database.db, filename, 'w'); + return gridStore.open(); + }).then((gridStore) => { + return gridStore.unlink(); + }).then((gridStore) => { + return gridStore.close(); + }); + } + getFileData(config, filename) { return config.database.connect().then(() => { return GridStore.exist(config.database.db, filename); diff --git a/src/Adapters/Files/S3Adapter.js b/src/Adapters/Files/S3Adapter.js index 2c892246b8..b33b66f1de 100644 --- a/src/Adapters/Files/S3Adapter.js +++ b/src/Adapters/Files/S3Adapter.js @@ -56,6 +56,20 @@ export class S3Adapter extends FilesAdapter { }); } + deleteFile(config, filename) { + return new Promise((resolve, reject) => { + let params = { + Key: this._bucketPrefix + filename + }; + this._s3Client.deleteObject(params, (err, data) =>{ + if(err !== null) { + return reject(err); + } + resolve(data); + }); + }); + } + // Search for and return a file if found by filename // Returns a promise that succeeds with the buffer result from S3 getFileData(config, filename) { diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 47454f076c..321042b97d 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -74,6 +74,26 @@ export class FilesController { }; } + deleteHandler() { + return (req, res, next) => { + // enforce use of master key for file deletions + if(!req.auth.isMaster){ + next(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, + 'Master key required for file deletion.')); + return; + } + + this._filesAdapter.deleteFile(req.config, req.params.filename).then(() => { + res.status(200); + // TODO: return useful JSON here? + res.end(); + }).catch((error) => { + next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR, + 'Could not delete file.')); + }); + }; + } + /** * Find file references in REST-format object and adds the url key * with the current mount point and app id. @@ -119,6 +139,12 @@ export class FilesController { this.createHandler() ); + router.delete('/files/:filename', + Middlewares.allowCrossDomain, + Middlewares.handleParseHeaders, + this.deleteHandler() + ); + return router; } }