2018-09-04 16:15:09 -04:00
|
|
|
/**
|
|
|
|
|
GridFSBucketAdapter
|
|
|
|
|
Stores files in Mongo using GridStore
|
|
|
|
|
Requires the database adapter to be based on mongoclient
|
|
|
|
|
|
|
|
|
|
@flow weak
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// @flow-disable-next
|
|
|
|
|
import { MongoClient, GridFSBucket, Db } from 'mongodb';
|
2019-10-26 19:15:21 -07:00
|
|
|
import { FilesAdapter, validateFilename } from './FilesAdapter';
|
2018-09-04 16:15:09 -04:00
|
|
|
import defaults from '../../defaults';
|
2020-07-01 19:43:26 -04:00
|
|
|
const crypto = require('crypto');
|
2018-09-04 16:15:09 -04:00
|
|
|
|
|
|
|
|
export class GridFSBucketAdapter extends FilesAdapter {
|
|
|
|
|
_databaseURI: string;
|
|
|
|
|
_connectionPromise: Promise<Db>;
|
2019-05-01 00:44:10 -05:00
|
|
|
_mongoOptions: Object;
|
2020-07-01 19:43:26 -04:00
|
|
|
_algorithm: string;
|
2018-09-04 16:15:09 -04:00
|
|
|
|
2020-07-01 19:43:26 -04:00
|
|
|
constructor(
|
|
|
|
|
mongoDatabaseURI = defaults.DefaultMongoURI,
|
|
|
|
|
mongoOptions = {},
|
|
|
|
|
fileKey = undefined
|
|
|
|
|
) {
|
2018-09-04 16:15:09 -04:00
|
|
|
super();
|
|
|
|
|
this._databaseURI = mongoDatabaseURI;
|
2020-07-01 19:43:26 -04:00
|
|
|
this._algorithm = 'aes-256-gcm';
|
|
|
|
|
this._fileKey =
|
|
|
|
|
fileKey !== undefined
|
|
|
|
|
? crypto
|
|
|
|
|
.createHash('sha256')
|
|
|
|
|
.update(String(fileKey))
|
|
|
|
|
.digest('base64')
|
|
|
|
|
.substr(0, 32)
|
|
|
|
|
: null;
|
2019-08-18 22:56:26 -07:00
|
|
|
const defaultMongoOptions = {
|
|
|
|
|
useNewUrlParser: true,
|
|
|
|
|
useUnifiedTopology: true,
|
|
|
|
|
};
|
2019-05-01 00:44:10 -05:00
|
|
|
this._mongoOptions = Object.assign(defaultMongoOptions, mongoOptions);
|
2018-09-04 16:15:09 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_connect() {
|
|
|
|
|
if (!this._connectionPromise) {
|
2019-05-01 00:44:10 -05:00
|
|
|
this._connectionPromise = MongoClient.connect(
|
|
|
|
|
this._databaseURI,
|
|
|
|
|
this._mongoOptions
|
2020-10-02 00:19:26 +02:00
|
|
|
).then(client => {
|
2019-08-19 00:35:06 -07:00
|
|
|
this._client = client;
|
|
|
|
|
return client.db(client.s.options.dbName);
|
|
|
|
|
});
|
2018-09-04 16:15:09 -04:00
|
|
|
}
|
|
|
|
|
return this._connectionPromise;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_getBucket() {
|
2020-10-02 00:19:26 +02:00
|
|
|
return this._connect().then(database => new GridFSBucket(database));
|
2018-09-04 16:15:09 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For a given config object, filename, and data, store a file
|
|
|
|
|
// Returns a promise
|
2020-05-08 15:32:20 -05:00
|
|
|
async createFile(filename: string, data, contentType, options = {}) {
|
2018-09-04 16:15:09 -04:00
|
|
|
const bucket = await this._getBucket();
|
2020-05-08 15:32:20 -05:00
|
|
|
const stream = await bucket.openUploadStream(filename, {
|
|
|
|
|
metadata: options.metadata,
|
|
|
|
|
});
|
2020-07-01 19:43:26 -04:00
|
|
|
if (this._fileKey !== null) {
|
|
|
|
|
const iv = crypto.randomBytes(16);
|
|
|
|
|
const cipher = crypto.createCipheriv(this._algorithm, this._fileKey, iv);
|
|
|
|
|
const encryptedResult = Buffer.concat([
|
|
|
|
|
cipher.update(data),
|
|
|
|
|
cipher.final(),
|
|
|
|
|
iv,
|
|
|
|
|
cipher.getAuthTag(),
|
|
|
|
|
]);
|
|
|
|
|
await stream.write(encryptedResult);
|
|
|
|
|
} else {
|
|
|
|
|
await stream.write(data);
|
|
|
|
|
}
|
2018-09-04 16:15:09 -04:00
|
|
|
stream.end();
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
stream.on('finish', resolve);
|
|
|
|
|
stream.on('error', reject);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteFile(filename: string) {
|
|
|
|
|
const bucket = await this._getBucket();
|
2019-09-11 09:34:39 -05:00
|
|
|
const documents = await bucket.find({ filename }).toArray();
|
2018-09-04 16:15:09 -04:00
|
|
|
if (documents.length === 0) {
|
|
|
|
|
throw new Error('FileNotFound');
|
|
|
|
|
}
|
|
|
|
|
return Promise.all(
|
2020-10-02 00:19:26 +02:00
|
|
|
documents.map(doc => {
|
2018-09-04 16:15:09 -04:00
|
|
|
return bucket.delete(doc._id);
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getFileData(filename: string) {
|
2019-09-11 09:34:39 -05:00
|
|
|
const bucket = await this._getBucket();
|
|
|
|
|
const stream = bucket.openDownloadStreamByName(filename);
|
2018-09-04 16:15:09 -04:00
|
|
|
stream.read();
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const chunks = [];
|
2020-10-02 00:19:26 +02:00
|
|
|
stream.on('data', data => {
|
2018-09-04 16:15:09 -04:00
|
|
|
chunks.push(data);
|
|
|
|
|
});
|
|
|
|
|
stream.on('end', () => {
|
2020-07-01 19:43:26 -04:00
|
|
|
const data = Buffer.concat(chunks);
|
|
|
|
|
if (this._fileKey !== null) {
|
|
|
|
|
const authTagLocation = data.length - 16;
|
|
|
|
|
const ivLocation = data.length - 32;
|
|
|
|
|
const authTag = data.slice(authTagLocation);
|
|
|
|
|
const iv = data.slice(ivLocation, authTagLocation);
|
|
|
|
|
const encrypted = data.slice(0, ivLocation);
|
|
|
|
|
const decipher = crypto.createDecipheriv(
|
|
|
|
|
this._algorithm,
|
|
|
|
|
this._fileKey,
|
|
|
|
|
iv
|
|
|
|
|
);
|
|
|
|
|
decipher.setAuthTag(authTag);
|
|
|
|
|
return resolve(
|
|
|
|
|
Buffer.concat([decipher.update(encrypted), decipher.final()])
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
resolve(data);
|
2018-09-04 16:15:09 -04:00
|
|
|
});
|
2020-10-02 00:19:26 +02:00
|
|
|
stream.on('error', err => {
|
2018-09-04 16:15:09 -04:00
|
|
|
reject(err);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getFileLocation(config, filename) {
|
|
|
|
|
return (
|
|
|
|
|
config.mount +
|
|
|
|
|
'/files/' +
|
|
|
|
|
config.applicationId +
|
|
|
|
|
'/' +
|
|
|
|
|
encodeURIComponent(filename)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-08 15:32:20 -05:00
|
|
|
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 };
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-11 09:34:39 -05:00
|
|
|
async handleFileStream(filename: string, req, res, contentType) {
|
2018-09-04 16:15:09 -04:00
|
|
|
const bucket = await this._getBucket();
|
2019-09-11 09:34:39 -05:00
|
|
|
const files = await bucket.find({ filename }).toArray();
|
|
|
|
|
if (files.length === 0) {
|
|
|
|
|
throw new Error('FileNotFound');
|
|
|
|
|
}
|
|
|
|
|
const parts = req
|
|
|
|
|
.get('Range')
|
|
|
|
|
.replace(/bytes=/, '')
|
|
|
|
|
.split('-');
|
|
|
|
|
const partialstart = parts[0];
|
|
|
|
|
const partialend = parts[1];
|
|
|
|
|
|
|
|
|
|
const start = parseInt(partialstart, 10);
|
|
|
|
|
const end = partialend ? parseInt(partialend, 10) : files[0].length - 1;
|
|
|
|
|
|
|
|
|
|
res.writeHead(206, {
|
|
|
|
|
'Accept-Ranges': 'bytes',
|
|
|
|
|
'Content-Length': end - start + 1,
|
|
|
|
|
'Content-Range': 'bytes ' + start + '-' + end + '/' + files[0].length,
|
|
|
|
|
'Content-Type': contentType,
|
|
|
|
|
});
|
|
|
|
|
const stream = bucket.openDownloadStreamByName(filename);
|
|
|
|
|
stream.start(start);
|
2020-10-02 00:19:26 +02:00
|
|
|
stream.on('data', chunk => {
|
2019-09-11 09:34:39 -05:00
|
|
|
res.write(chunk);
|
|
|
|
|
});
|
|
|
|
|
stream.on('error', () => {
|
|
|
|
|
res.sendStatus(404);
|
|
|
|
|
});
|
|
|
|
|
stream.on('end', () => {
|
|
|
|
|
res.end();
|
|
|
|
|
});
|
2018-09-04 16:15:09 -04:00
|
|
|
}
|
2019-08-19 00:35:06 -07:00
|
|
|
|
|
|
|
|
handleShutdown() {
|
|
|
|
|
if (!this._client) {
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
}
|
|
|
|
|
return this._client.close(false);
|
|
|
|
|
}
|
2019-10-26 19:15:21 -07:00
|
|
|
|
|
|
|
|
validateFilename(filename) {
|
|
|
|
|
return validateFilename(filename);
|
|
|
|
|
}
|
2018-09-04 16:15:09 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default GridFSBucketAdapter;
|