Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Metadata in GridFSAdapter #6660

Merged
merged 5 commits into from
May 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 22 additions & 14 deletions spec/FilesController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter')
.GridStoreAdapter;
const Config = require('../lib/Config');
const FilesController = require('../lib/Controllers/FilesController').default;
const databaseURI = 'mongodb://localhost:27017/parse';

const mockAdapter = {
createFile: () => {
Expand All @@ -23,13 +24,13 @@ const mockAdapter = {

// Small additional tests to improve overall coverage
describe('FilesController', () => {
it('should properly expand objects', done => {
it('should properly expand objects', (done) => {
const config = Config.get(Parse.applicationId);
const gridStoreAdapter = new GridFSBucketAdapter(
'mongodb://localhost:27017/parse'
);
const filesController = new FilesController(gridStoreAdapter);
const result = filesController.expandFilesInObject(config, function() {});
const result = filesController.expandFilesInObject(config, function () {});

expect(result).toBeUndefined();

Expand All @@ -47,7 +48,7 @@ describe('FilesController', () => {
done();
});

it('should create a server log on failure', done => {
it('should create a server log on failure', (done) => {
const logController = new LoggerController(new WinstonLoggerAdapter());

reconfigureServer({ filesAdapter: mockAdapter })
Expand All @@ -56,30 +57,28 @@ describe('FilesController', () => {
() => done.fail('should not succeed'),
() => setImmediate(() => Promise.resolve('done'))
)
.then(() => new Promise(resolve => setTimeout(resolve, 200)))
.then(() => new Promise((resolve) => setTimeout(resolve, 200)))
.then(() =>
logController.getLogs({ from: Date.now() - 1000, size: 1000 })
)
.then(logs => {
.then((logs) => {
// we get two logs here: 1. the source of the failure to save the file
// and 2 the message that will be sent back to the client.

const log1 = logs.find(
x => x.message === 'Error creating a file: it failed with xyz'
(x) => x.message === 'Error creating a file: it failed with xyz'
);
expect(log1.level).toBe('error');

const log2 = logs.find(
x => x.message === 'it failed with xyz'
);
const log2 = logs.find((x) => x.message === 'it failed with xyz');
expect(log2.level).toBe('error');
expect(log2.code).toBe(130);

done();
});
});

it('should create a parse error when a string is returned', done => {
it('should create a parse error when a string is returned', (done) => {
const mock2 = mockAdapter;
mock2.validateFilename = () => {
return 'Bad file! No biscuit!';
Expand All @@ -92,7 +91,7 @@ describe('FilesController', () => {
done();
});

it('should add a unique hash to the file name when the preserveFileName option is false', done => {
it('should add a unique hash to the file name when the preserveFileName option is false', (done) => {
const config = Config.get(Parse.applicationId);
const gridStoreAdapter = new GridFSBucketAdapter(
'mongodb://localhost:27017/parse'
Expand All @@ -115,7 +114,7 @@ describe('FilesController', () => {
done();
});

it('should not add a unique hash to the file name when the preserveFileName option is true', done => {
it('should not add a unique hash to the file name when the preserveFileName option is true', (done) => {
const config = Config.get(Parse.applicationId);
const gridStoreAdapter = new GridFSBucketAdapter(
'mongodb://localhost:27017/parse'
Expand All @@ -137,7 +136,16 @@ describe('FilesController', () => {
done();
});

it('should reject slashes in file names', done => {
it('should handle adapter without getMetadata', async () => {
const gridStoreAdapter = new GridFSBucketAdapter(databaseURI);
gridStoreAdapter.getMetadata = null;
const filesController = new FilesController(gridStoreAdapter);

const result = await filesController.getMetadata();
expect(result).toEqual({});
});

it('should reject slashes in file names', (done) => {
const gridStoreAdapter = new GridFSBucketAdapter(
'mongodb://localhost:27017/parse'
);
Expand All @@ -146,7 +154,7 @@ describe('FilesController', () => {
done();
});

it('should also reject slashes in file names', done => {
it('should also reject slashes in file names', (done) => {
const gridStoreAdapter = new GridStoreAdapter(
'mongodb://localhost:27017/parse'
);
Expand Down
69 changes: 69 additions & 0 deletions spec/GridFSBucketStorageAdapter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
.GridFSBucketAdapter;
const { randomString } = require('../lib/cryptoUtils');
const databaseURI = 'mongodb://localhost:27017/parse';
const request = require('../lib/request');
const Config = require('../lib/Config');

async function expectMissingFile(gfsAdapter, name) {
try {
Expand Down Expand Up @@ -33,6 +35,73 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
expect(gfsResult.toString('utf8')).toBe(originalString);
});

it('should save metadata', async () => {
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
const originalString = 'abcdefghi';
const metadata = { hello: 'world' };
await gfsAdapter.createFile('myFileName', originalString, null, {
metadata,
});
const gfsResult = await gfsAdapter.getFileData('myFileName');
expect(gfsResult.toString('utf8')).toBe(originalString);
let gfsMetadata = await gfsAdapter.getMetadata('myFileName');
expect(gfsMetadata.metadata).toEqual(metadata);

// Empty json for file not found
gfsMetadata = await gfsAdapter.getMetadata('myUnknownFile');
dplewis marked this conversation as resolved.
Show resolved Hide resolved
expect(gfsMetadata).toEqual({});
});

it('should save metadata with file', async () => {
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
await reconfigureServer({ filesAdapter: gfsAdapter });
const str = 'Hello World!';
const data = [];
for (let i = 0; i < str.length; i++) {
data.push(str.charCodeAt(i));
}
const metadata = { foo: 'bar' };
const file = new Parse.File('hello.txt', data, 'text/plain');
file.addMetadata('foo', 'bar');
await file.save();
let fileData = await gfsAdapter.getMetadata(file.name());
expect(fileData.metadata).toEqual(metadata);

// Can only add metadata on create
file.addMetadata('hello', 'world');
await file.save();
fileData = await gfsAdapter.getMetadata(file.name());
expect(fileData.metadata).toEqual(metadata);

const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'GET',
headers,
url: `http://localhost:8378/1/files/test/metadata/${file.name()}`,
});
fileData = response.data;
expect(fileData.metadata).toEqual(metadata);
});

it('should handle getMetadata error', async () => {
const config = Config.get('test');
config.filesController.getMetadata = () => Promise.reject();

const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'GET',
headers,
url: `http://localhost:8378/1/files/test/metadata/filename.txt`,
});
expect(response.data).toEqual({});
});

it('properly fetches a large file from GridFS', async () => {
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
const twoMegabytesFile = randomString(2048 * 1024);
Expand Down
8 changes: 8 additions & 0 deletions src/Adapters/Files/FilesAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ export class FilesAdapter {
* @returns {Promise} Data for byte range
*/
// handleFileStream(filename: string, res: any, req: any, contentType: string): Promise

/** Responsible for retrieving metadata and tags
*
* @param {string} filename - the filename to retrieve metadata
*
* @return {Promise} a promise that should pass with metadata
*/
// getMetadata(filename: string): Promise<any> {}
}

/**
Expand Down
28 changes: 20 additions & 8 deletions src/Adapters/Files/GridFSBucketAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class GridFSBucketAdapter extends FilesAdapter {
this._connectionPromise = MongoClient.connect(
this._databaseURI,
this._mongoOptions
).then(client => {
).then((client) => {
this._client = client;
return client.db(client.s.options.dbName);
});
Expand All @@ -41,14 +41,16 @@ export class GridFSBucketAdapter extends FilesAdapter {
}

_getBucket() {
return this._connect().then(database => new GridFSBucket(database));
return this._connect().then((database) => new GridFSBucket(database));
}

// For a given config object, filename, and data, store a file
// Returns a promise
async createFile(filename: string, data) {
async createFile(filename: string, data, contentType, options = {}) {
const bucket = await this._getBucket();
const stream = await bucket.openUploadStream(filename);
const stream = await bucket.openUploadStream(filename, {
metadata: options.metadata,
});
await stream.write(data);
stream.end();
return new Promise((resolve, reject) => {
Expand All @@ -64,7 +66,7 @@ export class GridFSBucketAdapter extends FilesAdapter {
throw new Error('FileNotFound');
}
return Promise.all(
documents.map(doc => {
documents.map((doc) => {
return bucket.delete(doc._id);
})
);
Expand All @@ -76,13 +78,13 @@ export class GridFSBucketAdapter extends FilesAdapter {
stream.read();
return new Promise((resolve, reject) => {
const chunks = [];
stream.on('data', data => {
stream.on('data', (data) => {
chunks.push(data);
});
stream.on('end', () => {
resolve(Buffer.concat(chunks));
});
stream.on('error', err => {
stream.on('error', (err) => {
reject(err);
});
});
Expand All @@ -98,6 +100,16 @@ export class GridFSBucketAdapter extends FilesAdapter {
);
}

async getMetadata(filename) {
const bucket = await this._getBucket();
const files = await bucket.find({ filename }).toArray();
if (files.length === 0) {
return {};
}
const { metadata } = files[0];
return { metadata };
}

async handleFileStream(filename: string, req, res, contentType) {
const bucket = await this._getBucket();
const files = await bucket.find({ filename }).toArray();
Expand All @@ -122,7 +134,7 @@ export class GridFSBucketAdapter extends FilesAdapter {
});
const stream = bucket.openDownloadStreamByName(filename);
stream.start(start);
stream.on('data', chunk => {
stream.on('data', (chunk) => {
res.write(chunk);
});
stream.on('error', () => {
Expand Down
9 changes: 8 additions & 1 deletion src/Controllers/FilesController.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,21 @@ export class FilesController extends AdaptableController {
return this.adapter.deleteFile(filename);
}

getMetadata(filename) {
if (typeof this.adapter.getMetadata === 'function') {
return this.adapter.getMetadata(filename);
}
return Promise.resolve({});
}

/**
* Find file references in REST-format object and adds the url key
* with the current mount point and app id.
* Object may be a single object or list of REST-format objects.
*/
expandFilesInObject(config, object) {
if (object instanceof Array) {
object.map(obj => this.expandFilesInObject(config, obj));
object.map((obj) => this.expandFilesInObject(config, obj));
return;
}
if (typeof object !== 'object') {
Expand Down
Loading