hfs
Version:
HTTP File Server
187 lines (186 loc) • 10.9 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.serveGuiAndSharedFiles = void 0;
const path_1 = require("path");
const vfs_1 = require("./vfs");
const errorPages_1 = require("./errorPages");
const events_1 = __importDefault(require("./events"));
const cross_const_1 = require("./cross-const");
const upload_1 = require("./upload");
const formidable_1 = __importDefault(require("formidable"));
const stream_1 = require("stream");
const serveFile_1 = require("./serveFile");
const const_1 = require("./const");
const zip_1 = require("./zip");
const adminApis_1 = require("./adminApis");
const serveGuiFiles_1 = require("./serveGuiFiles");
const koa_mount_1 = __importDefault(require("koa-mount"));
const listen_1 = require("./listen");
const misc_1 = require("./misc");
const promises_1 = require("fs/promises");
const comments_1 = require("./comments");
const basicWeb_1 = require("./basicWeb");
const icons_1 = require("./icons");
const plugins_1 = require("./plugins");
const serveFrontendFiles = (0, serveGuiFiles_1.serveGuiFiles)(process.env.FRONTEND_PROXY, cross_const_1.FRONTEND_URI);
const serveFrontendPrefixed = (0, koa_mount_1.default)(cross_const_1.FRONTEND_URI.slice(0, -1), serveFrontendFiles);
const serveAdminFiles = (0, serveGuiFiles_1.serveGuiFiles)(process.env.ADMIN_PROXY, cross_const_1.ADMIN_URI);
const serveAdminPrefixed = (0, koa_mount_1.default)(cross_const_1.ADMIN_URI.slice(0, -1), serveAdminFiles);
const serveGuiAndSharedFiles = async (ctx, next) => {
var _a, _b;
const { path } = ctx;
// dynamic import on frontend|admin (used for non-https login) while developing (vite4) is not producing a relative path
if (const_1.DEV && path.startsWith('/node_modules/')) {
const { referer: r } = ctx.headers;
return (0, misc_1.try_)(() => { var _a; return r && ((_a = new URL(r).pathname) === null || _a === void 0 ? void 0 : _a.startsWith(cross_const_1.ADMIN_URI)); }) ? serveAdminFiles(ctx, next)
: serveFrontendFiles(ctx, next);
}
if (path.startsWith(cross_const_1.FRONTEND_URI))
return serveFrontendPrefixed(ctx, next);
if (path.length === cross_const_1.ADMIN_URI.length - 1 && cross_const_1.ADMIN_URI.startsWith(path))
return ctx.redirect(ctx.state.revProxyPath + cross_const_1.ADMIN_URI);
if (path.startsWith(cross_const_1.ADMIN_URI))
return (0, adminApis_1.allowAdmin)(ctx) ? serveAdminPrefixed(ctx, next)
: (0, errorPages_1.sendErrorPage)(ctx, cross_const_1.HTTP_FORBIDDEN);
if (path.startsWith(cross_const_1.ICONS_URI)) {
const a = path.substring(cross_const_1.ICONS_URI.length).split('/');
const iconName = a.at(-1);
if (!iconName)
return;
const plugin = a.length > 1 && (0, plugins_1.getPluginInfo)(a[0]); // an extra level in the path indicates a plugin
const file = plugin ? (_a = plugin.icons) === null || _a === void 0 ? void 0 : _a[iconName] : icons_1.customizedIcons === null || icons_1.customizedIcons === void 0 ? void 0 : icons_1.customizedIcons[iconName];
if (!file)
return;
ctx.state.considerAsGui = true;
return (0, serveFile_1.serveFile)(ctx, (0, path_1.join)((plugin === null || plugin === void 0 ? void 0 : plugin.folder) || '', icons_1.ICONS_FOLDER, file), const_1.MIME_AUTO);
}
if (ctx.method === 'PUT') { // curl -T file url/
const decPath = decodeURIComponent(path);
let rest = (0, path_1.basename)(decPath);
const folderUri = (0, misc_1.pathEncode)((0, path_1.dirname)(decPath)); // re-encode to get readable urls
const folder = await (0, vfs_1.urlToNode)(folderUri, ctx, vfs_1.vfs, v => rest = v + '/' + rest);
if (!folder)
return (0, errorPages_1.sendErrorPage)(ctx, cross_const_1.HTTP_NOT_FOUND);
ctx.state.uploadPath = decPath;
const dest = (0, upload_1.uploadWriter)(folder, folderUri, rest, ctx);
if (dest) {
ctx.req.pipe(dest).on('error', err => {
ctx.status = cross_const_1.HTTP_SERVER_ERROR;
ctx.body = err.message || String(err);
});
ctx.req.on('close', () => dest.end());
const uri = await dest.lockMiddleware; // we need to wait more than just the stream
if (uri) // falsy = aborted
ctx.body = { uri };
else if (ctx.status === 404) // nodejs already sent 400, but koa ignores it (ctx.headersSent is false and ctx.status is 404), so we adjust koa state to have correct data in the log
ctx.status = 400;
}
return;
}
if (/^\/favicon.ico(\??.*)/.test(ctx.originalUrl) && adminApis_1.favicon.get() && ctx.method === 'GET') // originalUrl to not be subject to changes (vhosting plugin)
return (0, serveFile_1.serveFile)(ctx, adminApis_1.favicon.get());
let node = await (0, vfs_1.urlToNode)(path, ctx);
if (!node)
return (0, errorPages_1.sendErrorPage)(ctx, cross_const_1.HTTP_NOT_FOUND);
if (ctx.method === 'POST') { // curl -F upload=@file url/
if (ctx.request.type !== 'multipart/form-data')
return ctx.status = cross_const_1.HTTP_BAD_REQUEST;
ctx.state.uploads = [];
let locks = [];
const form = (0, formidable_1.default)({
maxFileSize: Infinity,
allowEmptyFiles: true,
fileWriteStreamHandler: f => {
const fn = f.originalFilename;
ctx.state.uploadPath = decodeURI(ctx.path) + fn;
ctx.state.uploads.push(fn);
const ret = (0, upload_1.uploadWriter)(node, path, fn, ctx);
if (!ret)
return new stream_1.Writable({ write(data, enc, cb) { cb(); } }); // just discard data
locks.push(ret.lockMiddleware);
return ret;
}
});
const uris = await new Promise(res => form.parse(ctx.req, async (err) => {
if (err)
console.error(String(err));
res(Promise.all(locks));
}));
ctx.body = { uris };
return;
}
if (ctx.method === 'DELETE') {
const { source } = node;
if (!source)
return ctx.status = cross_const_1.HTTP_METHOD_NOT_ALLOWED;
if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_delete', ctx))
return;
try {
if ((_b = (await events_1.default.emitAsync('deleting', { node, ctx }))) === null || _b === void 0 ? void 0 : _b.isDefaultPrevented())
return ctx.status = cross_const_1.HTTP_FAILED_DEPENDENCY;
await (0, promises_1.rm)(source, { recursive: true });
void (0, comments_1.setCommentFor)(source, ''); // necessary only to clean a possible descript.ion or kvstorage
return ctx.status = cross_const_1.HTTP_OK;
}
catch (e) {
ctx.body = String(e);
return ctx.status = cross_const_1.HTTP_SERVER_ERROR;
}
}
const { get } = ctx.query;
if (node.default && path.endsWith('/') && !get) { // final/ needed on browser to make resource urls correctly with html pages
const found = await (0, vfs_1.urlToNode)(node.default, ctx, node);
if (found && /\.html?/i.test(node.default))
ctx.state.considerAsGui = true;
node = found !== null && found !== void 0 ? found : node;
}
if (get === 'icon')
return (0, serveFile_1.serveFile)(ctx, node.icon || '|'); // pipe to cause not-found
if (!await (0, vfs_1.nodeIsDirectory)(node))
return node.url ? ctx.redirect(node.url)
: !node.source ? (0, errorPages_1.sendErrorPage)(ctx, cross_const_1.HTTP_METHOD_NOT_ALLOWED) // !dir && !source is not supported at this moment
: !(0, vfs_1.statusCodeForMissingPerm)(node, 'can_read', ctx) ? (0, serveFile_1.serveFileNode)(ctx, node) // all good
: ctx.status !== cross_const_1.HTTP_UNAUTHORIZED ? null // all errors don't need extra handling, except unauthorized
: (0, basicWeb_1.detectBasicAgent)(ctx) ? (ctx.set('WWW-Authenticate', 'Basic'), (0, errorPages_1.sendErrorPage)(ctx))
: ctx.query.dl === undefined && (ctx.state.serveApp = true) && serveFrontendFiles(ctx, next);
if (!path.endsWith('/'))
return ctx.redirect(ctx.state.revProxyPath + ctx.originalUrl.replace(/(\?|$)/, '/$1')); // keep query-string, if any
if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_list', ctx)) {
if (ctx.status === cross_const_1.HTTP_FORBIDDEN)
return (0, errorPages_1.sendErrorPage)(ctx, cross_const_1.HTTP_FORBIDDEN);
// detect if we are dealing with a download-manager, as it may need basic authentication, while we don't want it on browsers
const { authenticate } = ctx.query;
const downloadManagerDetected = /DAP|FDM|[Mm]anager/.test(ctx.get('user-agent'));
if (downloadManagerDetected || authenticate || (0, basicWeb_1.detectBasicAgent)(ctx))
return ctx.set('WWW-Authenticate', authenticate || 'Basic'); // basic authentication for DMs getting the folder as a zip
ctx.state.serveApp = true;
return serveFrontendFiles(ctx, next);
}
ctx.set({ server: `HFS ${const_1.VERSION} ${const_1.BUILD_TIMESTAMP}` });
return get === 'zip' ? (0, zip_1.zipStreamFromFolder)(node, ctx)
: get === 'list' ? sendFolderList(node, ctx)
: ((0, basicWeb_1.basicWeb)(ctx, node) || serveFrontendFiles(ctx, next));
};
exports.serveGuiAndSharedFiles = serveGuiAndSharedFiles;
async function sendFolderList(node, ctx) {
var _a;
if ((_a = (await events_1.default.emitAsync('getList', { node, ctx }))) === null || _a === void 0 ? void 0 : _a.isDefaultPrevented())
return;
let { depth = 0, folders, prepend } = ctx.query;
ctx.type = 'text';
if (prepend === undefined || prepend === '*') { // * = force auto-detection even if we have baseUrl set
const { URL } = ctx;
const base = prepend === undefined && listen_1.baseUrl.get()
|| URL.protocol + '//' + URL.host + ctx.state.revProxyPath;
prepend = base + (0, misc_1.pathEncode)(decodeURI(ctx.path)); // redo the encoding our way, keeping unicode chars unchanged
}
const walker = (0, vfs_1.walkNode)(node, { ctx, depth: depth === '*' ? Infinity : Number(depth), parallelizeRecursion: false });
ctx.body = (0, misc_1.asyncGeneratorToReadable)((0, misc_1.filterMapGenerator)(walker, async (el) => {
const isFolder = await (0, vfs_1.nodeIsDirectory)(el);
return !folders && isFolder ? undefined
: prepend + (0, misc_1.pathEncode)((0, vfs_1.getNodeName)(el)) + (isFolder ? '/' : '') + '\n';
}));
}
;