hfs
Version:
HTTP File Server
268 lines (267 loc) • 14.1 kB
JavaScript
// 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';
;