hfs
Version:
HTTP File Server
153 lines (152 loc) • 8.68 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
Object.defineProperty(exports, "__esModule", { value: true });
exports.get_file_list = void 0;
exports.paramsToFilter = paramsToFilter;
const vfs_1 = require("./vfs");
const apiMiddleware_1 = require("./apiMiddleware");
const promises_1 = require("fs/promises");
const plugins_1 = require("./plugins");
const misc_1 = require("./misc");
const const_1 = require("./const");
const comments_1 = require("./comments");
const path_1 = require("path");
const connections_1 = require("./connections");
const adminApis_1 = require("./adminApis");
const upload_1 = require("./upload");
const SendList_1 = require("./SendList");
function paramsToFilter({ search, wild, searchComment, fileMask }) {
search = String(search || '').toLocaleLowerCase();
searchComment = String(searchComment || '').toLocaleLowerCase();
return {
depth: search || searchComment ? Infinity : 0,
filterName: search > '' && (wild === 'no' ? (s) => s.includes(search) : (0, misc_1.pattern2filter)(search)),
fileMask: fileMask > '' && (0, misc_1.pattern2filter)(fileMask),
filterComment: searchComment > '' && (wild === 'no' ? (s) => s.includes(searchComment) : (0, misc_1.pattern2filter)(searchComment))
};
}
const get_file_list = async ({ uri = '/', offset, limit, c, onlyFolders, onlyFiles, admin, ...rest }, ctx) => {
var _a;
const node = await (0, vfs_1.urlToNode)(uri, ctx);
const list = ctx.get('accept') === 'text/event-stream' ? new SendList_1.SendListReadable() : undefined;
if (!node)
return fail(const_1.HTTP_NOT_FOUND);
admin && (admin = (0, adminApis_1.ctxAdminAccess)(ctx)); // validate 'admin' flag
if (await (0, vfs_1.hasDefaultFile)(node, ctx) || !await (0, vfs_1.nodeIsDirectory)(node)) // in case of files without permission, we are provided with the frontend, and the location is the file itself
// so, we first check if you have a permission problem, to tell frontend to show login, otherwise we fall back to method_not_allowed, as it's proper for files.
return fail(!admin && (0, vfs_1.statusCodeForMissingPerm)(node, 'can_read', ctx) ? undefined : const_1.HTTP_METHOD_NOT_ALLOWED);
if (!admin && (0, vfs_1.statusCodeForMissingPerm)(node, 'can_list', ctx))
return fail();
offset = Number(offset);
limit = Number(limit);
const { filterName, filterComment, fileMask, depth } = paramsToFilter(rest);
const walker = (0, vfs_1.walkNode)(node, { ctx: admin ? undefined : ctx, onlyFolders, onlyFiles, depth });
const onDirEntryHandlers = (0, plugins_1.mapPlugins)(plug => plug.onDirEntry);
const can_upload = admin || (0, vfs_1.hasPermission)(node, 'can_upload', ctx);
const can_delete = admin || (0, vfs_1.hasPermission)(node, 'can_delete', ctx);
const fakeChild = await (0, vfs_1.applyParentToChild)({ source: 'dummy-file', original: undefined }, node); // used to check permission; simple but can produce false results; 'original' to simulate a non-vfs node
const can_delete_children = admin || (0, vfs_1.hasPermission)(fakeChild, 'can_delete', ctx);
const can_archive = admin || (0, vfs_1.hasPermission)(node, 'can_archive', ctx);
const can_comment = can_upload && (0, comments_1.areCommentsEnabled)();
const can_overwrite = can_upload && (can_delete || !upload_1.dontOverwriteUploading.get());
const comment = (_a = node.comment) !== null && _a !== void 0 ? _a : await (0, comments_1.getCommentFor)(node.source);
const props = { can_archive, can_upload, can_delete, can_delete_children, can_overwrite, can_comment, comment, accept: node.accept, icon: getNodeIcon(node) };
ctx.state.browsing = uri.replace(/\/{2,}/g, '/');
(0, connections_1.updateConnectionForCtx)(ctx);
if (!list)
return { ...props, list: await (0, misc_1.asyncGeneratorToArray)(produceEntries()) };
setTimeout(async () => {
list.props(props);
for await (const entry of produceEntries())
list.add(entry);
list.close();
});
return list;
function fail(code = ctx.status) {
if (!list)
return new apiMiddleware_1.ApiError(code);
list.error(code, true);
return list;
}
async function* produceEntries() {
for await (const sub of walker) {
let name = (0, vfs_1.getNodeName)(sub);
name = (0, path_1.basename)(name) || name; // on windows, basename('C:') === ''
if (filterName && !filterName(name) || fileMask && !await (0, vfs_1.nodeIsDirectory)(sub) && !fileMask(name)
|| filterComment && !filterComment(await (0, comments_1.getCommentFor)(sub.source) || ''))
continue;
const entry = await nodeToDirEntry(ctx, sub);
if (!entry)
continue;
const cbParams = { entry, ctx, listUri: uri, node: sub };
try {
const res = await Promise.all(onDirEntryHandlers.map(cb => cb(cbParams)));
if (res.some(x => x === false))
continue;
}
catch (e) {
console.log("a plugin with onDirEntry is causing problems:", e);
}
if (offset) {
--offset;
continue;
}
if (c === 'no' && entry.c) // allow excluding c for smaller payload
entry.c = undefined;
yield entry;
if (limit && !--limit)
break;
}
}
function getNodeIcon(node) {
var _a;
return ((_a = node.icon) === null || _a === void 0 ? void 0 : _a.includes('.')) || node.icon; // true = specific for this file, otherwise is a SYS_ICONS
}
async function nodeToDirEntry(ctx, node) {
var _a;
const { source, url } = node;
const name = (0, vfs_1.getNodeName)(node);
if (url)
return name ? { n: name, url, target: node.target } : null;
const isFolder = await (0, vfs_1.nodeIsDirectory)(node);
try {
const st = source ? node.stats || await (0, promises_1.stat)(source).catch(e => {
var _a;
if (!isFolder || !((_a = node.children) === null || _a === void 0 ? void 0 : _a.length)) // folders with virtual children, keep them
throw e;
}) : undefined;
// permissions of entries are sent as a difference with permissions of parent
const pl = node.can_list === misc_1.WHO_NO_ONE ? 'l'
: !(0, vfs_1.hasPermission)(node, 'can_list', ctx) ? 'L'
: '';
// no download here, but maybe inside?
const pr = node.can_read === misc_1.WHO_NO_ONE && !(isFolder && filesInsideCould()) ? 'r'
: !(0, vfs_1.hasPermission)(node, 'can_read', ctx) ? 'R'
: '';
// for delete, the diff is based on can_delete_children instead of can_delete, because it will produce fewer data
const pd = Boolean(can_delete_children) === (0, vfs_1.hasPermission)(node, 'can_delete', ctx) ? '' : can_delete_children ? 'd' : 'D';
const pa = Boolean(can_archive) === (0, vfs_1.hasPermission)(node, 'can_archive', ctx) ? '' : can_archive ? 'a' : 'A';
const pu = !isFolder || Boolean(can_upload) === (0, vfs_1.hasPermission)(node, 'can_upload', ctx) ? '' : can_upload ? 'u' : 'U';
return {
n: name + (isFolder ? '/' : ''),
c: st === null || st === void 0 ? void 0 : st.birthtime,
m: !st || Math.abs(st.mtimeMs - st.birthtimeMs) < 1000 ? undefined : st.mtime,
s: isFolder ? undefined : st === null || st === void 0 ? void 0 : st.size,
p: (pr + pl + pd + pa + pu) || undefined,
order: node.order,
comment: (_a = node.comment) !== null && _a !== void 0 ? _a : await (0, comments_1.getCommentFor)(source),
icon: getNodeIcon(node),
web: await (0, vfs_1.hasDefaultFile)(node, ctx) ? true : undefined,
};
}
catch (_b) {
return null;
}
function filesInsideCould(n = node) {
var _a;
return (0, vfs_1.masksCouldGivePermission)(n.masks, 'can_read')
|| ((_a = n.children) === null || _a === void 0 ? void 0 : _a.some(c => c.can_read || filesInsideCould(c))); // we count on the boolean-compliant nature of the permission type here
}
}
};
exports.get_file_list = get_file_list;
;