Skip to content

Commit b2f002d

Browse files
committed
Support Metadata in GridFSAdapter
* Useful for testing in the JS SDK * Adds new endpoint to be used with `Parse.File.getData` * Allows file adapters to return tags as well as future data.
1 parent b150699 commit b2f002d

File tree

5 files changed

+123
-36
lines changed

5 files changed

+123
-36
lines changed

spec/GridFSBucketStorageAdapter.spec.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
44
.GridFSBucketAdapter;
55
const { randomString } = require('../lib/cryptoUtils');
66
const databaseURI = 'mongodb://localhost:27017/parse';
7+
const request = require('../lib/request');
78

89
async function expectMissingFile(gfsAdapter, name) {
910
try {
@@ -33,6 +34,57 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
3334
expect(gfsResult.toString('utf8')).toBe(originalString);
3435
});
3536

37+
it('should save metadata', async () => {
38+
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
39+
const originalString = 'abcdefghi';
40+
const metadata = { hello: 'world' };
41+
await gfsAdapter.createFile('myFileName', originalString, null, {
42+
metadata,
43+
});
44+
const gfsResult = await gfsAdapter.getFileData('myFileName');
45+
expect(gfsResult.toString('utf8')).toBe(originalString);
46+
let gfsMetadata = await gfsAdapter.getMetadata('myFileName');
47+
expect(gfsMetadata.metadata).toEqual(metadata);
48+
49+
// Empty json for file not found
50+
gfsMetadata = await gfsAdapter.getMetadata('myUnknownFile');
51+
expect(gfsMetadata).toEqual({});
52+
});
53+
54+
it('should save metadata with file', async () => {
55+
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
56+
await reconfigureServer({ filesAdapter: gfsAdapter });
57+
const str = 'Hello World!';
58+
const data = [];
59+
for (let i = 0; i < str.length; i++) {
60+
data.push(str.charCodeAt(i));
61+
}
62+
const metadata = { foo: 'bar' };
63+
const file = new Parse.File('hello.txt', data, 'text/plain');
64+
file.addMetadata('foo', 'bar');
65+
await file.save();
66+
let fileData = await gfsAdapter.getMetadata(file.name());
67+
expect(fileData.metadata).toEqual(metadata);
68+
69+
// Can only add metadata on create
70+
file.addMetadata('hello', 'world');
71+
await file.save();
72+
fileData = await gfsAdapter.getMetadata(file.name());
73+
expect(fileData.metadata).toEqual(metadata);
74+
75+
const headers = {
76+
'X-Parse-Application-Id': 'test',
77+
'X-Parse-REST-API-Key': 'rest',
78+
};
79+
const response = await request({
80+
method: 'GET',
81+
headers,
82+
url: `http://localhost:8378/1/files/test/metadata/${file.name()}`,
83+
});
84+
fileData = response.data;
85+
expect(fileData.metadata).toEqual(metadata);
86+
});
87+
3688
it('properly fetches a large file from GridFS', async () => {
3789
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
3890
const twoMegabytesFile = randomString(2048 * 1024);

src/Adapters/Files/FilesAdapter.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ export class FilesAdapter {
6161
*/
6262
getFileData(filename: string): Promise<any> {}
6363

64+
/** Responsible for retrieving metadata and tags
65+
*
66+
* @param {string} filename - the filename to retrieve metadata
67+
*
68+
* @return {Promise} a promise that should pass with metadata
69+
*/
70+
getMetadata(filename: string): Promise<any> {}
71+
6472
/** Returns an absolute URL where the file can be accessed
6573
*
6674
* @param {Config} config - server configuration

src/Adapters/Files/GridFSBucketAdapter.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class GridFSBucketAdapter extends FilesAdapter {
3232
this._connectionPromise = MongoClient.connect(
3333
this._databaseURI,
3434
this._mongoOptions
35-
).then(client => {
35+
).then((client) => {
3636
this._client = client;
3737
return client.db(client.s.options.dbName);
3838
});
@@ -41,14 +41,16 @@ export class GridFSBucketAdapter extends FilesAdapter {
4141
}
4242

4343
_getBucket() {
44-
return this._connect().then(database => new GridFSBucket(database));
44+
return this._connect().then((database) => new GridFSBucket(database));
4545
}
4646

4747
// For a given config object, filename, and data, store a file
4848
// Returns a promise
49-
async createFile(filename: string, data) {
49+
async createFile(filename: string, data, contentType, options = {}) {
5050
const bucket = await this._getBucket();
51-
const stream = await bucket.openUploadStream(filename);
51+
const stream = await bucket.openUploadStream(filename, {
52+
metadata: options.metadata,
53+
});
5254
await stream.write(data);
5355
stream.end();
5456
return new Promise((resolve, reject) => {
@@ -64,7 +66,7 @@ export class GridFSBucketAdapter extends FilesAdapter {
6466
throw new Error('FileNotFound');
6567
}
6668
return Promise.all(
67-
documents.map(doc => {
69+
documents.map((doc) => {
6870
return bucket.delete(doc._id);
6971
})
7072
);
@@ -76,13 +78,13 @@ export class GridFSBucketAdapter extends FilesAdapter {
7678
stream.read();
7779
return new Promise((resolve, reject) => {
7880
const chunks = [];
79-
stream.on('data', data => {
81+
stream.on('data', (data) => {
8082
chunks.push(data);
8183
});
8284
stream.on('end', () => {
8385
resolve(Buffer.concat(chunks));
8486
});
85-
stream.on('error', err => {
87+
stream.on('error', (err) => {
8688
reject(err);
8789
});
8890
});
@@ -98,6 +100,16 @@ export class GridFSBucketAdapter extends FilesAdapter {
98100
);
99101
}
100102

103+
async getMetadata(filename) {
104+
const bucket = await this._getBucket();
105+
const files = await bucket.find({ filename }).toArray();
106+
if (files.length === 0) {
107+
return {};
108+
}
109+
const { metadata } = files[0];
110+
return { metadata };
111+
}
112+
101113
async handleFileStream(filename: string, req, res, contentType) {
102114
const bucket = await this._getBucket();
103115
const files = await bucket.find({ filename }).toArray();
@@ -122,7 +134,7 @@ export class GridFSBucketAdapter extends FilesAdapter {
122134
});
123135
const stream = bucket.openDownloadStreamByName(filename);
124136
stream.start(start);
125-
stream.on('data', chunk => {
137+
stream.on('data', (chunk) => {
126138
res.write(chunk);
127139
});
128140
stream.on('error', () => {

src/Controllers/FilesController.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,21 @@ export class FilesController extends AdaptableController {
4545
return this.adapter.deleteFile(filename);
4646
}
4747

48+
getMetadata(filename) {
49+
if (typeof this.adapter.getMetadata === 'function') {
50+
return this.adapter.getMetadata(filename);
51+
}
52+
return Promise.resolve({});
53+
}
54+
4855
/**
4956
* Find file references in REST-format object and adds the url key
5057
* with the current mount point and app id.
5158
* Object may be a single object or list of REST-format objects.
5259
*/
5360
expandFilesInObject(config, object) {
5461
if (object instanceof Array) {
55-
object.map(obj => this.expandFilesInObject(config, obj));
62+
object.map((obj) => this.expandFilesInObject(config, obj));
5663
return;
5764
}
5865
if (typeof object !== 'object') {

src/Routers/FilesRouter.js

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ const http = require('http');
1010

1111
const downloadFileFromURI = (uri) => {
1212
return new Promise((res, rej) => {
13-
http.get(uri, (response) => {
14-
response.setDefaultEncoding('base64');
15-
let body = `data:${response.headers['content-type']};base64,`;
16-
response.on('data', data => body += data);
17-
response.on('end', () => res(body));
18-
}).on('error', (e) => {
19-
rej(`Error downloading file from ${uri}: ${e.message}`);
20-
});
13+
http
14+
.get(uri, (response) => {
15+
response.setDefaultEncoding('base64');
16+
let body = `data:${response.headers['content-type']};base64,`;
17+
response.on('data', (data) => (body += data));
18+
response.on('end', () => res(body));
19+
})
20+
.on('error', (e) => {
21+
rej(`Error downloading file from ${uri}: ${e.message}`);
22+
});
2123
});
2224
};
2325

@@ -38,14 +40,15 @@ const errorMessageFromError = (e) => {
3840
return e.message;
3941
}
4042
return undefined;
41-
}
43+
};
4244

4345
export class FilesRouter {
4446
expressRouter({ maxUploadSize = '20Mb' } = {}) {
4547
var router = express.Router();
4648
router.get('/files/:appId/:filename', this.getHandler);
49+
router.get('/files/:appId/metadata/:filename', this.metadataHandler);
4750

48-
router.post('/files', function(req, res, next) {
51+
router.post('/files', function (req, res, next) {
4952
next(
5053
new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename not provided.')
5154
);
@@ -88,7 +91,7 @@ export class FilesRouter {
8891
} else {
8992
filesController
9093
.getFileData(config, filename)
91-
.then(data => {
94+
.then((data) => {
9295
res.status(200);
9396
res.set('Content-Type', contentType);
9497
res.set('Content-Length', data.length);
@@ -135,7 +138,7 @@ export class FilesRouter {
135138
fileObject,
136139
config,
137140
req.auth
138-
)
141+
);
139142
let saveResult;
140143
// if a new ParseFile is returned check if it's an already saved file
141144
if (triggerResult instanceof Parse.File) {
@@ -187,16 +190,12 @@ export class FilesRouter {
187190
res.status(201);
188191
res.set('Location', saveResult.url);
189192
res.json(saveResult);
190-
191193
} catch (e) {
192194
logger.error('Error creating a file: ', e);
193-
const errorMessage = errorMessageFromError(e) || `Could not store file: ${fileObject.file._name}.`;
194-
next(
195-
new Parse.Error(
196-
Parse.Error.FILE_SAVE_ERROR,
197-
errorMessage
198-
)
199-
);
195+
const errorMessage =
196+
errorMessageFromError(e) ||
197+
`Could not store file: ${fileObject.file._name}.`;
198+
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, errorMessage));
200199
}
201200
}
202201

@@ -207,7 +206,7 @@ export class FilesRouter {
207206
// run beforeDeleteFile trigger
208207
const file = new Parse.File(filename);
209208
file._url = filesController.adapter.getFileLocation(req.config, filename);
210-
const fileObject = { file, fileSize: null }
209+
const fileObject = { file, fileSize: null };
211210
await triggers.maybeRunFileTrigger(
212211
triggers.Types.beforeDeleteFile,
213212
fileObject,
@@ -229,12 +228,21 @@ export class FilesRouter {
229228
} catch (e) {
230229
logger.error('Error deleting a file: ', e);
231230
const errorMessage = errorMessageFromError(e) || `Could not delete file.`;
232-
next(
233-
new Parse.Error(
234-
Parse.Error.FILE_DELETE_ERROR,
235-
errorMessage
236-
)
237-
);
231+
next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR, errorMessage));
232+
}
233+
}
234+
235+
async metadataHandler(req, res) {
236+
const config = Config.get(req.params.appId);
237+
const { filesController } = config;
238+
const { filename } = req.params;
239+
try {
240+
const data = await filesController.getMetadata(filename);
241+
res.status(200);
242+
res.json(data);
243+
} catch (e) {
244+
res.status(200);
245+
res.json({});
238246
}
239247
}
240248
}

0 commit comments

Comments
 (0)