@remotion/renderer
Version:
Render Remotion videos using Node.js or Bun
209 lines (208 loc) • 7.89 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.serveHandler = void 0;
const node_fs_1 = require("node:fs");
const node_path_1 = __importDefault(require("node:path"));
const mime_types_1 = require("../mime-types");
// Packages
const is_path_inside_1 = require("./is-path-inside");
const range_parser_1 = require("./range-parser");
const getHeaders = (absolutePath, stats) => {
const { base } = node_path_1.default.parse(absolutePath);
let defaultHeaders = {};
if (stats) {
defaultHeaders = {
'Content-Length': String(stats.size),
'Accept-Ranges': 'bytes',
};
defaultHeaders['Last-Modified'] = stats.mtime.toUTCString();
const _contentType = (0, mime_types_1.mimeContentType)(base);
if (_contentType) {
defaultHeaders['Content-Type'] = _contentType;
}
}
return defaultHeaders;
};
const getPossiblePaths = (relativePath, extension) => [
node_path_1.default.join(relativePath, `index${extension}`),
relativePath.endsWith('/')
? relativePath.replace(/\/$/g, extension)
: relativePath + extension,
].filter((item) => node_path_1.default.basename(item) !== extension);
const findRelated = async (current, relativePath) => {
const possible = getPossiblePaths(relativePath, '.html');
let stats = null;
for (let index = 0; index < possible.length; index++) {
const related = possible[index];
const absolutePath = node_path_1.default.join(current, related);
try {
stats = await node_fs_1.promises.lstat(absolutePath);
}
catch (err) {
if (err.code !== 'ENOENT' &&
err.code !== 'ENOTDIR') {
throw err;
}
}
if (stats) {
return {
stats,
absolutePath,
};
}
}
return null;
};
const sendError = (absolutePath, response, spec) => {
const { message, statusCode } = spec;
const headers = getHeaders(absolutePath, null);
response.writeHead(statusCode, {
...headers,
'Content-Type': 'application/json',
});
response.end(JSON.stringify({ statusCode, message }));
};
const internalError = (absolutePath, response) => {
return sendError(absolutePath, response, {
statusCode: 500,
code: 'internal_server_error',
message: 'A server error has occurred',
});
};
const serveHandler = async (request, response, config) => {
const cwd = process.cwd();
const current = node_path_1.default.resolve(cwd, config.public);
let relativePath = null;
try {
const parsedUrl = new URL(request.url, `http://${request.headers.host}`);
relativePath = decodeURIComponent(parsedUrl.pathname);
}
catch (_a) {
return sendError('/', response, {
statusCode: 400,
code: 'bad_request',
message: 'Bad Request',
});
}
let absolutePath = node_path_1.default.join(current, relativePath);
// Prevent path traversal vulnerabilities. We could do this
// by ourselves, but using the package covers all the edge cases.
if (!(0, is_path_inside_1.isPathInside)(absolutePath, current)) {
return sendError(absolutePath, response, {
statusCode: 400,
code: 'bad_request',
message: 'Bad Request',
});
}
let stats = null;
// It's extremely important that we're doing multiple stat calls. This one
// right here could technically be removed, but then the program
// would be slower. Because for directories, we always want to see if a related file
// exists and then (after that), fetch the directory itself if no
// related file was found. However (for files, of which most have extensions), we should
// always stat right away.
//
// When simulating a file system without directory indexes, calculating whether a
// directory exists requires loading all the file paths and then checking if
// one of them includes the path of the directory. As that's a very
// performance-expensive thing to do, we need to ensure it's not happening if not really necessary.
if (node_path_1.default.extname(relativePath) !== '') {
try {
stats = await node_fs_1.promises.lstat(absolutePath);
}
catch (err) {
if (err.code !== 'ENOENT' &&
err.code !== 'ENOTDIR') {
return internalError(absolutePath, response);
}
}
}
if (!stats) {
try {
const related = await findRelated(current, relativePath);
if (related) {
stats = related.stats;
absolutePath = related.absolutePath;
}
}
catch (err) {
if (err.code !== 'ENOENT' &&
err.code !== 'ENOTDIR') {
return internalError(absolutePath, response);
}
}
try {
stats = await node_fs_1.promises.lstat(absolutePath);
}
catch (err) {
if (err.code !== 'ENOENT' &&
err.code !== 'ENOTDIR') {
return internalError(absolutePath, response);
}
}
}
if (stats === null || stats === void 0 ? void 0 : stats.isDirectory()) {
const directory = null;
const singleFile = null;
if (directory) {
const _contentType = 'text/html; charset=utf-8';
response.statusCode = 200;
response.setHeader('Content-Type', _contentType);
response.end('Is a directory');
return;
}
if (!singleFile) {
// The directory listing is disabled, so we want to
// render a 404 error.
stats = null;
}
}
const isSymLink = stats === null || stats === void 0 ? void 0 : stats.isSymbolicLink();
// There are two scenarios in which we want to reply with
// a 404 error: Either the path does not exist, or it is a
// symlink while the `symlinks` option is disabled (which it is by default).
if (!stats || isSymLink) {
// allow for custom 404 handling
return sendError(absolutePath, response, {
statusCode: 404,
code: 'not_found',
message: 'The requested path (' + absolutePath + ') could not be found',
});
}
let streamOpts = null;
if (request.headers.range && stats.size) {
const range = (0, range_parser_1.rangeParser)(stats.size, request.headers.range);
if (typeof range === 'object' && range.type === 'bytes') {
const { start, end } = range.ranges[0];
streamOpts = {
start,
end,
};
response.statusCode = 206;
}
else {
response.statusCode = 416;
response.setHeader('Content-Range', `bytes */${stats.size}`);
}
}
let stream = null;
try {
stream = (0, node_fs_1.createReadStream)(absolutePath, streamOpts !== null && streamOpts !== void 0 ? streamOpts : {});
}
catch (_b) {
return internalError(absolutePath, response);
}
const headers = getHeaders(absolutePath, stats);
if (streamOpts !== null) {
headers['Content-Range'] =
`bytes ${streamOpts.start}-${streamOpts.end}/${stats.size}`;
headers['Content-Length'] = String(streamOpts.end - streamOpts.start + 1);
}
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
response.writeHead(response.statusCode || 200, headers);
stream.pipe(response);
};
exports.serveHandler = serveHandler;