UNPKG

hfs

Version:
187 lines (186 loc) 10.9 kB
"use strict"; 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'; })); }