536 lines
21 KiB
JavaScript
536 lines
21 KiB
JavaScript
"use strict";
|
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
});
|
|
};
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.GridFsStorageCtr = exports.GridFsStorage = void 0;
|
|
/**
|
|
*
|
|
* Plugin definition
|
|
* @module multer-gridfs-storage/gridfs
|
|
*
|
|
*/
|
|
const crypto_1 = __importDefault(require("crypto"));
|
|
const events_1 = require("events");
|
|
const mongodb_1 = require("mongodb");
|
|
const is_promise_1 = __importDefault(require("is-promise"));
|
|
const is_generator_1 = __importDefault(require("is-generator"));
|
|
const pump_1 = __importDefault(require("pump"));
|
|
const mongodb_uri_1 = __importDefault(require("mongodb-uri"));
|
|
const utils_1 = require("./utils");
|
|
const cache_1 = require("./cache");
|
|
const isGeneratorFn = is_generator_1.default.fn;
|
|
/**
|
|
* Default file information
|
|
* @const defaults
|
|
**/
|
|
const defaults = {
|
|
metadata: null,
|
|
chunkSize: 261120,
|
|
bucketName: 'fs',
|
|
aliases: null,
|
|
};
|
|
/**
|
|
* Multer GridFS Storage Engine class definition.
|
|
* @extends EventEmitter
|
|
* @param {object} configuration
|
|
* @param {string} [configuration.url] - The url pointing to a MongoDb database
|
|
* @param {object} [configuration.options] - Options to use when connection with an url.
|
|
* @param {object} [configuration.connectionOpts] - DEPRECATED: Use options instead.
|
|
* @param {boolean | string} [configuration.cache] - Store this connection in the internal cache.
|
|
* @param {Db | Promise} [configuration.db] - The MongoDb database instance to use or a promise that resolves with it
|
|
* @param {Function} [configuration.file] - A function to control the file naming in the database
|
|
* @fires GridFsStorage#connection
|
|
* @fires GridFsStorage#connectionFailed
|
|
* @fires GridFsStorage#file
|
|
* @fires GridFsStorage#streamError
|
|
* @fires GridFsStorage#dbError
|
|
* @version 0.0.3
|
|
*/
|
|
class GridFsStorage extends events_1.EventEmitter {
|
|
constructor(configuration) {
|
|
super();
|
|
this.db = null;
|
|
this.client = null;
|
|
this.connected = false;
|
|
this.connecting = false;
|
|
this.caching = false;
|
|
this.error = null;
|
|
if (!configuration ||
|
|
(!configuration.url &&
|
|
!configuration.db)) {
|
|
throw new Error('Error creating storage engine. At least one of url or db option must be provided.');
|
|
}
|
|
this.setMaxListeners(0);
|
|
this.configuration = configuration;
|
|
this._file = this.configuration.file;
|
|
const { url, cache, options } = this.configuration;
|
|
if (url) {
|
|
this.caching = Boolean(cache);
|
|
this._options = options;
|
|
}
|
|
if (this.caching) {
|
|
const { cache, url } = configuration;
|
|
const cacheName = typeof cache === 'string' ? cache : 'default';
|
|
this.cacheName = cacheName;
|
|
this.cacheIndex = GridFsStorage.cache.initialize({
|
|
url,
|
|
cacheName,
|
|
init: this._options,
|
|
});
|
|
}
|
|
this._connect();
|
|
}
|
|
/**
|
|
* Generates 16 bytes long strings in hexadecimal format
|
|
*/
|
|
static generateBytes() {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
return new Promise((resolve, reject) => {
|
|
crypto_1.default.randomBytes(16, (error, buffer) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
resolve({ filename: buffer.toString('hex') });
|
|
});
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* Merge the properties received in the file function with default values
|
|
* @param extra Extra properties like contentType
|
|
* @param fileSettings Properties received in the file function
|
|
* @return An object with the merged properties wrapped in a promise
|
|
*/
|
|
static _mergeProps(extra, fileSettings) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
// If the filename is not provided generate one
|
|
const previous = yield (fileSettings.filename
|
|
? {}
|
|
: GridFsStorage.generateBytes());
|
|
// If no id is provided generate one
|
|
// If an error occurs the emitted file information will contain the id
|
|
const hasId = fileSettings.id;
|
|
if (!hasId) {
|
|
previous.id = new mongodb_1.ObjectId();
|
|
}
|
|
return Object.assign(Object.assign(Object.assign(Object.assign({}, previous), defaults), extra), fileSettings);
|
|
});
|
|
}
|
|
/**
|
|
* Handles generator function and promise results
|
|
* @param result - Can be a promise or a generator yielded value
|
|
* @param isGen - True if is a yielded value
|
|
**/
|
|
static _handleResult(result, isGen) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
let value = result;
|
|
if (isGen) {
|
|
if (result.done) {
|
|
throw new Error('Generator ended unexpectedly');
|
|
}
|
|
value = result.value;
|
|
}
|
|
return value;
|
|
});
|
|
}
|
|
/**
|
|
* Storage interface method to handle incoming files
|
|
* @param {Request} request - The request that trigger the upload
|
|
* @param {File} file - The uploaded file stream
|
|
* @param cb - A standard node callback to signal the end of the upload or an error
|
|
**/
|
|
_handleFile(request, file, cb) {
|
|
if (this.connecting) {
|
|
this.ready()
|
|
/* eslint-disable-next-line promise/prefer-await-to-then */
|
|
.then(() => __awaiter(this, void 0, void 0, function* () { return this.fromFile(request, file); }))
|
|
/* eslint-disable-next-line promise/prefer-await-to-then */
|
|
.then((file) => {
|
|
cb(null, file);
|
|
})
|
|
.catch(cb);
|
|
return;
|
|
}
|
|
this._updateConnectionStatus();
|
|
if (this.connected) {
|
|
this.fromFile(request, file)
|
|
/* eslint-disable-next-line promise/prefer-await-to-then */
|
|
.then((file) => {
|
|
cb(null, file);
|
|
})
|
|
.catch(cb);
|
|
return;
|
|
}
|
|
cb(new Error('The database connection must be open to store files'));
|
|
}
|
|
/**
|
|
* Storage interface method to delete files in case an error turns the request invalid
|
|
* @param request - The request that trigger the upload
|
|
* @param {File} file - The uploaded file stream
|
|
* @param cb - A standard node callback to signal the end of the upload or an error
|
|
**/
|
|
_removeFile(request, file, cb) {
|
|
const options = { bucketName: file.bucketName };
|
|
const bucket = new mongodb_1.GridFSBucket(this.db, options);
|
|
bucket.delete(file.id, cb);
|
|
}
|
|
/**
|
|
* Waits for the MongoDb connection associated to the storage to succeed or fail
|
|
*/
|
|
ready() {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (this.error) {
|
|
throw this.error;
|
|
}
|
|
if (this.connected) {
|
|
return { db: this.db, client: this.client };
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const done = (result) => {
|
|
this.removeListener('connectionFailed', fail);
|
|
resolve(result);
|
|
};
|
|
const fail = (error) => {
|
|
this.removeListener('connection', done);
|
|
reject(error);
|
|
};
|
|
this.once('connection', done);
|
|
this.once('connectionFailed', fail);
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* Pipes the file stream to the MongoDb database. The file requires a property named `file` which is a readable stream
|
|
* @param request - The http request where the file was uploaded
|
|
* @param {File} file - The file stream to pipe
|
|
* @return {Promise} Resolves with the uploaded file
|
|
*/
|
|
fromFile(request, file) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
return this.fromStream(file.stream, request, file);
|
|
});
|
|
}
|
|
/**
|
|
* Pipes the file stream to the MongoDb database. The request and file parameters are optional and used for file generation only
|
|
* @param readStream - The http request where the file was uploaded
|
|
* @param [request] - The http request where the file was uploaded
|
|
* @param {File} [file] - The file stream to pipe
|
|
* @return Resolves with the uploaded file
|
|
*/
|
|
fromStream(readStream, request, file) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
return new Promise((resolve, reject) => {
|
|
readStream.on('error', reject);
|
|
this.fromMulterStream(readStream, request, file)
|
|
/* eslint-disable-next-line promise/prefer-await-to-then */
|
|
.then(resolve)
|
|
.catch(reject);
|
|
});
|
|
});
|
|
}
|
|
_openConnection(url, options) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
let client = null;
|
|
let db;
|
|
const connection = yield mongodb_1.MongoClient.connect(url, options);
|
|
if (connection instanceof mongodb_1.MongoClient) {
|
|
client = connection;
|
|
const parsedUri = mongodb_uri_1.default.parse(url);
|
|
db = client.db(parsedUri.database);
|
|
}
|
|
else {
|
|
db = connection;
|
|
}
|
|
return { client, db };
|
|
});
|
|
}
|
|
/**
|
|
* Create a writable stream with backwards compatibility with GridStore
|
|
* @param {object} options - The stream options
|
|
*/
|
|
createStream(options) {
|
|
const settings = {
|
|
id: options.id,
|
|
chunkSizeBytes: options.chunkSize,
|
|
contentType: options.contentType,
|
|
metadata: options.metadata,
|
|
aliases: options.aliases,
|
|
disableMD5: options.disableMD5,
|
|
};
|
|
const gfs = new mongodb_1.GridFSBucket(this.db, { bucketName: options.bucketName });
|
|
return gfs.openUploadStream(options.filename, settings);
|
|
}
|
|
fromMulterStream(readStream, request, file) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (this.connecting) {
|
|
yield this.ready();
|
|
}
|
|
const fileSettings = yield this._generate(request, file);
|
|
let settings;
|
|
const setType = typeof fileSettings;
|
|
const allowedTypes = new Set(['undefined', 'number', 'string', 'object']);
|
|
if (!allowedTypes.has(setType)) {
|
|
throw new Error('Invalid type for file settings, got ' + setType);
|
|
}
|
|
if (fileSettings === null || fileSettings === undefined) {
|
|
settings = {};
|
|
}
|
|
else if (setType === 'string' || setType === 'number') {
|
|
settings = {
|
|
filename: fileSettings.toString(),
|
|
};
|
|
}
|
|
else {
|
|
settings = fileSettings;
|
|
}
|
|
const contentType = file ? file.mimetype : undefined;
|
|
const streamOptions = yield GridFsStorage._mergeProps({ contentType }, settings);
|
|
return new Promise((resolve, reject) => {
|
|
const emitError = (streamError) => {
|
|
this.emit('streamError', streamError, streamOptions);
|
|
reject(streamError);
|
|
};
|
|
const emitFile = (f) => {
|
|
const storedFile = {
|
|
id: f._id,
|
|
filename: f.filename,
|
|
metadata: f.metadata || null,
|
|
bucketName: streamOptions.bucketName,
|
|
chunkSize: f.chunkSize,
|
|
size: f.length,
|
|
md5: f.md5,
|
|
uploadDate: f.uploadDate,
|
|
contentType: f.contentType,
|
|
};
|
|
this.emit('file', storedFile);
|
|
resolve(storedFile);
|
|
};
|
|
const writeStream = this.createStream(streamOptions);
|
|
// Multer already handles the error event on the readable stream(Busboy).
|
|
// Invoking the callback with an error will cause file removal and aborting routines to be called twice
|
|
writeStream.on('error', emitError);
|
|
writeStream.on('finish', emitFile);
|
|
// @ts-ignore
|
|
pump_1.default([readStream, writeStream]);
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* Determines if a new connection should be created, a explicit connection is provided or a cached instance is required.
|
|
*/
|
|
_connect() {
|
|
const { db, client = null } = this.configuration;
|
|
if (db && !is_promise_1.default(db) && !is_promise_1.default(client)) {
|
|
this._setDb(db, client);
|
|
return;
|
|
}
|
|
this._resolveConnection()
|
|
/* eslint-disable-next-line promise/prefer-await-to-then */
|
|
.then(({ db, client }) => {
|
|
this._setDb(db, client);
|
|
})
|
|
.catch((error) => {
|
|
this._fail(error);
|
|
});
|
|
}
|
|
/**
|
|
* Returns a promise that will resolve to the db and client from the cache or a new connection depending on the provided configuration
|
|
*/
|
|
_resolveConnection() {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
this.connecting = true;
|
|
const { db, client = null } = this.configuration;
|
|
if (db) {
|
|
const [_db, _client] = yield Promise.all([db, client]);
|
|
return { db: _db, client: _client };
|
|
}
|
|
if (!this.caching) {
|
|
return this._createConnection();
|
|
}
|
|
const { cache } = GridFsStorage;
|
|
if (!cache.isOpening(this.cacheIndex) && cache.isPending(this.cacheIndex)) {
|
|
const cached = cache.get(this.cacheIndex);
|
|
cached.opening = true;
|
|
return this._createConnection();
|
|
}
|
|
return cache.waitFor(this.cacheIndex);
|
|
});
|
|
}
|
|
/**
|
|
* Handles creating a new connection from an url and storing it in the cache if necessary*}>}
|
|
*/
|
|
_createConnection() {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const { url } = this.configuration;
|
|
const options = this._options;
|
|
const { cache } = GridFsStorage;
|
|
try {
|
|
const { db, client } = yield this._openConnection(url, options);
|
|
if (this.caching) {
|
|
cache.resolve(this.cacheIndex, db, client);
|
|
}
|
|
return { db, client };
|
|
}
|
|
catch (error) {
|
|
if (this.cacheIndex) {
|
|
cache.reject(this.cacheIndex, error);
|
|
}
|
|
throw error;
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Updates the connection status based on the internal db or client object
|
|
**/
|
|
_updateConnectionStatus() {
|
|
var _a, _b;
|
|
if (!this.db) {
|
|
this.connected = false;
|
|
this.connecting = false;
|
|
return;
|
|
}
|
|
if (this.client) {
|
|
// @ts-ignore
|
|
this.connected = this.client.isConnected
|
|
? // @ts-ignore
|
|
this.client.isConnected()
|
|
: true;
|
|
return;
|
|
}
|
|
// @ts-expect-error
|
|
this.connected = ((_b = (_a = this.db) === null || _a === void 0 ? void 0 : _a.topology) === null || _b === void 0 ? void 0 : _b.isConnected()) || true;
|
|
}
|
|
/**
|
|
* Sets the database connection and emit the connection event
|
|
* @param db - Database instance or Mongoose instance to set
|
|
* @param [client] - Optional Mongo client for MongoDb v3
|
|
**/
|
|
_setDb(db, client) {
|
|
this.connecting = false;
|
|
// Check if the object is a mongoose instance, a mongoose Connection or a mongo Db object
|
|
this.db = utils_1.getDatabase(db);
|
|
if (client) {
|
|
this.client = client;
|
|
}
|
|
const errorEvent = (error_) => {
|
|
// Needs verification. Sometimes the event fires without an error object
|
|
// although the docs specify each of the events has a MongoError argument
|
|
this._updateConnectionStatus();
|
|
const error = error_ || new Error('Unknown database error');
|
|
this.emit('dbError', error);
|
|
};
|
|
// This are all the events that emit errors
|
|
const errorEventNames = ['error', 'parseError', 'timeout', 'close'];
|
|
let eventSource;
|
|
if (utils_1.shouldListenOnDb()) {
|
|
eventSource = this.db;
|
|
}
|
|
else if (this.client) {
|
|
eventSource = this.client;
|
|
}
|
|
if (eventSource) {
|
|
for (const evt of errorEventNames)
|
|
eventSource.on(evt, errorEvent);
|
|
}
|
|
this._updateConnectionStatus();
|
|
// Emit on next tick so user code can set listeners in case the db object is already available
|
|
process.nextTick(() => {
|
|
this.emit('connection', { db: this.db, client: this.client });
|
|
});
|
|
}
|
|
/**
|
|
* Removes the database reference and emit the connectionFailed event
|
|
* @param err - The error received while trying to connect
|
|
**/
|
|
_fail(error) {
|
|
this.connecting = false;
|
|
this.db = null;
|
|
this.client = null;
|
|
this.error = error;
|
|
this._updateConnectionStatus();
|
|
// Fail event is only emitted after either a then promise handler or an I/O phase so is guaranteed to be asynchronous
|
|
this.emit('connectionFailed', error);
|
|
}
|
|
/**
|
|
* Tests for generator functions or plain functions and delegates to the appropriate method
|
|
* @param request - The request that trigger the upload as received in _handleFile
|
|
* @param {File} file - The uploaded file stream as received in _handleFile
|
|
* @return A promise with the value generated by the file function
|
|
**/
|
|
_generate(request, file) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
let result;
|
|
let generator;
|
|
let isGen = false;
|
|
if (!this._file) {
|
|
return {};
|
|
}
|
|
if (isGeneratorFn(this._file)) {
|
|
isGen = true;
|
|
generator = this._file(request, file);
|
|
this._file = generator;
|
|
result = generator.next();
|
|
}
|
|
else if (is_generator_1.default(this._file)) {
|
|
isGen = true;
|
|
generator = this._file;
|
|
result = generator.next([request, file]);
|
|
}
|
|
else {
|
|
result = this._file(request, file);
|
|
}
|
|
return GridFsStorage._handleResult(result, isGen);
|
|
});
|
|
}
|
|
}
|
|
exports.GridFsStorage = GridFsStorage;
|
|
GridFsStorage.cache = new cache_1.Cache();
|
|
/**
|
|
* Event emitted when the MongoDb connection is ready to use
|
|
* @event module:multer-gridfs-storage/gridfs~GridFSStorage#connection
|
|
* @param {{db: Db, client: MongoClient}} result - An object containing the mongodb database and client
|
|
* @version 0.0.3
|
|
*/
|
|
/**
|
|
* Event emitted when the MongoDb connection fails to open
|
|
* @event module:multer-gridfs-storage/gridfs~GridFSStorage#connectionFailed
|
|
* @param {Error} err - The error received when attempting to connect
|
|
* @version 2.0.0
|
|
*/
|
|
/**
|
|
* Event emitted when a new file is uploaded
|
|
* @event module:multer-gridfs-storage/gridfs~GridFSStorage#file
|
|
* @param {File} file - The uploaded file
|
|
* @version 0.0.3
|
|
*/
|
|
/**
|
|
* Event emitted when an error occurs streaming to MongoDb
|
|
* @event module:multer-gridfs-storage/gridfs~GridFSStorage#streamError
|
|
* @param {Error} error - The error thrown by the stream
|
|
* @param {Object} conf - The failed file configuration
|
|
* @version 1.3
|
|
*/
|
|
/**
|
|
* Event emitted when the internal database connection emits an error
|
|
* @event module:multer-gridfs-storage/gridfs~GridFSStorage#dbError
|
|
* @param {Error} error - The error thrown by the database connection
|
|
* @version 1.2.2
|
|
**/
|
|
exports.GridFsStorageCtr = new Proxy(GridFsStorage, {
|
|
apply(target, thisArg, argumentsList) {
|
|
// @ts-expect-error
|
|
return new target(...argumentsList); // eslint-disable-line new-cap
|
|
},
|
|
});
|
|
//# sourceMappingURL=gridfs.js.map
|