"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.resourceNotAvailable = exports.pkgFileName = exports.noSuchFile = exports.fileExist = exports.fSError = exports.default = void 0;

var _fs = _interopRequireDefault(require("fs"));

var _path = _interopRequireDefault(require("path"));

var _debug = _interopRequireDefault(require("debug"));

var _lodash = _interopRequireDefault(require("lodash"));

var _mkdirp = _interopRequireDefault(require("mkdirp"));

var _streams = require("@verdaccio/streams");

var _fileLocking = require("@verdaccio/file-locking");

var _commonsApi = require("@verdaccio/commons-api");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

const fileExist = 'EEXISTS';
exports.fileExist = fileExist;
const noSuchFile = 'ENOENT';
exports.noSuchFile = noSuchFile;
const resourceNotAvailable = 'EAGAIN';
exports.resourceNotAvailable = resourceNotAvailable;
const pkgFileName = 'package.json';
exports.pkgFileName = pkgFileName;
const debug = (0, _debug.default)('verdaccio:plugin:local-storage:fs');

const fSError = function (message, code = 409) {
  const err = (0, _commonsApi.getCode)(code, message); // FIXME: we should return http-status codes here instead, future improvement
  // @ts-ignore

  err.code = message;
  return err;
};

exports.fSError = fSError;

const tempFile = function (str) {
  return `${str}.tmp${String(Math.random()).substr(2)}`;
};

const renameTmp = function (src, dst, _cb) {
  const cb = err => {
    if (err) {
      _fs.default.unlink(src, () => {});
    }

    _cb(err);
  };

  if (process.platform !== 'win32') {
    return _fs.default.rename(src, dst, cb);
  } // windows can't remove opened file,
  // but it seem to be able to rename it


  const tmp = tempFile(dst);

  _fs.default.rename(dst, tmp, function (err) {
    _fs.default.rename(src, dst, cb);

    if (!err) {
      _fs.default.unlink(tmp, () => {});
    }
  });
};

class LocalFS {
  constructor(path, logger) {
    _defineProperty(this, "path", void 0);

    _defineProperty(this, "logger", void 0);

    this.path = path;
    this.logger = logger;
  }
  /**
    *  This function allows to update the package thread-safely
      Algorithm:
      1. lock package.json for writing
      2. read package.json
      3. updateFn(pkg, cb), and wait for cb
      4. write package.json.tmp
      5. move package.json.tmp package.json
      6. callback(err?)
    * @param {*} name
    * @param {*} updateHandler
    * @param {*} onWrite
    * @param {*} transformPackage
    * @param {*} onEnd
    */


  updatePackage(name, updateHandler, onWrite, transformPackage, onEnd) {
    this._lockAndReadJSON(pkgFileName, (err, json) => {
      let locked = false;
      const self = this; // callback that cleans up lock first

      const unLockCallback = function (lockError) {
        // eslint-disable-next-line prefer-rest-params
        const _args = arguments;

        if (locked) {
          self._unlockJSON(pkgFileName, () => {
            // ignore any error from the unlock
            if (lockError !== null) {
              debug('lock file: %o has failed with error %o', name, lockError);
            }

            onEnd.apply(lockError, _args);
          });
        } else {
          debug('file: %o has been updated', name);
          onEnd(..._args);
        }
      };

      if (!err) {
        locked = true;
        debug('file: %o has been locked', name);
      }

      if (_lodash.default.isNil(err) === false) {
        if (err.code === resourceNotAvailable) {
          return unLockCallback((0, _commonsApi.getInternalError)('resource temporarily unavailable'));
        } else if (err.code === noSuchFile) {
          return unLockCallback((0, _commonsApi.getNotFound)());
        } else {
          return unLockCallback(err);
        }
      }

      updateHandler(json, err => {
        if (err) {
          return unLockCallback(err);
        }

        onWrite(name, transformPackage(json), unLockCallback);
      });
    });
  }

  deletePackage(packageName, callback) {
    debug('delete a package %o', packageName);
    return _fs.default.unlink(this._getStorage(packageName), callback);
  }

  removePackage(callback) {
    debug('remove a package %o', this.path);

    _fs.default.rmdir(this._getStorage('.'), callback);
  }

  createPackage(name, value, cb) {
    debug('create a package %o', name);

    this._createFile(this._getStorage(pkgFileName), this._convertToString(value), cb);
  }

  savePackage(name, value, cb) {
    debug('save a package %o', name);

    this._writeFile(this._getStorage(pkgFileName), this._convertToString(value), cb);
  }

  readPackage(name, cb) {
    debug('read a package %o', name);

    this._readStorageFile(this._getStorage(pkgFileName)).then(res => {
      try {
        const data = JSON.parse(res.toString('utf8'));
        debug('read storage file %o has succeed', name);
        cb(null, data);
      } catch (err) {
        debug('parse storage file %o has failed with error %o', name, err);
        cb(err);
      }
    }, err => {
      debug('read storage file %o has failed with error %o', name, err);
      return cb(err);
    });
  }

