hfs
Version:
HTTP File Server
169 lines (168 loc) • 6.37 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.parseFileCache = void 0;
exports.isDirectory = isDirectory;
exports.readFileBusy = readFileBusy;
exports.watchDir = watchDir;
exports.dirTraversal = dirTraversal;
exports.adjustStaticPathForGlob = adjustStaticPathForGlob;
exports.unzip = unzip;
exports.prepareFolder = prepareFolder;
exports.createFileWithPath = createFileWithPath;
exports.safeWriteStream = safeWriteStream;
exports.isValidFileName = isValidFileName;
exports.exists = exists;
exports.parseFile = parseFile;
exports.parseFileContent = parseFileContent;
const promises_1 = require("fs/promises");
const misc_1 = require("./misc");
const fs_1 = require("fs");
const path_1 = require("path");
const fast_glob_1 = __importDefault(require("fast-glob"));
const const_1 = require("./const");
const stream_1 = require("stream");
// @ts-ignore
const unzip_stream_1 = __importDefault(require("unzip-stream"));
async function isDirectory(path) {
try {
return (await (0, promises_1.stat)(path)).isDirectory();
}
catch (_a) { }
}
async function readFileBusy(path) {
return (0, promises_1.readFile)(path, 'utf8').catch(e => {
if ((e === null || e === void 0 ? void 0 : e.code) !== 'EBUSY')
throw e;
console.debug('busy');
return (0, misc_1.wait)(100).then(() => readFileBusy(path));
});
}
function watchDir(dir, cb, atStart = false) {
let watcher;
let paused = false;
try {
watcher = (0, fs_1.watch)(dir, controlledCb);
}
catch (_a) {
// failing watching the content of the dir, we try to monitor its parent, but filtering events only for our target dir
const base = (0, path_1.basename)(dir);
try {
watcher = (0, fs_1.watch)((0, path_1.dirname)(dir), (event, name) => {
if (name !== base)
return;
try {
watcher.close(); // if we succeed, we give up the parent watching
watcher = (0, fs_1.watch)(dir, controlledCb); // attempt at passing to a more specific watching
}
catch (_a) { }
controlledCb();
});
}
catch (e) {
console.debug(String(e));
}
}
if (atStart)
controlledCb();
return {
working() { return Boolean(watcher); },
stop() { watcher === null || watcher === void 0 ? void 0 : watcher.close(); },
pause() { paused = true; },
unpause() { paused = false; },
};
function controlledCb() {
if (!paused)
cb();
}
}
function dirTraversal(s) {
return s && /(^|[/\\])\.\.($|[/\\])/.test(s);
}
// apply this to paths that may contain \ as separator (not supported by fast-glob) and other special chars to be escaped (parenthesis)
function adjustStaticPathForGlob(path) {
return fast_glob_1.default.escapePath(path.replace(/\\/g, '/'));
}
async function unzip(stream, cb) {
let pending = Promise.resolve();
return new Promise((resolve, reject) => stream.pipe(unzip_stream_1.default.Parse())
.on('end', () => pending.then(resolve))
.on('error', reject)
.on('entry', (entry) => pending = pending.then(async () => {
const { path, type } = entry;
const dest = await (0, misc_1.try_)(() => cb(path), e => console.warn(String(e)));
if (!dest || type !== 'File')
return entry.autodrain();
console.debug('unzip', dest);
const thisFile = entry.pipe(await safeWriteStream(dest));
await (0, stream_1.once)(thisFile, 'finish');
})));
}
async function prepareFolder(path, dirnameIt = true) {
if (dirnameIt)
path = (0, path_1.dirname)(path);
if ((0, misc_1.isWindowsDrive)(path))
return;
try {
await (0, promises_1.mkdir)(path, { recursive: true });
return true;
}
catch (_a) {
return false;
}
}
function createFileWithPath(path, options) {
const folder = (0, path_1.dirname)(path);
if (!(0, misc_1.isWindowsDrive)(folder)) // can't use prepareFolder because it's async
try {
(0, fs_1.mkdirSync)(folder, { recursive: true });
}
catch (_a) {
return;
}
return (0, fs_1.createWriteStream)(path, options);
}
async function safeWriteStream(path, options) {
await prepareFolder(path);
return new Promise((resolve, reject) => {
const first = (0, fs_1.createWriteStream)(path, options)
.on('open', () => resolve(first))
.on('error', (e) => {
if (!const_1.IS_WINDOWS || e.code !== 'EPERM') // Windows throws EPERM for hidden files with flags 'w' and 'a'
return reject(e);
if (typeof options === 'string')
options = { encoding: options };
else
options || (options = {});
if (options.flags && options.flags !== 'w') // we only handle the 'w' case
return reject(e);
options.flags = 'r+';
const second = (0, fs_1.createWriteStream)(path, options)
.on('open', fd => (0, fs_1.ftruncate)(fd, 0, () => resolve(second)))
.on('error', reject);
});
});
}
function isValidFileName(name) {
return !(const_1.IS_WINDOWS ? /[/:"*?<>|\\]/ : /\//).test(name) && !dirTraversal(name);
}
function exists(path) {
return (0, promises_1.access)(path).then(() => true, () => false);
}
// parse a file, caching unless timestamp has changed
exports.parseFileCache = new Map();
async function parseFile(path, parse) {
const { mtime: ts } = await (0, promises_1.stat)(path);
const cached = exports.parseFileCache.get(path);
if (cached && Number(ts) === Number(cached.ts))
return cached.parsed;
const parsed = parse(path);
exports.parseFileCache.set(path, { ts, parsed });
return parsed;
}
async function parseFileContent(path, parse) {
return parseFile(path, () => (0, promises_1.readFile)(path).then(parse));
}
;