'use strict';

function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }

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

var FileServer =
/*#__PURE__*/
function () {
  function FileServer(config, logger, request, response, errorCallback) {
    _classCallCheck(this, FileServer);

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

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

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

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

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

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


  _createClass(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.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();
        });
        readStream.pipe(_this.response, {
          end: false
        });
      }).then(function () {
        return _this.response.end();
      }).catch(function (err) {
        readStream.close();
        return Utils.handleError(err, _this.errorCallback, _this.logger, _this.request, _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 _iteratorNormalCompletion = true;
      var _didIteratorError = false;
      var _iteratorError = undefined;

      try {
        for (var _iterator = ranges[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
          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) {
        _didIteratorError = true;
        _iteratorError = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion && _iterator.return != null) {
            _iterator.return();
          }
        } finally {
          if (_didIteratorError) {
            throw _iteratorError;
          }
        }
      }

      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.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.statSync(filePath);
      return {
        'Content-Type': mime.lookup(path.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 _iteratorNormalCompletion2 = true;
      var _didIteratorError2 = false;
      var _iteratorError2 = undefined;

      try {
        for (var _iterator2 = this.config.defaultDocuments[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
          var file = _step2.value;

          if (fs.existsSync(path.join(dir, file))) {
            return file;
          }
        }
      } catch (err) {
        _didIteratorError2 = true;
        _iteratorError2 = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion2 && _iterator2.return != null) {
            _iterator2.return();
          }
        } finally {
          if (_didIteratorError2) {
            throw _iteratorError2;
          }
        }
      }

      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, ''); //bounce back if request url is a hidden resource

      if (!this.config.serveHiddenFiles && /^(\.|.*\/\.)/.test(url)) {
        return null;
      }

      var _iteratorNormalCompletion3 = true;
      var _didIteratorError3 = false;
      var _iteratorError3 = undefined;

      try {
        for (var _iterator3 = this.config.publicPaths[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
          var publicPath = _step3.value;
          var testPath = path.resolve(this.config.entryPath, publicPath, url);

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

              if (defaultDocument) {
                return path.join(testPath, defaultDocument);
              }
            }
          }
        }
      } catch (err) {
        _didIteratorError3 = true;
        _iteratorError3 = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion3 && _iterator3.return != null) {
            _iterator3.return();
          }
        } finally {
          if (_didIteratorError3) {
            throw _iteratorError3;
          }
        }
      }

      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 = nodeUtils.resolvePaths(this.config.entryPath, httpErrors.baseDir, httpErrors[status]);
      } else {
        filePath = nodeUtils.resolvePaths(__dirname, "../httpErrors/".concat(status, ".html"));
      }

      if (!fs.existsSync(filePath) || fs.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 = nodeUtils.resolvePaths(this.config.entryPath, filePath);

      if (!fs.existsSync(absPath) || fs.statSync(absPath).isDirectory()) {
        found = false;
        var _iteratorNormalCompletion4 = true;
        var _didIteratorError4 = false;
        var _iteratorError4 = undefined;

        try {
          for (var _iterator4 = this.config.publicPaths[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
            var current = _step4.value;
            absPath = nodeUtils.resolvePaths(this.config.entryPath, current, filePath);

            if (fs.existsSync(absPath) && fs.statSync(absPath).isFile()) {
              found = true;
              break;
            }
          }
        } catch (err) {
          _didIteratorError4 = true;
          _iteratorError4 = err;
        } finally {
          try {
            if (!_iteratorNormalCompletion4 && _iterator4.return != null) {
              _iterator4.return();
            }
          } finally {
            if (_didIteratorError4) {
              throw _iteratorError4;
            }
          }
        }
      }

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

  return FileServer;
}();

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