UNPKG

hfs

Version:
443 lines (442 loc) 20.4 kB
"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.vfs = void 0; exports.permsFromParent = permsFromParent; exports.isSameFilenameAs = isSameFilenameAs; exports.applyParentToChild = applyParentToChild; exports.urlToNode = urlToNode; exports.nodeStats = nodeStats; exports.getNodeByName = getNodeByName; exports.saveVfs = saveVfs; exports.isRoot = isRoot; exports.getNodeName = getNodeName; exports.nodeIsDirectory = nodeIsDirectory; exports.hasDefaultFile = hasDefaultFile; exports.nodeIsLink = nodeIsLink; exports.hasPermission = hasPermission; exports.statusCodeForMissingPerm = statusCodeForMissingPerm; exports.walkNode = walkNode; exports.masksCouldGivePermission = masksCouldGivePermission; exports.parentMaskApplier = parentMaskApplier; const promises_1 = __importDefault(require("fs/promises")); const path_1 = require("path"); const misc_1 = require("./misc"); const lodash_1 = __importDefault(require("lodash")); const config_1 = require("./config"); const const_1 = require("./const"); const events_1 = __importDefault(require("./events")); const perm_1 = require("./perm"); const auth_1 = require("./auth"); const fswin_1 = __importDefault(require("fswin")); const comments_1 = require("./comments"); const walkDir_1 = require("./walkDir"); const node_stream_1 = require("node:stream"); const showHiddenFiles = (0, config_1.defineConfig)('show_hidden_files', false); function permsFromParent(parent, child) { const ret = {}; for (const k of misc_1.PERM_KEYS) { let p = parent; let inheritedPerm; while (p) { inheritedPerm = p[k]; // in case of object without children, parent is skipped in favor of the parent's parent if (!(0, misc_1.isWhoObject)(inheritedPerm)) break; inheritedPerm = inheritedPerm.children; if (inheritedPerm !== undefined) break; p = p.parent; } if (inheritedPerm !== undefined && child[k] === undefined) // small optimization: don't expand the object ret[k] = inheritedPerm; } return lodash_1.default.isEmpty(ret) ? undefined : ret; } function inheritFromParent(parent, child) { var _a, _b, _c; Object.assign(child, permsFromParent(parent, child)); if (typeof parent.mime === 'object' && typeof child.mime === 'object') lodash_1.default.defaults(child.mime, parent.mime); else if (parent.mime) (_a = child.mime) !== null && _a !== void 0 ? _a : (child.mime = parent.mime); if (parent.accept) (_b = child.accept) !== null && _b !== void 0 ? _b : (child.accept = parent.accept); if (parent.default) (_c = child.default) !== null && _c !== void 0 ? _c : (child.default = parent.default); return child; } function isSameFilenameAs(name) { const normalized = normalizeFilename(name); return (other) => normalized === normalizeFilename(typeof other === 'string' ? other : getNodeName(other)); } function normalizeFilename(x) { return (const_1.IS_WINDOWS || const_1.IS_MAC ? x.toLocaleLowerCase() : x).normalize(); } async function applyParentToChild(child, parent, name) { var _a, _b; const ret = { original: child, // this can be overridden by passing an 'original' in `child` ...child, isFolder: (_a = child === null || child === void 0 ? void 0 : child.isFolder) !== null && _a !== void 0 ? _a : (((_b = child === null || child === void 0 ? void 0 : child.children) === null || _b === void 0 ? void 0 : _b.length) > 0 || undefined), // isFolder is hidden in original node, so we must read it to copy it isTemp: true, parent, }; name || (name = child ? getNodeName(child) : ''); inheritMasks(ret, parent, name); await parentMaskApplier(parent)(ret, name); inheritFromParent(parent, ret); return ret; } async function urlToNode(url, ctx, parent = exports.vfs, getRest) { let initialSlashes = 0; while (url[initialSlashes] === '/') initialSlashes++; let nextSlash = url.indexOf('/', initialSlashes); const name = decodeURIComponent(url.slice(initialSlashes, nextSlash < 0 ? undefined : nextSlash)); if (!name) return parent; const rest = nextSlash < 0 ? '' : url.slice(nextSlash + 1, url.endsWith('/') ? -1 : undefined); const ret = await getNodeByName(name, parent); if (!ret) return; if (rest || (ret === null || ret === void 0 ? void 0 : ret.original)) return urlToNode(rest, ctx, ret, getRest); if (ret.source) try { if (!showHiddenFiles.get() && await isHiddenFile(ret.source)) throw 'hiddenFile'; ret.isFolder = (await nodeStats(ret)).isDirectory(); // throws if it doesn't exist on disk } catch (_a) { if (!getRest) return; const rest = ret.source.slice(parent.source.length); // parent has source, otherwise !ret.source || ret.original getRest((0, misc_1.removeStarting)('/', rest)); return parent; } return ret; } async function nodeStats(ret) { if (ret.stats) if (lodash_1.default.isPlainObject(ret.stats)) delete ret.stats; // legacy pre-55-alpha1 else return ret.stats; const stats = ret.source ? await promises_1.default.stat(ret.source) : undefined; (0, misc_1.setHidden)(ret, { stats }); return stats; } async function isHiddenFile(path) { return const_1.IS_WINDOWS ? new Promise(res => fswin_1.default.getAttributes(path, x => res(x === null || x === void 0 ? void 0 : x.IS_HIDDEN))) : path[path.lastIndexOf('/') + 1] === '.'; } async function getNodeByName(name, parent) { var _a; // does the tree node have a child that goes by this name, otherwise attempt disk const child = ((_a = parent.children) === null || _a === void 0 ? void 0 : _a.find(isSameFilenameAs(name))) || childFromDisk(); return child && applyParentToChild(child, parent, name); function childFromDisk() { if (!parent.source) return; const ret = {}; let onDisk = name; if (parent.rename) { // reverse the mapping for (const [from, to] of Object.entries(parent.rename)) if (name === to) { onDisk = from; break; // found, search no more } ret.rename = renameUnderPath(parent.rename, name); } if (!(0, misc_1.isValidFileName)(onDisk)) return; ret.source = (0, path_1.join)(parent.source, onDisk); ret.original = undefined; // this will overwrite the 'original' set in applyParentToChild, so we know this is not part of the vfs return ret; } } exports.vfs = {}; (0, config_1.defineConfig)('vfs', {}).sub(data => exports.vfs = (function recur(node) { const { masks } = node; lodash_1.default.each(masks, (v, mask) => { if (v.maskOnly) { masks[`${mask}|${v.maskOnly}|`] = v = lodash_1.default.omit(v, 'maskOnly'); delete masks[mask]; } recur(v); }); if (node.children) for (const c of node.children) recur(c); return node; })(data)); function saveVfs() { return (0, config_1.setConfig)({ vfs: lodash_1.default.cloneDeep(exports.vfs) }, true); } function isRoot(node) { return node === exports.vfs; } function getNodeName(node) { if (isRoot(node)) return ''; if (node.name) return node.name; const { source } = node; if (!source) return ''; // shoulnd't happen if (source === '/') return 'root'; // better name than if (/^[a-zA-Z]:\\?$/.test(source)) return source.slice(0, 2); // exclude trailing slash const base = (0, path_1.basename)(source); if (/^[./\\]*$/.test(base)) // if empty or special-chars-only return (0, path_1.basename)((0, path_1.resolve)(source)); // resolve to try to get more if (base.includes('\\') && !source.includes('/')) // source was Windows but now we are running posix. This probably happens only debugging, so it's DX return source.slice(source.lastIndexOf('\\') + 1); return base; } async function nodeIsDirectory(node) { var _a; if (node.isFolder !== undefined) return node.isFolder; if (nodeIsLink(node)) return false; if (((_a = node.children) === null || _a === void 0 ? void 0 : _a.length) || !node.source) return true; const isFolder = await nodeStats(node).then(x => x.isDirectory(), () => false); (0, misc_1.setHidden)(node, { isFolder }); // don't make it to the storage (a node.isTemp doesn't need it to be hidden) return isFolder; } async function hasDefaultFile(node, ctx) { return node.default && await nodeIsDirectory(node) && await urlToNode(node.default, ctx, node) || undefined; } function nodeIsLink(node) { return node.url; } function hasPermission(node, perm, ctx) { return !statusCodeForMissingPerm(node, perm, ctx, false); } function statusCodeForMissingPerm(node, perm, ctx, assign = true) { const ret = getCode(); if (ret && assign) { ctx.status = ret; ctx.body = ret === const_1.HTTP_UNAUTHORIZED ? "Unauthorized" : "Forbidden"; } return ret; function getCode() { var _a; if ((isRoot(node) || node.original) && perm === 'can_delete' // we currently don't allow deleting of vfs nodes from frontend || !node.source && perm === 'can_upload') // Upload possible only if we know where to store. First check node.source because is supposedly faster. return const_1.HTTP_FORBIDDEN; // calculate value of permission resolving references to other permissions, avoiding infinite loop let who; let max = misc_1.PERM_KEYS.length; let cur = perm; do { who = node[cur]; if ((0, misc_1.isWhoObject)(who)) who = who.this; who !== null && who !== void 0 ? who : (who = misc_1.defaultPerms[cur]); if (typeof who !== 'string' || who === misc_1.WHO_ANY_ACCOUNT) break; if (!max--) { console.error(`endless loop in permission ${perm}=${(_a = node[perm]) !== null && _a !== void 0 ? _a : misc_1.defaultPerms[perm]} for ${node.url || getNodeName(node)}`); return misc_1.HTTP_SERVER_ERROR; } cur = who; } while (1); if (Array.isArray(who)) return (0, perm_1.ctxBelongsTo)(ctx, who) ? 0 : const_1.HTTP_UNAUTHORIZED; return typeof who === 'boolean' ? (who ? 0 : const_1.HTTP_FORBIDDEN) : who === misc_1.WHO_ANY_ACCOUNT ? ((0, auth_1.getCurrentUsername)(ctx) ? 0 : const_1.HTTP_UNAUTHORIZED) : (0, misc_1.throw_)(Error(`invalid permission: ${perm}=${(0, misc_1.try_)(() => JSON.stringify(who))}`)); } } // it's the responsibility of the caller to verify you have list permission on parent, as callers have different needs. async function* walkNode(parent, { ctx, depth = Infinity, prefixPath = '', requiredPerm, onlyFolders = false, onlyFiles = false, parallelizeRecursion = true, } = {}) { let started = false; const stream = new node_stream_1.Readable({ objectMode: true, async read() { var _a; if (started) return; // for simplicity, we care about starting, and never suspend started = true; const { children, source } = parent; const taken = prefixPath ? undefined : new Set(); const maskApplier = parentMaskApplier(parent); const visitLater = []; if (children) for (const child of children) { const nodeName = getNodeName(child); const name = prefixPath + nodeName; taken === null || taken === void 0 ? void 0 : taken.add(normalizeFilename(name)); const item = { ...child, original: child, name }; if (await cantSee(item)) continue; if (item.source && !((_a = item.children) === null || _a === void 0 ? void 0 : _a.length)) // real items must be accessible, unless there's more to it try { await promises_1.default.access(item.source); } catch (_b) { continue; } const isFolder = await nodeIsDirectory(child); if (onlyFiles ? !isFolder : (!onlyFolders || isFolder)) stream.push(item); if (!depth || !isFolder || cantRecur(item)) continue; inheritMasks(item, parent); visitLater.push([item, name]); // prioritize siblings } try { if (!source) return; if (requiredPerm && ctx // no permission, no reason to continue (at least for dynamic elements) && !hasPermission(parent, requiredPerm, ctx) && !masksCouldGivePermission(parent.masks, requiredPerm)) return; try { await (0, walkDir_1.walkDir)(source, { depth, ctx, hidden: showHiddenFiles.get(), parallelizeRecursion }, async (entry) => { var _a; if (ctx === null || ctx === void 0 ? void 0 : ctx.isAborted()) { stream.push(null); return null; } if ((0, comments_1.usingDescriptIon)() && entry.name === comments_1.DESCRIPT_ION) return; const { path } = entry; const isFolder = entry.isDirectory(); let renamed = (_a = parent.rename) === null || _a === void 0 ? void 0 : _a[path]; if (renamed) { const dir = (0, path_1.dirname)(path); // if `path` isn't just the name, copy its dir in renamed if (dir !== '.') renamed = dir + '/' + renamed; } const name = prefixPath + (renamed || path); if (taken === null || taken === void 0 ? void 0 : taken.has(normalizeFilename(name))) // taken by vfs node above return false; // false just in case it's a folder const item = { name, isFolder, source: (0, path_1.join)(source, path) }; if (await cantSee(item)) // can't see: don't produce and don't recur return false; if (onlyFiles ? !isFolder : (!onlyFolders || isFolder)) stream.push(item); if (cantRecur(item)) return false; }); } catch (e) { console.debug('walkNode', source, e); // ENOTDIR, or lacking permissions } } finally { for (const [item, name] of visitLater) for await (const x of walkNode(item, { depth: depth - 1, prefixPath: name + '/', ctx, requiredPerm, onlyFolders, parallelizeRecursion })) stream.push(x); stream.push(null); } function cantRecur(item) { return ctx && !hasPermission(item, 'can_list', ctx); } // item will be changed, so be sure to pass a temp node async function cantSee(item) { await maskApplier(item); inheritFromParent(parent, item); if (ctx && !hasPermission(item, 'can_see', ctx)) return true; item.isTemp = true; } } }); // must use a stream to be able to work with the callback-based mechanism of walkDir, but Readable is not typed so we wrap it with a generator for await (const item of stream) yield item; } function masksCouldGivePermission(masks, perm) { return masks !== undefined && Object.values(masks).some(props => props[perm] || masksCouldGivePermission(props.masks, perm)); } function parentMaskApplier(parent) { // rules are met in the parent.masks object from nearest to farthest, but since we finally apply with _.defaults, the nearest has precedence in the final result const matchers = (0, misc_1.onlyTruthy)(lodash_1.default.map(parent.masks, (mods, k) => { if (!mods) return; const mustBeFolder = (() => { if (k.at(-1) !== '|') return; // parse special flag syntax as suffix |FLAG| inside the key. This allows specifying different flags with the same mask using separate keys. To avoid syntax conflicts with the rest of the file-mask, we look for an ending pipe, as it has no practical use. Ending-pipe was preferred over starting-pipe to leave the rest of the logic (inheritMasks) untouched. const i = k.lastIndexOf('|', k.length - 2); if (i < 0) return; const type = k.slice(i + 1, -1); k = k.slice(0, i); // remove return type === 'folders'; })(); const m = /^(!?)\*\*\//.exec(k); // ** globstar matches also zero subfolders, so this mask must be applied here too k = m ? m[1] + k.slice(m[0].length) : !k.includes('/') ? k : ''; return k && { mods, matcher: (0, misc_1.makeMatcher)(k), mustBeFolder }; })); return async (item, virtualBasename = (0, path_1.basename)(getNodeName(item))) => { let isFolder = undefined; for (const { matcher, mods, mustBeFolder } of matchers) { if (mustBeFolder !== undefined) { isFolder !== null && isFolder !== void 0 ? isFolder : (isFolder = await nodeIsDirectory(item)); if (mustBeFolder !== isFolder) continue; } if (!matcher(virtualBasename)) continue; item.masks && (item.masks = lodash_1.default.merge(lodash_1.default.cloneDeep(mods.masks), item.masks)); // item.masks must take precedence lodash_1.default.defaults(item, mods); } }; } function inheritMasks(item, parent, virtualBasename = getNodeName(item)) { const { masks } = parent; if (!masks) return; const o = {}; for (const [k, v] of Object.entries(masks)) { if (k.startsWith('**')) { o[k] = v; continue; } const i = k.indexOf('/'); if (i < 0) continue; if (!(0, misc_1.matches)(virtualBasename, k.slice(0, i))) continue; o[k.slice(i + 1)] = v; } if (Object.keys(o).length) item.masks = Object.assign(o, item.masks); // don't change item.masks object as it is the same object of item.original } function renameUnderPath(rename, path) { if (!rename) return rename; const match = path + '/'; rename = Object.fromEntries(Object.entries(rename).map(([k, v]) => [k.startsWith(match) ? k.slice(match.length) : '', v])); delete rename['']; return lodash_1.default.isEmpty(rename) ? undefined : rename; } events_1.default.on('accountRenamed', ({ from, to }) => { ; (function renameInNode(n) { var _a; for (const k of misc_1.PERM_KEYS) renameInPerm(n[k]); if (n.masks) Object.values(n.masks).forEach(renameInNode); (_a = n.children) === null || _a === void 0 ? void 0 : _a.forEach(renameInNode); })(exports.vfs); saveVfs(); function renameInPerm(a) { if (!Array.isArray(a)) return; for (let i = 0; i < a.length; i++) if (a[i] === from) a[i] = to; } });