"use strict";
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.pickProps = pickProps;
exports.simplifyName = simplifyName;
const vfs_1 = require("./vfs");
const lodash_1 = __importDefault(require("lodash"));
const promises_1 = require("fs/promises");
const apiMiddleware_1 = require("./apiMiddleware");
const path_1 = require("path");
const misc_1 = require("./misc");
const const_1 = require("./const");
const util_os_1 = require("./util-os");
const listen_1 = require("./listen");
const SendList_1 = require("./SendList");
const walkDir_1 = require("./walkDir");
const roots_1 = require("./roots");
// to manipulate the tree we need the original node
async function urlToNodeOriginal(uri) {
    const n = await (0, vfs_1.urlToNode)(uri);
    return n?.isTemp ? n.original : n;
}
exports.default = {
    async get_vfs() {
        return { root: await recur() };
        async function recur(node = vfs_1.vfs) {
            const { source } = node;
            const stats = await (0, vfs_1.nodeStats)(node);
            const isFolder = (0, vfs_1.nodeIsFolder)(node);
            const copyStats = stats ? lodash_1.default.pick(stats, ['size', 'birthtime', 'mtime'])
                : { size: source ? -1 : undefined };
            if (copyStats.mtime && (stats?.mtimeMs - stats?.birthtimeMs) < 1000)
                delete copyStats.mtime;
            const inherited = node.parent && (0, vfs_1.permsFromParent)(node.parent, {});
            const byMasks = node.original && lodash_1.default.pickBy(node, (v, k) => v !== node.original[k] // something is changing me...
                && !(inherited && k in inherited) // ...and it's not inheritance...
                && misc_1.PERM_KEYS.includes(k)); // ...must be masks. Please limit this to perms
            return {
                ...copyStats,
                ...node.original || node,
                inherited,
                byMasks: lodash_1.default.isEmpty(byMasks) ? undefined : byMasks,
                website: node.children?.some((0, vfs_1.isSameFilenameAs)('index.html'))
                    || isFolder && source && await (0, misc_1.statWithTimeout)((0, path_1.join)(source, 'index.html')).then(() => true, () => undefined)
                    || undefined,
                name: (0, vfs_1.getNodeName)(node),
                type: isFolder ? 'folder' : undefined,
                children: node.children && await Promise.all(node.children.map(async (child) => recur(await (0, vfs_1.applyParentToChild)(child, node))))
            };
        }
    },
    async set_vfs({ uri, props, uriRemaps = {} }) {
        const n = uri && await urlToNodeOriginal(uri);
        if (!n)
            return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'path not found');
        if (props.name && props.name !== (0, vfs_1.getNodeName)(n)) {
            if (!(0, misc_1.isValidFileName)(props.name))
                return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad name');
            // check for siblings with the same name
            const parent = await urlToNodeOriginal((0, path_1.dirname)(uri));
            if (parent?.children?.find(x => (0, vfs_1.getNodeName)(x) === props.name))
                return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT, 'name already present');
        }
        Object.assign(n, sanitizeVfsProps(props));
        simplifyName(n);
        n.isFolder = undefined; // reset field, it will be set by saveVfs
        await (0, vfs_1.saveVfs)();
        if (!(0, vfs_1.isRoot)(n)) // not actually used by admin-panel but still
            uriRemaps[uri] = uriForNode(uri, n); // just in case the current node was modified
        await updateRootsForVfsUriRemaps(uriRemaps);
        return n;
        function uriForNode(uri, n) {
            return (0, misc_1.enforceFinal)('/', (0, path_1.dirname)(uri).replace(/\\/g, '/')) + (0, misc_1.pathEncode)((0, vfs_1.getNodeName)(n)) + ((0, vfs_1.nodeIsFolder)(n) ? '/' : '');
        }
    },
    // legacy – not currently used by the UI
    async move_vfs({ from, parent }) {
        if (!from || !parent)
            return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST);
        const fromNode = await urlToNodeOriginal(from);
        if (!fromNode)
            return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'from not found');
        if ((0, vfs_1.isRoot)(fromNode))
            return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'from is root');
        if (parent.startsWith(from))
            return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'incompatible parent');
        const parentNode = await urlToNodeOriginal(parent);
        if (!parentNode)
            return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'parent not found');
        const name = (0, vfs_1.getNodeName)(fromNode);
        if (parentNode.children?.find(x => name === (0, vfs_1.getNodeName)(x)))
            return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT, 'item with same name already present in destination');
        const oldParent = await urlToNodeOriginal((0, path_1.dirname)(from));
        lodash_1.default.pull(oldParent.children, fromNode);
        if (lodash_1.default.isEmpty(oldParent.children)) {
            delete oldParent.children;
        }
        ;
        (parentNode.children ||= []).push(fromNode);
        await (0, vfs_1.saveVfs)();
        await updateRootsForVfsUriRemaps({
            [from]: (0, misc_1.enforceFinal)('/', parent) + (0, misc_1.pathEncode)(name) + ((0, vfs_1.nodeIsFolder)(fromNode) ? '/' : '')
        });
        return {};
    },
    // legacy – not currently used by the UI
    async add_vfs({ parent, source, name, ...rest }) {
        if (!source && !name)
            return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'name or source required');
        if (name && !(0, misc_1.isValidFileName)(name))
            return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad name');
        const parentNode = parent ? await urlToNodeOriginal(parent) : vfs_1.vfs;
        if (!parentNode)
            return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'parent not found');
        if (!(0, vfs_1.nodeIsFolder)(parentNode))
            return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_ACCEPTABLE, 'parent not a folder');
        if ((0, misc_1.isWindowsDrive)(source))
            source += '\\'; // slash must be included, otherwise it will refer to the cwd of that drive
        const isFolder = source && await (0, misc_1.isDirectory)(source);
        if (source && isFolder === undefined)
            return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'source not found');
        const child = { source, name, ...sanitizeVfsProps(rest) };
        name = (0, vfs_1.getNodeName)(child); // could be not given as input
        const ext = (0, path_1.extname)(name);
        const noExt = ext ? name.slice(0, -ext.length) : name;
        let idx = 2;
        while (parentNode.children?.find((0, vfs_1.isSameFilenameAs)(name)))
            name = `${noExt} ${idx++}${ext}`;
        child.name = name;
        simplifyName(child);
        (parentNode.children ||= []).unshift(child);
        await (0, vfs_1.saveVfs)();
        const link = rest.url ? undefined : await (0, listen_1.getBaseUrlOrDefault)()
            + (parent ? (0, misc_1.enforceStarting)('/', (0, misc_1.enforceFinal)('/', parent)) : '/')
            + encodeURIComponent((0, vfs_1.getNodeName)(child))
            + (isFolder ? '/' : '');
        return { name, link };
    },
    // legacy – not currently used by the UI
    async del_vfs({ uris }) {
        if (!uris || !Array.isArray(uris))
            return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad uris');
        return {
            errors: await Promise.all(uris.map(async (uri) => {
                if (typeof uri !== 'string')
                    return const_1.HTTP_BAD_REQUEST;
                const node = await urlToNodeOriginal(uri);
                if (!node)
                    return const_1.HTTP_NOT_FOUND;
                if ((0, vfs_1.isRoot)(node))
                    return const_1.HTTP_NOT_ACCEPTABLE;
                const parentNode = await urlToNodeOriginal((0, path_1.dirname)(uri));
                const c = parentNode?.children; // since node is not root, parentNode must exist and have children
                if (!c) // inconsistent state
                    return const_1.HTTP_SERVER_ERROR;
                const idx = c.indexOf(node);
                if (idx < 0) // inconsistent state
                    return const_1.HTTP_SERVER_ERROR;
                c.splice(idx, 1);
                if (!c.length)
                    parentNode.children = undefined;
                return 0; // error code 0 is OK
            })).finally(vfs_1.saveVfs)
        };
    },
    get_cwd() {
        return { path: process.cwd() };
    },
    async resolve_path({ path, closestFolder }) {
        path = (0, path_1.resolve)(path);
        if (closestFolder)
            while (path && !await (0, misc_1.isDirectory)(path))
                path = (0, path_1.dirname)(path);
        return { path, isFolder: await (0, misc_1.isDirectory)(path) };
    },
    async mkdir({ path }) {
        await (0, promises_1.mkdir)(path, { recursive: true });
        return {};
    },
    get_disk_spaces: util_os_1.getDiskSpaces,
    get_ls({ path, files = true, fileMask }, ctx) {
        return new SendList_1.SendListReadable({
            async doAtStart(list) {
                if (!path && const_1.IS_WINDOWS) {
                    try {
                        for (const n of await (0, util_os_1.getDrives)())
                            list.add({ n, k: 'd' });
                    }
                    catch (error) {
                        console.debug(error);
                    }
                    return;
                }
                const sendPropsAsap = (0, util_os_1.getDiskSpace)(path).then(x => x && list.props(x));
                try {
                    const matching = (0, misc_1.makeMatcher)(fileMask);
                    path = (0, misc_1.isWindowsDrive)(path) ? path + '\\' : (0, path_1.resolve)(path || '/');
                    await (0, walkDir_1.walkDir)(path, { ctx }, async (entry) => {
                        if (ctx.isAborted())
                            return null;
                        const { path: name } = entry;
                        const isDir = entry.isDirectory();
                        if (!isDir)
                            if (!files || fileMask && !matching(name))
                                return;
                        try {
                            const stats = entry.stats || await (0, misc_1.statWithTimeout)((0, path_1.join)(path, name));
                            list.add({
                                n: name,
                                s: stats.size,
                                c: stats.birthtime.toJSON(),
                                m: stats.mtime.toJSON(),
                                k: isDir ? 'd' : undefined,
                            });
                        }
                        catch { } // just ignore entries we can't stat
                    });
                    await sendPropsAsap.catch(() => { });
                    list.close();
                }
                catch (e) {
                    list.error(e.code || e.message || String(e), true);
                }
            }
        });
    },
    async windows_integration({ parent }) {
        const status = await (0, listen_1.getServerStatus)(true);
        const useHttp = status.http.listening;
        const h = useHttp ? status.http : status.https; // prefer http on localhost
        const url = h.srv.name + '://localhost:' + h.port;
        for (const k of ['*', 'Directory']) {
            await (0, util_os_1.reg)('add', WINDOWS_REG_KEY.replace('*', k), '/ve', '/f', '/d', 'Add to HFS (new)');
            await (0, util_os_1.reg)('add', WINDOWS_REG_KEY.replace('*', k), '/v', 'icon', '/f', '/d', const_1.IS_BINARY ? process.execPath : const_1.APP_PATH + '\\hfs.ico');
            await (0, util_os_1.reg)('add', WINDOWS_REG_KEY.replace('*', k) + '\\command', '/ve', '/f', '/d', `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -Command "
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12;
            $wsh = New-Object -ComObject Wscript.Shell;
            $j = @{parent=@'\n${parent}\n'@; source=@'\n%1\n'@} | ConvertTo-Json -Compress
            $j = [System.Text.Encoding]::UTF8.GetBytes($j);
            ${useHttp ? '' : '[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}'}  
            try {
                $res = Invoke-WebRequest -Uri '${url}/~/api/add_vfs' -UseBasicParsing -Method POST -Headers @{ 'x-hfs-anti-csrf' = '1' } -ContentType 'application/json' -TimeoutSec 3 -Body $j; 
                $json = $res.Content | ConvertFrom-Json; $link = $json.link; $link | Set-Clipboard;
                $wsh.Popup('The link is ready to be pasted');
            } catch { $wsh.Popup($_.Exception.Message + ' – ' + '${url}', 0, 'Error', 16); }"`);
        }
        return {};
    },
    async windows_integrated() {
        return {
            is: await (0, util_os_1.reg)('query', WINDOWS_REG_KEY)
                .then(x => x.includes('REG_SZ'), () => false)
        };
    },
    async windows_remove() {
        for (const k of ['*', 'Directory'])
            await (0, util_os_1.reg)('delete', WINDOWS_REG_KEY.replace('*', k), '/f');
        return {};
    },
};
// pick only selected props, and consider null and empty string as undefined, as it's the default value and we don't want to store it
function pickProps(o, keys) {
    const ret = {};
    if (o && typeof o === 'object')
        for (const k in o)
            if (keys.includes(k))
                ret[k] = o[k] === null || o[k] === '' ? undefined : o[k];
    return ret;
}
function sanitizeVfsProps(props) {
    const ret = pickProps(props, misc_1.VFS_STORED_KEYS);
    if (ret.masks && typeof ret.masks !== 'object')
        ret.masks = undefined;
    if (props?.children === null)
        delete ret.children;
    else if (Array.isArray(props?.children))
        ret.children = !props.children.length ? undefined
            : props.children.map(sanitizeVfsProps);
    return ret;
}
function updateRootsForVfsUriRemaps(uriRemaps = {}) {
    const remaps = Object.entries(uriRemaps)
        .map(([from, to]) => [normalizeVfsId(from), normalizeVfsId(to)])
        .filter(x => x[0] !== x[1])
        .sort(([a], [b]) => b.length - a.length); // longest first because, in case of both parent and child, it's more specific
    let changed = false;
    const updatedRoots = lodash_1.default.mapValues(roots_1.roots.get(), root => {
        if (typeof root !== 'string' || !root)
            return root;
        const normalizedRoot = normalize(root);
        for (const [from, to] of remaps) {
            // roots are stored outside the VFS tree, so rename/move edits need an explicit path remap
            const remappedRoot = replaceUriPrefix(normalizedRoot, from, to);
            if (!remappedRoot)
                continue;
            if (remappedRoot !== root)
                changed = true;
            return remappedRoot;
        }
        return root;
    });
    if (changed)
        roots_1.roots.set(updatedRoots);
    function normalize(uri) {
        return String(uri).replace(/^\/+|^(?!\/)|\/{2,}|\/+$|(?<!\/)$/g, '/');
    }
    function normalizeVfsId(uri) {
        return normalize((0, misc_1.pathDecode)(String(uri))); // admin remaps come from tree ids, while roots are stored as readable VFS paths
    }
    function replaceUriPrefix(uri, oldPrefix, newPrefix) {
        return uri === oldPrefix ? newPrefix
            : uri.startsWith(oldPrefix) ? newPrefix + uri.slice(oldPrefix.length)
                : '';
    }
}
function simplifyName(node) {
    const { name, ...noName } = node;
    if ((0, vfs_1.getNodeName)(noName) === name)
        delete node.name;
}
const WINDOWS_REG_KEY = 'HKCU\\Software\\Classes\\*\\shell\\AddToHFS3';
