"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.serveSharedFiles = exports.guiFilesMiddleware = 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 multipartUpload_1 = require("./multipartUpload");
const events_2 = require("events");
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 roots_1 = require("./roots");
const xxhashjs_1 = __importDefault(require("xxhashjs"));
const fs_1 = __importDefault(require("fs"));
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 guiFilesMiddleware = async (ctx, next) => {
    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_)(() => r && new URL(r).pathname?.startsWith(cross_const_1.ADMIN_URI)) ? serveAdminFiles(ctx, next)
            : serveFrontendFiles(ctx, next);
    }
    if (path.startsWith(cross_const_1.FRONTEND_URI) && !path.endsWith('/'))
        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.preventAdminAccess)(ctx) ? (0, errorPages_1.sendErrorPage)(ctx, cross_const_1.HTTP_FORBIDDEN) : serveAdminPrefixed(ctx, next);
    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 ? plugin.icons?.[iconName] : icons_1.customizedIcons?.[iconName];
        if (!file)
            return;
        ctx.state.considerAsGui = true;
        return (0, serveFile_1.serveFile)(ctx, (0, path_1.join)(plugin?.folder || '', icons_1.ICONS_FOLDER, file), const_1.MIME_AUTO);
    }
    return next();
};
exports.guiFilesMiddleware = guiFilesMiddleware;
const serveSharedFiles = async (ctx, next) => {
    const { path } = ctx;
    const { get } = ctx.query;
    const getUploadTempHash = get === cross_const_1.UPLOAD_TEMP_HASH;
    if (ctx.method === 'PUT' || getUploadTempHash) { // PUT is what you get with `curl -T file url/`
        const decPath = (0, misc_1.safeDecodeURIComponent)(path, '');
        const fn = (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, true); // we don't require the folder to already exist, but to be mapped on disk AND to have proper permissions
        if (!folder)
            return (0, errorPages_1.sendErrorPage)(ctx, cross_const_1.HTTP_NOT_FOUND);
        ctx.state.uploadPath = decPath;
        if (getUploadTempHash)
            return !folder.source || !(0, misc_1.isValidFileName)(fn) ? (0, errorPages_1.sendErrorPage)(ctx, cross_const_1.HTTP_NOT_FOUND)
                : (0, vfs_1.statusCodeForMissingPerm)(folder, 'can_upload', ctx) ? null
                    : (0, misc_1.loadFileCached)((0, upload_1.getUploadTempFor)((0, path_1.join)(folder.source, fn)), calcHash) // negligible memory leak
                        .then(hash => ctx.body = hash, e => ctx.status = e?.code === 'ENOENT' ? cross_const_1.HTTP_NOT_FOUND : cross_const_1.HTTP_SERVER_ERROR);
        const dest = (0, upload_1.uploadWriter)(folder, folderUri, fn, 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/
        return (0, multipartUpload_1.handleMultipartUpload)(ctx, node);
    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 ((await events_1.default.emitAsync('deleting', { node, ctx }))?.isDefaultPrevented())
                return ctx.status = cross_const_1.HTTP_FAILED_DEPENDENCY;
            await (0, promises_1.rm)(source, { recursive: true });
            await (0, misc_1.deleteStoredFileAttrs)(source);
            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;
        }
    }
    if (path.endsWith('/') && !get) { // final slash needed on browsers to make resource urls working with html pages
        const found = await (0, vfs_1.getDefaultFile)(node, ctx);
        if (found && /\.html?/i.test((0, vfs_1.getNodeName)(node = found)))
            ctx.state.considerAsGui = true;
    }
    if (get === 'icon')
        return (0, serveFile_1.serveFile)(ctx, node.icon || '|'); // pipe to cause not-found
    if (!(0, vfs_1.nodeIsFolder)(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', cross_const_1.BASIC_AUTHENTICATE_HEADER), (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 || cross_const_1.BASIC_AUTHENTICATE_HEADER); // 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.serveSharedFiles = serveSharedFiles;
async function sendFolderList(node, ctx) {
    if ((await events_1.default.emitAsync('getList', { node, ctx }))?.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 requestBase = URL.protocol + '//' + URL.host + ctx.state.revProxyPath;
        const configuredBaseUrl = prepend === undefined && listen_1.baseUrl.get();
        const configuredRoot = configuredBaseUrl && roots_1.roots.compiled()(listen_1.baseUrl.compiled() || '');
        const pathInConfiguredRoot = configuredRoot && (configuredRoot === '/' ? ctx.path
            : ctx.path.startsWith(configuredRoot) ? ctx.path.slice(configuredRoot.length - 1)
                : ctx.path === configuredRoot.slice(0, -1) ? '/'
                    : false);
        // base_url may expose a host-rooted home; use it only for VFS paths inside that host root
        const [base, path] = !configuredBaseUrl ? [requestBase, ctx.path]
            : pathInConfiguredRoot === false ? [requestBase, ctx.path]
                : [configuredBaseUrl, pathInConfiguredRoot || ctx.path];
        // redo the encoding our way, keeping unicode chars unchanged. decode each segment separately because decodeURI preserves reserved escapes like %3A, which pathEncode would double-encode
        prepend = base + (0, misc_1.pathDecodeSegments)(path, misc_1.pathEncode);
    }
    const walker = (0, vfs_1.walkNode)(node, { ctx, depth: depth === '*' ? Infinity : Number(depth), parallelizeRecursion: false }); // parallelization produces out-of-order results, and we don't want it like that here
    ctx.body = (0, misc_1.asyncGeneratorToReadable)((0, misc_1.filterMapGenerator)(walker, async (el) => {
        const isFolder = (0, vfs_1.nodeIsFolder)(el);
        return !folders && isFolder ? undefined
            : prepend + (0, misc_1.pathEncode)((0, vfs_1.getNodeName)(el)) + (isFolder ? '/' : '') + '\n';
    }));
}
async function calcHash(fn, limit = Infinity) {
    const hash = xxhashjs_1.default.h32();
    const stream = new stream_1.Transform({
        transform(chunk, enc, done) {
            hash.update(chunk);
            done();
        }
    });
    fs_1.default.createReadStream(fn, { end: limit - 1 }).pipe(stream);
    console.debug('Hashing', fn);
    await (0, events_2.once)(stream, 'finish');
    console.debug('Hashed', fn);
    return hash.digest().toString(16);
}
