text: "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