"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.fileAttrDb = void 0;
exports.storeFileAttr = storeFileAttr;
exports.loadFileAttr = loadFileAttr;
exports.purgeFileAttr = purgeFileAttr;
exports.moveStoredFileAttrs = moveStoredFileAttrs;
exports.deleteStoredFileAttrs = deleteStoredFileAttrs;
const kvstorage_1 = require("@rejetto/kvstorage");
const util_1 = require("util");
const promises_1 = require("fs/promises");
const cross_1 = require("./cross");
const first_1 = require("./first");
const promises_2 = require("node:fs/promises");
const util_files_1 = require("./util-files");
const const_1 = require("./const");
const path_1 = require("path");
const fsx = (0, cross_1.try_)(() => {
    const lib = require('fs-x-attributes');
    return { set: (0, util_1.promisify)(lib.set), get: (0, util_1.promisify)(lib.get) };
}, () => console.warn('fs-x-attributes not available'));
const FN = 'file-attr.kv';
exports.fileAttrDb = new kvstorage_1.KvStorage({ defaultPutDelay: 1000, maxPutDelay: 5000 });
(0, first_1.onProcessExit)(() => exports.fileAttrDb.close());
exports.fileAttrDb.open(FN).catch(e => console.error(String(e)));
const FILE_ATTR_PREFIX = 'user.hfs.'; // user. prefix to be linux compatible
const FILE_ATTR_KEY_SEPARATOR = '|';
/* @param v must be JSON-able or undefined */
async function storeFileAttr(path, k, v) {
    const s = await (0, util_files_1.statWithTimeout)(path).catch(() => null);
    // since we don't have fsx.remove, we simulate it with an empty string
    if (s && await fsx?.set(path, FILE_ATTR_PREFIX + k, v === undefined ? '' : JSON.stringify(v)).then(() => 1, () => 0)) {
        if (const_1.IS_WINDOWS)
            (0, promises_2.utimes)(path, s.atime, s.mtime); // restore timestamps, necessary only on Windows
        return true;
    }
    // fallback to our kv-storage
    return await exports.fileAttrDb.put(fileAttrKey(path, k), v)?.catch((e) => {
        console.error("Couldn't store metadata on", path, String(e.message || e));
        return false;
    }) ?? true; // if put is undefined, the value was already there
}
async function loadFileAttr(path, k) {
    return await fsx?.get(path, FILE_ATTR_PREFIX + k)
        .then((x) => x === '' ? undefined : (0, cross_1.tryJson)(String(x)), () => exports.fileAttrDb.isOpen() ? exports.fileAttrDb.get(fileAttrKey(path, k)).catch(console.error) : null)
        ?? undefined; // normalize, as we get null instead of undefined on windows
}
async function purgeFileAttr() {
    let n = 0;
    await Promise.all(Array.from(exports.fileAttrDb.keys()).map(k => {
        const fn = splitFileAttrKey(k)?.filePath;
        return fn && (0, promises_1.access)(fn).catch(() => n++ && void exports.fileAttrDb.del(k));
    }));
    if (n)
        await exports.fileAttrDb.rewrite();
    console.log(`Removed ${n} entrie(s)`);
}
async function moveStoredFileAttrs(fromPath, toPath) {
    try {
        if (fromPath === toPath || !exports.fileAttrDb.isOpen())
            return;
        const entries = storedFileAttrEntries();
        const affectedEntries = entries.filter(x => isSameOrInside(fromPath, x.filePath));
        if (!affectedEntries.length)
            return;
        const affectedWithValues = await Promise.all(affectedEntries.map(async (x) => ({
            ...x,
            value: await exports.fileAttrDb.get(x.key)
        })));
        const oldDestinationKeys = entries.filter(x => isSameOrInside(toPath, x.filePath)).map(x => x.key);
        // destination attrs must be cleared first because a replaced file may not have all attrs owned by the source
        await Promise.all(oldDestinationKeys.map(k => exports.fileAttrDb.del(k)));
        await Promise.all(affectedWithValues.map(async ({ key, filePath, attr, value }) => {
            const rel = (0, path_1.relative)(fromPath, filePath);
            // physical path keys the fallback DB, so filesystem moves must carry descendant entries explicitly
            await exports.fileAttrDb.put(fileAttrKey((0, path_1.join)(toPath, rel), attr), value);
            await exports.fileAttrDb.del(key);
        }));
    }
    // metadata sync runs after the filesystem mutation, so it must not report the completed file operation as failed
    catch (e) {
        console.error("Couldn't move metadata in file-attr DB", fromPath, toPath, String(e.message || e));
    }
}
async function deleteStoredFileAttrs(path) {
    try {
        if (!exports.fileAttrDb.isOpen())
            return;
        const keys = storedFileAttrEntries().filter(x => isSameOrInside(path, x.filePath)).map(x => x.key);
        await Promise.all(keys.map(k => exports.fileAttrDb.del(k)));
    }
    // metadata cleanup runs after deletion, so surfacing this would leave clients seeing a false delete failure
    catch (e) {
        console.error("Couldn't delete metadata from file-attr DB", path, String(e.message || e));
    }
}
function storedFileAttrEntries() {
    return (0, cross_1.onlyTruthy)(Array.from(exports.fileAttrDb.keys()).map(splitFileAttrKey));
}
function splitFileAttrKey(key) {
    const i = key.lastIndexOf(FILE_ATTR_KEY_SEPARATOR);
    if (i < 0)
        return;
    return {
        key,
        filePath: key.slice(0, i),
        attr: key.slice(i + 1)
    };
}
function fileAttrKey(path, attr) {
    return path + FILE_ATTR_KEY_SEPARATOR + attr;
}
function isSameOrInside(parent, path) {
    const rel = (0, path_1.relative)(parent, path);
    return rel === '' || Boolean(rel) && !rel.startsWith('..') && !(0, path_1.isAbsolute)(rel);
}
