prototypes/server/extend/response-pipe-file-static.js

const _zlib = require('zlib');
const { Stats } = require('fs');
const { pipeline } = require('stream');
const _path = require('path');
/**
 * @private
 * @method ResponsePipeFileStaticDecorator
 * @param {SGAppsServerRequest} request 
 * @param {SGAppsServerResponse} response 
 * @param {SGAppsServer} server
 * @param {function} callback
 */
function ResponsePipeFileStaticDecorator(request, response, server, callback) {
	if (request === null || response === null) {
		callback();
		return;
	}

	/**
	 * @inner
	 * @memberof SGAppsServerResponse#
	 * @callback pipeFileStaticCallback
	 * @param {Error} error 
	 */

	/**
	 * @method pipeFileStatic
	 * @memberof SGAppsServerResponse#
	 * @param {string} filePath
	 * @param {string} fileName
	 * @param {SGAppsServerResponse.pipeFileStaticCallback} callback
	 * @param {object} [options]
	 * @param {number} [options.timeout=0]
	 * @param {string} [options.filePath] originap path is autoIndex was applied
	 * @param {string[]} [options.autoIndex] list of auto-index files, ex: ['index.html', 'index.htm', 'default.html']
	 */
	response.pipeFileStatic = function ResponsePipeFileStatic(filePath, fileName, callback, options = { timeout: 0, autoIndex: [] }) {
		var returned  = false;
		var _callback = function (err) {
			if (!returned && callback) {
				callback(err);
			}
		};
		function isWritable(stream) {
			return stream && !stream.destroyed && stream.writable;
		};
		server._fs.stat(filePath, function (
			err,
			stat
		) {
			if (err) {
				if (
					options.filePath
					&& options.filePath !== filePath
					&& options.autoIndex
					&& options.autoIndex.length
					&& !options.autoIndex.includes(_path.basename(filePath))
				) {
					return response.pipeFileStatic(
						_path.resolve(options.filePath, options.autoIndex[0]),
						fileName,
						callback,
						Object.assign(
							{},
							options || {},
							{
								filePath: options.filePath || filePath,
								autoIndex: options.autoIndex.slice(1)
							}
						)
					);
				} else if (callback) {
					_callback(err);
				} else {
					response.sendError(err);
				}
			} else if (stat.isDirectory() && options.autoIndex && options.autoIndex.length) {
				return response.pipeFileStatic(
					_path.resolve(filePath, options.autoIndex[0]),
					fileName,
					callback,
					Object.assign(
						{},
						options || {},
						{
							filePath: options.filePath || filePath,
							autoIndex: options.autoIndex.slice(1)
						}
					)
				);
			} else if (!stat.isFile()) {
				const err = Error('IS_NOT_FILE File not allowed to be accessed');
				if (callback) {
					_callback(err);
				} else {
					response.sendError(err);
				}
			} else {
				var eTagTime = Date.parse(stat.mtime);
				var eTag = `${stat.size}-${eTagTime}`;
				response.response.setHeader(
					'Last-Modified',
					new Date(eTagTime).toUTCString()
				);
				if (request && request.request.headers['if-none-match'] === eTag) {
					response.response.statusCode = 304;
					response.response.end();
					_callback();
				} else {
					if (!isWritable(response.response)) {
						_callback(new Error('Client connection closed or response already destroyed.'));
						return;
					}
					var acceptEncoding = request.request.headers['accept-encoding'];
					if (Array.isArray(acceptEncoding)) acceptEncoding = acceptEncoding[0];
					var raw = function () {
						const stream = server._fs.createReadStream(filePath);
						if (options.timeout) {
							setTimeout(() => {
								stream.close();
								stream.push(null);
								stream.read(0);
							}, options.timeout);
						}
						return stream;
					};
					if (!acceptEncoding) {
						acceptEncoding = '';
					}

					response.response.setHeader(
						'Content-Type',
						server.EXTENSIONS.mime(fileName || filePath || "file")
					);
					response.response.setHeader(
						'ETag',
						eTag
					);
					response.response.statusCode = 200;
					let resError;
					if (acceptEncoding.match(/\bdeflate\b/)) {
						if (!isWritable(response.response)) {
							_callback(new Error('Client connection closed or response already destroyed.'));
							return;
						}
						response.response.setHeader(
							'content-encoding',
							'deflate'
						);
						
						response.response.on(
							'end',
							function () {
								_callback(resError);
							}
						);
						response.response.on(
							'error',
							function (err) {
								resError = err;
							}
						);
						
						pipeline(
							raw(),
							_zlib.createDeflate(),
							//@ts-ignore
							response.response,
							(err) => {
								response.sendError(err, {statusCode: 500});
							}
						);
					} else if (acceptEncoding.match(/\bgzip\b/)) {
						if (!isWritable(response.response)) {
							_callback(new Error('Client connection closed or response already destroyed.'));
							return;
						}
						response.response.setHeader(
							'content-encoding',
							'gzip'
						);
						response.response.on(
							'end',
							function () {
								_callback(resError);
							}
						);
						response.response.on(
							'error',
							function (err) {
								resError = err;
							}
						);

						pipeline(
							raw(),
							_zlib.createGzip(),
							//@ts-ignore
							response.response,
							(err) => {
								response.sendError(err, {statusCode: 500});
							}
						);
					} else {
						if (!isWritable(response.response)) {
							_callback(new Error('Client connection closed or response already destroyed.'));
							return;
						}
						response.response.setHeader(
							'Content-Length',
							stat.size
						);
						response.pipeFile(
							filePath,
							(
								callback || function (err) {
									if (err) {
										response.sendError(err);
									} else {
										response.response.end();
									}
								}
							)
						);
					}
				}
			}
		});
	};

	// response._destroy.push(function () {
	// 	delete response.pipeFileStatic;
	// });

	callback();
}

module.exports = ResponsePipeFileStaticDecorator;