'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var _classCallCheck = require('@babel/runtime/helpers/classCallCheck');
var _createClass = require('@babel/runtime/helpers/createClass');
var _defineProperty = require('@babel/runtime/helpers/defineProperty');
var crypto = require('crypto');
var fs = require('fs');
var path = require('path');
var utils = require('@teclone/utils');
var mime = require('mime-types');
var Constants = require('./Constants');
var Utils = require('./Utils');

function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }

function _interopNamespace(e) {
  if (e && e.__esModule) return e;
  var n = Object.create(null);
  if (e) {
    Object.keys(e).forEach(function (k) {
      if (k !== 'default') {
        var d = Object.getOwnPropertyDescriptor(e, k);
        Object.defineProperty(n, k, d.get ? d : {
          enumerable: true,
          get: function () {
            return e[k];
          }
        });
      }
    });
  }
  n['default'] = e;
  return Object.freeze(n);
}

var _classCallCheck__default = /*#__PURE__*/_interopDefaultLegacy(_classCallCheck);
var _createClass__default = /*#__PURE__*/_interopDefaultLegacy(_createClass);
var _defineProperty__default = /*#__PURE__*/_interopDefaultLegacy(_defineProperty);
var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
var path__namespace = /*#__PURE__*/_interopNamespace(path);
var mime__default = /*#__PURE__*/_interopDefaultLegacy(mime);

function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }

function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }

