hfs
Version:
HTTP File Server
200 lines (199 loc) • 9.89 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.autoCheckUpdateResult = void 0;
exports.getVersions = getVersions;
exports.getUpdates = getUpdates;
exports.localUpdateAvailable = localUpdateAvailable;
exports.previousAvailable = previousAvailable;
exports.updateSupported = updateSupported;
exports.update = update;
const github_1 = require("./github");
const const_1 = require("./const");
const path_1 = require("path");
const child_process_1 = require("child_process");
const misc_1 = require("./misc");
const fs_1 = require("fs");
const plugins_1 = require("./plugins");
const promises_1 = require("fs/promises");
const open_1 = __importDefault(require("open"));
const config_1 = require("./config");
const util_os_1 = require("./util-os");
const first_1 = require("./first");
const persistence_1 = require("./persistence");
const lodash_1 = __importDefault(require("lodash"));
const argv_1 = require("./argv");
const updateToBeta = (0, config_1.defineConfig)('update_to_beta', false);
const autoCheckUpdate = (0, config_1.defineConfig)('auto_check_update', true);
const lastCheckUpdate = persistence_1.storedMap.singleSync('lastCheckUpdate', 0);
const AUTO_CHECK_EVERY = misc_1.DAY;
exports.autoCheckUpdateResult = persistence_1.storedMap.singleSync('autoCheckUpdateResult', undefined);
exports.autoCheckUpdateResult.ready().then(() => {
exports.autoCheckUpdateResult.set(v => {
if (!v)
return; // refresh isNewer, as currentVersion may have changed
v.isNewer = config_1.currentVersion.olderThan(v.tag_name);
return v;
});
});
setInterval((0, misc_1.debounceAsync)(async () => {
if (!autoCheckUpdate.get())
return;
if (Date.now() < lastCheckUpdate.get() + AUTO_CHECK_EVERY)
return;
console.log("checking for updates");
try {
const u = (await getUpdates(true))[0];
if (u)
console.log("new version available", u.name);
exports.autoCheckUpdateResult.set(u);
lastCheckUpdate.set(Date.now());
}
catch (_a) { }
}), misc_1.HOUR);
const ReleaseKeys = ['prerelease', 'tag_name', 'name', 'body', 'assets', 'isNewer', 'versionScalar'];
const ReleaseAssetKeys = ['name', 'browser_download_url'];
const curV = config_1.currentVersion.scalar;
function prepareRelease(r) {
const v = (0, config_1.versionToScalar)(r.name);
return Object.assign(lodash_1.default.pick(r, ReleaseKeys), {
versionScalar: v,
isNewer: v > curV, // make easy to know what's newer
assets: r.assets.map((a) => lodash_1.default.pick(a, ReleaseAssetKeys))
});
}
async function getVersions(interrupt) {
if (!updateToBeta.get() && !const_1.RUNNING_BETA)
return [];
const ret = [];
for await (const x of (0, github_1.apiGithubPaginated)(`repos/${const_1.HFS_REPO}/releases`)) {
if (x.name.endsWith('-ignore'))
continue;
const rel = prepareRelease(x);
if (rel.versionScalar === curV)
continue;
if (interrupt === null || interrupt === void 0 ? void 0 : interrupt(rel))
break; // avoid fetching too much data
ret.push(rel);
}
return lodash_1.default.sortBy(ret, x => -x.versionScalar);
}
async function getUpdates(strict = false) {
(0, github_1.getProjectInfo)(); // check for alerts
const stable = prepareRelease(await (0, github_1.getRepoInfo)(const_1.HFS_REPO + '/releases/latest'));
const res = await getVersions(r => r.versionScalar < stable.versionScalar); // we don't consider betas before stable
const ret = res.filter(x => x.prerelease && (strict ? x.isNewer : (x.versionScalar !== curV)));
if (stable.isNewer || const_1.RUNNING_BETA && !strict)
ret.push(stable);
return ret;
}
const LOCAL_UPDATE = 'hfs-update.zip'; // update from file takes precedence over net
const INSTALLED_FN = 'hfs-installed.zip';
const PREVIOUS_FN = 'hfs-previous.zip';
function localUpdateAvailable() {
return (0, misc_1.exists)(LOCAL_UPDATE);
}
function previousAvailable() {
return (0, misc_1.exists)(PREVIOUS_FN);
}
async function updateSupported() {
return process.env.DISABLE_UPDATE ? false : (argv_1.argv.forceupdate || const_1.IS_BINARY && !await util_os_1.RUNNING_AS_SERVICE);
}
async function update(tagOrUrl = '') {
if (!await updateSupported())
throw "only binary versions supports automatic update for now";
let url = tagOrUrl.includes('://') && tagOrUrl;
if (tagOrUrl === const_1.PREVIOUS_TAG)
await (0, promises_1.rename)(PREVIOUS_FN, LOCAL_UPDATE);
else if (tagOrUrl ? !url : !await localUpdateAvailable()) {
if (/^\d/.test(tagOrUrl)) // work even if the tag is passed without the initial 'v' (useful for console commands)
tagOrUrl = 'v' + tagOrUrl;
const update = !tagOrUrl ? (await getUpdates(true))[0]
: await (0, github_1.getRepoInfo)(const_1.HFS_REPO + '/releases/tags/' + tagOrUrl).catch(e => {
if (e.message === '404')
console.error("version not found");
else
throw e;
});
if (!update)
throw "update not found";
const plat = '-' + (0, misc_1.xlate)(process.platform, { win32: 'windows', darwin: 'mac' });
const assetSearch = `${plat}-${process.arch}`;
const legacyAssetSearch = `${plat}${(0, misc_1.prefix)('-', (0, misc_1.xlate)(process.arch, { x64: '', arm64: 'arm' }))}.zip`; // legacy pre-0.53.0-rc16
const asset = update.assets.find((x) => x.name.includes(assetSearch) && x.name.endsWith('.zip'))
|| update.assets.find((x) => x.name.endsWith(legacyAssetSearch));
if (!asset)
throw `asset not found: ${assetSearch}`;
url = asset.browser_download_url;
}
if (url) {
console.log("downloading", url);
const { body } = await (0, misc_1.httpWithBody)(url);
if (!body)
throw "download failed for " + url;
await (0, promises_1.writeFile)(LOCAL_UPDATE, body);
}
const bin = process.execPath;
const binPath = (0, path_1.dirname)(bin);
const binFile = 'hfs' + (const_1.IS_WINDOWS ? '.exe' : ''); // currently running bin could have been renamed
let newBinFile = binFile;
do {
newBinFile = 'new-' + newBinFile;
} while ((0, fs_1.existsSync)((0, path_1.join)(binPath, newBinFile)));
plugins_1.pluginsWatcher.pause();
try {
await (0, misc_1.unzip)((0, fs_1.createReadStream)(LOCAL_UPDATE), path => (0, path_1.join)(binPath, path === binFile ? newBinFile : path));
const newBin = (0, path_1.join)(binPath, newBinFile);
if (!const_1.IS_WINDOWS) {
const { mode } = await (0, promises_1.stat)(bin);
await (0, promises_1.chmod)(newBin, mode).catch(console.error);
}
await (0, promises_1.rename)(INSTALLED_FN, PREVIOUS_FN).catch(console.warn);
await (0, promises_1.rename)(LOCAL_UPDATE, INSTALLED_FN).catch(console.warn);
(0, first_1.onProcessExit)(() => {
const oldBinFile = 'old-' + binFile;
const oldBin = (0, path_1.join)(binPath, oldBinFile);
try {
(0, fs_1.unlinkSync)(oldBin);
}
catch (_a) { }
(0, fs_1.renameSync)(bin, oldBin);
console.log("launching new version in background", newBinFile);
launch(newBin, ['--updating', binFile, '--cwd .'], { sync: true }); // sync necessary to work on Mac by double-click
});
console.log("quitting");
setTimeout(() => process.exit()); // give time to return (and caller to complete, eg: rest api to reply)
}
catch (e) {
plugins_1.pluginsWatcher.unpause();
throw (e === null || e === void 0 ? void 0 : e.message) || String(e);
}
}
function launch(cmd, pars = [], options) {
return ((options === null || options === void 0 ? void 0 : options.sync) ? child_process_1.spawnSync : child_process_1.spawn)((0, util_os_1.cmdEscape)(cmd), pars, { detached: true, shell: true, stdio: [0, 1, 2], ...options });
}
if (argv_1.argv.updating) { // we were launched with a temporary name, restore original name to avoid breaking references
const bin = process.execPath;
const dest = (0, path_1.join)((0, path_1.dirname)(bin), argv_1.argv.updating);
(0, fs_1.renameSync)(bin, dest);
// have to relaunch with new name, or otherwise next update will fail with EBUSY on hfs.exe
console.log("renamed binary file to", argv_1.argv.updating, "and restarting");
// be sure to test launching both double-clicking and in a terminal
if (const_1.IS_WINDOWS) // this method on Mac works only once, and without console
(0, first_1.onProcessExit)(() => launch(dest, ['--updated', '--cwd .'])); // launch+sync here would cause old process to stay open, locking ports
else {
/* open() is the only consistent way that i could find working on Mac that preserved console input/output over relaunching,
* but I couldn't find a way to pass parameters, at least on Linux. The workaround I'm using is to write them to a temp file, that's read and deleted at restart.
* For the record, on mac you can: write "./hfs arg1 arg2" to /tmp/tmp.sh with 0o700, and then spawn "open -a Terminal /tmp/tmp.sh"
*/
try {
(0, fs_1.writeFileSync)(const_1.ARGS_FILE, JSON.stringify(['--updated', '--cwd', process.cwd().replaceAll(' ', '\\ ')]));
}
catch (_a) { }
void (0, open_1.default)(dest);
}
process.exit();
}
;