UNPKG

hfs

Version:
268 lines (267 loc) 14.1 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.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"); // to manipulate the tree we need the original node async function urlToNodeOriginal(uri) { const n = await (0, vfs_1.urlToNode)(uri); return (n === null || n === void 0 ? void 0 : n.isTemp) ? n.original : n; } const ALLOWED_KEYS = ['name', 'source', 'masks', 'default', 'accept', 'rename', 'mime', 'url', 'target', 'comment', 'icon', 'order', ...misc_1.PERM_KEYS]; const apis = { async get_vfs() { return { root: await recur() }; async function recur(node = vfs_1.vfs) { var _a, _b, _c; const { source } = node; const stats = !source ? undefined : (node.stats || await (0, promises_1.stat)(source).catch(() => undefined)); const isDir = !(0, vfs_1.nodeIsLink)(node) && (!source || ((_a = stats === null || stats === void 0 ? void 0 : stats.isDirectory()) !== null && _a !== void 0 ? _a : (source.endsWith('/') || ((_b = node.children) === null || _b === void 0 ? void 0 : _b.length) > 0))); const copyStats = stats ? lodash_1.default.pick(stats, ['size', 'birthtime', 'mtime']) : { size: source ? -1 : undefined }; if (copyStats.mtime && ((stats === null || stats === void 0 ? void 0 : stats.mtimeMs) - (stats === null || stats === void 0 ? void 0 : 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: Boolean((_c = node.children) === null || _c === void 0 ? void 0 : _c.find((0, vfs_1.isSameFilenameAs)('index.html'))) || isDir && source && await (0, promises_1.stat)((0, path_1.join)(source, 'index.html')).then(() => true, () => undefined) || undefined, name: (0, vfs_1.getNodeName)(node), type: isDir ? 'folder' : undefined, children: node.children && await Promise.all(node.children.map(async (child) => recur(await (0, vfs_1.applyParentToChild)(child, node)))) }; } }, async move_vfs({ from, parent }) { var _a; 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 ((_a = parentNode.children) === null || _a === void 0 ? void 0 : _a.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 || (parentNode.children = [])).push(fromNode); (0, vfs_1.saveVfs)(); return {}; }, async set_vfs({ uri, props }) { var _a; const n = 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'); const parent = await urlToNodeOriginal((0, path_1.dirname)(uri)); if ((_a = parent === null || parent === void 0 ? void 0 : parent.children) === null || _a === void 0 ? void 0 : _a.find(x => (0, vfs_1.getNodeName)(x) === props.name)) return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT, 'name already present'); } if (props.masks && typeof props.masks !== 'object') delete props.masks; Object.assign(n, pickProps(props, ALLOWED_KEYS)); simplifyName(n); (0, vfs_1.saveVfs)(); return n; }, async add_vfs({ parent, source, name, ...rest }) { var _a; if (!source && !name) return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'name or source required'); if (!(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 (!await (0, vfs_1.nodeIsDirectory)(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 isDir = source && await (0, misc_1.isDirectory)(source); if (source && isDir === undefined) return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'source not found'); const child = { source, name, ...pickProps(rest, ALLOWED_KEYS) }; 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 ((_a = parentNode.children) === null || _a === void 0 ? void 0 : _a.find((0, vfs_1.isSameFilenameAs)(name))) name = `${noExt} ${idx++}${ext}`; child.name = name; simplifyName(child); (parentNode.children || (parentNode.children = [])).unshift(child); (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)) + (isDir ? '/' : ''); return { name, link }; }, 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; if (uri === '/') return const_1.HTTP_NOT_ACCEPTABLE; const node = await urlToNodeOriginal(uri); if (!node) return const_1.HTTP_NOT_FOUND; const parent = (0, path_1.dirname)(uri); const parentNode = await urlToNodeOriginal(parent); if (!parentNode) // shouldn't happen return const_1.HTTP_SERVER_ERROR; const { children } = parentNode; if (!children) // shouldn't happen return const_1.HTTP_SERVER_ERROR; const idx = children.indexOf(node); children.splice(idx, 1); (0, vfs_1.saveVfs)(); return 0; // error code 0 is OK })) }; }, 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, 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, promises_1.stat)((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 (_a) { } // 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 h = status.http.listening ? status.http : status.https; 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 -WindowStyle Hidden -Command " $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); try { $res = Invoke-WebRequest -Uri '${url}/~/api/add_vfs' -Method POST -Headers @{ 'x-hfs-anti-csrf' = '1' } -ContentType 'application/json' -TimeoutSec 2 -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('Server is down', 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 {}; }, }; exports.default = apis; // 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 of keys) if (k in o) ret[k] = o[k] === null || o[k] === '' ? undefined : o[k]; return ret; } 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';