function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
var FileServer = /*#__PURE__*/function () {
  function FileServer(config, request, response) {
    _classCallCheck__default['default'](this, FileServer);

    _defineProperty__default['default'](this, "config", void 0);

    _defineProperty__default['default'](this, "request", void 0);

    _defineProperty__default['default'](this, "response", void 0);

    this.config = config;
    this.request = request;
    this.response = response;
  }
  /**
   * ends the response.
   */


  _createClass__default['default'](FileServer, [{
    key: "endResponse",
    value: function endResponse(status, headers, data) {
      return this.response.status(status).setHeaders(headers).end(data);
    }
    /**
     * ends the streaming response
     */

  }, {
    key: "endStream",
    value: function endStream(filePath, status, headers, options) {
      var _this = this;

      var readStream = fs__namespace.createReadStream(filePath, options);
      this.response.status(status).setHeaders(headers);
      return new Promise(function (resolve, reject) {
        readStream.on('error', function (err) {
          return reject(err);
        });
        readStream.on('end', function () {
          return resolve(true);
        });
        readStream.pipe(_this.response, {
          end: false
        });
      }).then(function () {
        return _this.response.end();
      })["catch"](function (err) {
        readStream.close();
        return Utils.handleError(err, _this.response);
      });
    }
    /**
     * validates range request content.
     * @see https://tools.ietf.org/html/rfc7233
     */

  }, {
    key: "validateRangeRequest",
    value: function validateRangeRequest(headers, eTag, fileMTime, fileSize) {
      var result = {
        isMultiRange: false,
        statusCode: 206,
        ranges: []
      };
      var ifRange = headers['if-range']; //check if we should send everything

      if (!utils.isUndefined(ifRange) && ifRange !== eTag && ifRange !== fileMTime) {
        result.statusCode = 200;
        result.ranges.push({
          start: 0,
          end: fileSize - 1,
          length: fileSize
        });
        return result;
      } //we are not sending everything


      var ranges = headers['range'].replace(/^\s*[^=]*=\s*/, '').split(/,\s*/);
      result.isMultiRange = ranges.length > 0;

      var _iterator = _createForOfIteratorHelper(ranges),
          _step;

      try {
        for (_iterator.s(); !(_step = _iterator.n()).done;) {
          var range = _step.value;
          var start = 0;
          var end = fileSize - 1; //suffix-byte-range-spec

          if (/^-(\d+)$/.test(range)) {
            var suffixLength = Number.parseInt(RegExp.$1);

            if (suffixLength === 0) {
              continue;
            } else if (suffixLength < fileSize) {
              start = fileSize - suffixLength;
            }
          } //byte-range-spec
          else if (/^(\d+)-(\d+)?$/.test(range)) {
              var first = Number.parseInt(RegExp.$1);
              var last = RegExp.$2 ? Number.parseInt(RegExp.$2) : end;

              if (first >= fileSize || last < first) {
                continue;
              } else {
                start = first;

                if (last < end) {
                  end = last;
                }
              }
            } else {
              continue;
            }

          result.ranges.push({
            start: start,
            end: end,
            length: end - start + 1
          });
        }
      } catch (err) {
        _iterator.e(err);
      } finally {
        _iterator.f();
      }

      return result;
    }
    /**
     * check if file is not modified
     */

  }, {
    key: "fileNotModified",
    value: function fileNotModified(headers, eTag, fileMTime) {
      if (!utils.isUndefined(headers['if-none-match']) && headers['if-none-match'] === eTag) {
        return true;
      } else if (!utils.isUndefined(headers['if-modified-since']) && headers['if-modified-since'] === fileMTime) {
        return true;
      } else {
        return false;
      }
    }
    /**
     * computes and returns a files eTag
     */

  }, {
    key: "computeETag",
    value: function computeETag(fileMTime) {
      var length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 16;
      var hash = crypto__namespace.createHash('sha256');
      hash.update(fileMTime.toString());
      return hash.digest('hex').substring(0, length);
    }
    /**
     * returns default response headers
     */

  }, {
    key: "getDefaultHeaders",
    value: function getDefaultHeaders(filePath) {
      var stat = fs__namespace.statSync(filePath);
      return {
        'Content-Type': mime__default['default'].lookup(path__namespace.parse(filePath).ext.substring(1)) || 'application/octet-stream',
        'Last-Modified': stat.mtime.toString(),
        'Content-Length': stat.size.toString(),
        ETag: this.computeETag(stat.mtime),
        'Cache-Control': this.config.cacheControl
      };
    }
    /**
     * process file
     */

  }, {
    key: "process",
    value: function process(filePath) {
      var headers = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
      var method = this.request.method;

      if (method === 'options') {
        return this.endResponse(200, {
          Allow: Constants.ALLOWED_METHODS.join(',')
        });
      }

      var resHeaders = utils.copy({}, this.getDefaultHeaders(filePath), headers);

      if (method === 'head') {
        resHeaders['Accept-Ranges'] = 'bytes';
        return this.endResponse(200, resHeaders);
      }

      var eTag = resHeaders['ETag'];
      var lastModified = resHeaders['Last-Modified'];
      var fileSize = Number.parseInt(resHeaders['Content-Length']); //if it is not a range request, negotiate content

      if (utils.isUndefined(this.request.headers['range'])) {
        if (this.fileNotModified(this.request.headers, eTag, lastModified)) {
          return this.endResponse(304, {});
        } else {
          return this.endStream(filePath, 200, resHeaders);
        }
      } else {
        //we are dealing with range request
        var _this$validateRangeRe = this.validateRangeRequest(this.request.headers, eTag, lastModified, fileSize),
            statusCode = _this$validateRangeRe.statusCode,
            ranges = _this$validateRangeRe.ranges; //we are sending everything


        if (statusCode === 200) {
          return this.endStream(filePath, 200, resHeaders);
        } //range request is not satisfiable


        delete resHeaders['Content-Disposition'];

        if (ranges.length === 0) {
          return this.endResponse(416, {
            'Content-Range': "bytes */".concat(fileSize)
          });
        } //single range request


        if (ranges.length === 1) {
          var _ranges$ = ranges[0],
              start = _ranges$.start,
              end = _ranges$.end,
              length = _ranges$.length;
          resHeaders['Content-Length'] = length.toString();
          resHeaders['Content-Range'] = "bytes ".concat(start, "-").concat(end, "/").concat(fileSize);
          return this.endStream(filePath, 206, resHeaders, {
            start: start,
            end: end
          });
        } //multi range request. for now, we dont support multipart range. send everything


        resHeaders['Content-Length'] = fileSize.toString();
        resHeaders['Content-Range'] = "bytes ".concat(0, "-", fileSize - 1, "/").concat(fileSize);
        return this.endStream(filePath, 206, resHeaders);
      }
    }
    /**
     * returns the directory's default document if any
     */

  }, {
    key: "getDefaultDocument",
    value: function getDefaultDocument(dir) {
      var _iterator2 = _createForOfIteratorHelper(this.config.defaultDocuments),
          _step2;

      try {
        for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
          var file = _step2.value;

          if (fs__namespace.existsSync(path__namespace.join(dir, file))) {
            return file;
          }
        }
      } catch (err) {
        _iterator2.e(err);
      } finally {
        _iterator2.f();
      }

      return null;
    }
    /**
     * validates the request method and returns the public file or directory path that
     * matches the request url
     */

  }, {
    key: "validateRequest",
    value: function validateRequest(url, method) {
      if (!['head', 'get', 'options'].includes(method)) {
        return null;
      }

      url = url.replace(/[#?].*/, '').replace(/\.\./g, '');

      var _iterator3 = _createForOfIteratorHelper(this.config.publicPaths),
          _step3;

      try {
        for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
          var publicPath = _step3.value;
          var testPath = path__namespace.resolve(this.config.entryPath, publicPath, url);

          if (fs__namespace.existsSync(testPath)) {
            var stat = fs__namespace.statSync(testPath);

            if (stat.isFile()) {
              return testPath;
            } else if (stat.isDirectory()) {
              //check if there is a default document in the folder
              var defaultDocument = this.getDefaultDocument(testPath);

              if (defaultDocument) {
                return path__namespace.join(testPath, defaultDocument);
              }
            }
          }
        }
      } catch (err) {
        _iterator3.e(err);
      } finally {
        _iterator3.f();
      }

      return null;
    }
    /**
     * serves a static file response back to the client
     */

  }, {
    key: "serve",
    value: function serve(url) {
      var filePath = this.validateRequest(utils.stripSlashes(url), this.request.method);

      if (utils.isNull(filePath)) {
        return Promise.resolve(false);
      } else {
        return this.process(filePath);
      }
    }
    /**
     * serves server http error files. such as 504, 404, etc
     */

  }, {
    key: "serveHttpErrorFile",
    value: function serveHttpErrorFile(status) {
      var httpErrors = this.config.httpErrors;
      var filePath = '';

      if (httpErrors[status]) {
        filePath = path.resolve(this.config.entryPath, httpErrors.baseDir, httpErrors[status]);
      } else {
        filePath = path.resolve(__dirname, "../httpErrors/".concat(status, ".html"));
      }

      if (!fs__namespace.existsSync(filePath) || fs__namespace.statSync(filePath).isDirectory()) {
        return this.response.status(status).end();
      } else {
        return this.endStream(filePath, status, this.getDefaultHeaders(filePath));
      }
    }
    /**
     * serves file intended for download to the client
     */

  }, {
    key: "serveDownload",
    value: function serveDownload(filePath, filename) {
      var found = true;
      var absPath = path.resolve(this.config.entryPath, filePath);

      if (!fs__namespace.existsSync(absPath) || fs__namespace.statSync(absPath).isDirectory()) {
        found = false;

        var _iterator4 = _createForOfIteratorHelper(this.config.publicPaths),
            _step4;

        try {
          for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
            var current = _step4.value;
            absPath = path.resolve(this.config.entryPath, current, filePath);

            if (fs__namespace.existsSync(absPath) && fs__namespace.statSync(absPath).isFile()) {
              found = true;
              break;
            }
          }
        } catch (err) {
          _iterator4.e(err);
        } finally {
          _iterator4.f();
        }
      }

      if (!found) {
        return Promise.reject(new Error(filePath + ' does not exist'));
      } else {
        filename = utils.isString(filename) ? filename : path__namespace.parse(absPath).base;
        return this.process(absPath, 200, {
          'Content-Disposition': "attachment; filename=\"".concat(filename, "\"")
        });
      }
    }
  }]);

  return FileServer;
}();

exports.FileServer = FileServer;
//# sourceMappingURL=FileServer.js.map