  writeTarball(name) {
    const uploadStream = new _streams.UploadTarball({});
    debug('write a tarball for a package %o', name);
    let _ended = 0;
    uploadStream.on('end', function () {
      _ended = 1;
    });

    const pathName = this._getStorage(name);

    _fs.default.access(pathName, fileNotFound => {
      const exists = !fileNotFound;

      if (exists) {
        uploadStream.emit('error', fSError(fileExist));
      } else {
        const temporalName = _path.default.join(this.path, `${name}.tmp-${String(Math.random()).replace(/^0\./, '')}`);

        debug('write a temporal name %o', temporalName);

        const file = _fs.default.createWriteStream(temporalName);

        const removeTempFile = () => _fs.default.unlink(temporalName, () => {});

        let opened = false;
        uploadStream.pipe(file);

        uploadStream.done = function () {
          const onend = function () {
            file.on('close', function () {
              renameTmp(temporalName, pathName, function (err) {
                if (err) {
                  uploadStream.emit('error', err);
                } else {
                  uploadStream.emit('success');
                }
              });
            });
            file.end();
          };

          if (_ended) {
            onend();
          } else {
            uploadStream.on('end', onend);
          }
        };

        uploadStream.abort = function () {
          if (opened) {
            opened = false;
            file.on('close', function () {
              removeTempFile();
            });
          } else {
            // if the file does not recieve any byte never is opened and has to be removed anyway.
            removeTempFile();
          }

          file.end();
        };

        file.on('open', function () {
          opened = true; // re-emitting open because it's handled in storage.js

          uploadStream.emit('open');
        });
        file.on('error', function (err) {
          uploadStream.emit('error', err);
        });
      }
    });

    return uploadStream;
  }

  readTarball(name) {
    const pathName = this._getStorage(name);

    debug('read a a tarball %o on path %o', name, pathName);
    const readTarballStream = new _streams.ReadTarball({});

    const readStream = _fs.default.createReadStream(pathName);

    readStream.on('error', function (err) {
      debug('error on read a tarball %o with error %o', name, err);
      readTarballStream.emit('error', err);
    });
    readStream.on('open', function (fd) {
      _fs.default.fstat(fd, function (err, stats) {
        if (_lodash.default.isNil(err) === false) {
          debug('error on read a tarball %o with error %o', name, err);
          return readTarballStream.emit('error', err);
        }

        readTarballStream.emit('content-length', stats.size);
        readTarballStream.emit('open');
        debug('open on read a tarball %o', name);
        readStream.pipe(readTarballStream);
      });
    });

    readTarballStream.abort = function () {
      debug('abort on read a tarball %o', name);
      readStream.close();
    };

    return readTarballStream;
  }

  _createFile(name, contents, callback) {
    debug(' create a new file: %o', name);

    _fs.default.open(name, 'wx', err => {
      if (err) {
        // native EEXIST used here to check exception on fs.open
        if (err.code === 'EEXIST') {
          debug('file %o cannot be created, it already exists: %o', name);
          return callback(fSError(fileExist));
        }
      }

      this._writeFile(name, contents, callback);
    });
  }

  _readStorageFile(name) {
    return new Promise((resolve, reject) => {
      debug('reading the file: %o', name);

      _fs.default.readFile(name, (err, data) => {
        if (err) {
          debug('error reading the file: %o with error %o', name, err);
          reject(err);
        } else {
          debug('read file %o succeed', name);
          resolve(data);
        }
      });
    });
  }

  _convertToString(value) {
    return JSON.stringify(value, null, '\t');
  }

  _getStorage(fileName = '') {
    const storagePath = _path.default.join(this.path, fileName);

    return storagePath;
  }

  _writeFile(dest, data, cb) {
    const createTempFile = cb => {
      const tempFilePath = tempFile(dest);

      _fs.default.writeFile(tempFilePath, data, err => {
        if (err) {
          debug('error on write the file: %o', dest);
          return cb(err);
        }

        debug('creating a new file:: %o', dest);
        renameTmp(tempFilePath, dest, cb);
      });
    };

    createTempFile(err => {
      if (err && err.code === noSuchFile) {
        (0, _mkdirp.default)(_path.default.dirname(dest)).then(() => {
          createTempFile(cb);
        }).catch(err => {
          return cb(err);
        });
      } else {
        cb(err);
      }
    });
  }

  _lockAndReadJSON(name, cb) {
    const fileName = this._getStorage(name);

    (0, _fileLocking.readFile)(fileName, {
      lock: true,
      parse: true
    }, (err, res) => {
      if (err) {
        debug('error on lock and read json for file: %o', name);
        return cb(err);
      }

      debug('lock and read json for file: %o', name);
      return cb(null, res);
    });
  }

  _unlockJSON(name, cb) {
    (0, _fileLocking.unlockFile)(this._getStorage(name), cb);
  }

}

exports.default = LocalFS;
//# sourceMappingURL=local-fs.js.map