68 lines
2.3 KiB
JavaScript
68 lines
2.3 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { nowIso, readJsonIfExists } = require('./util');
|
|
|
|
/**
|
|
* Advisory sidecar lock files for shared .sfgz guides on network folders.
|
|
* For `guide.sfgz` the lock is `guide.lock-sfgz` next to it. This is a
|
|
* coordination mechanism, not a security boundary (see docs/SECURITY.md).
|
|
*/
|
|
|
|
const STALE_AFTER_MS = 1000 * 60 * 60 * 8; // 8h: treat crashed holders as stale
|
|
|
|
function lockPathFor(archivePath) {
|
|
const dir = path.dirname(archivePath);
|
|
const base = path.basename(archivePath);
|
|
const stem = base.endsWith('.sfgz') ? base.slice(0, -'.sfgz'.length) : base;
|
|
return path.join(dir, `${stem}.lock-sfgz`);
|
|
}
|
|
|
|
function currentHolder() {
|
|
return { host: os.hostname(), user: os.userInfo().username, pid: process.pid };
|
|
}
|
|
|
|
function readLock(archivePath) {
|
|
return readJsonIfExists(lockPathFor(archivePath), null);
|
|
}
|
|
|
|
function sameHolder(a, b) {
|
|
return a && b && a.host === b.host && a.user === b.user && a.pid === b.pid;
|
|
}
|
|
|
|
function isStale(lock, now = Date.now()) {
|
|
const t = Date.parse(lock && lock.acquiredAt);
|
|
return !Number.isFinite(t) || now - t > STALE_AFTER_MS;
|
|
}
|
|
|
|
/**
|
|
* Try to take the lock. Returns { acquired: true, lock } or
|
|
* { acquired: false, conflict } when someone else holds a fresh lock.
|
|
* Pass force=true to steal (after the user confirmed in the conflict dialog).
|
|
*/
|
|
function acquireLock(archivePath, { force = false } = {}) {
|
|
const file = lockPathFor(archivePath);
|
|
const existing = readLock(archivePath);
|
|
const me = currentHolder();
|
|
if (existing && !sameHolder(existing, me) && !isStale(existing) && !force) {
|
|
return { acquired: false, conflict: existing };
|
|
}
|
|
const lock = { ...me, acquiredAt: nowIso() };
|
|
fs.writeFileSync(file, JSON.stringify(lock, null, 2));
|
|
return { acquired: true, lock };
|
|
}
|
|
|
|
/** Release only if we are the holder (or force). */
|
|
function releaseLock(archivePath, { force = false } = {}) {
|
|
const file = lockPathFor(archivePath);
|
|
const existing = readLock(archivePath);
|
|
if (!existing) return true;
|
|
if (!force && !sameHolder(existing, currentHolder())) return false;
|
|
fs.rmSync(file, { force: true });
|
|
return true;
|
|
}
|
|
|
|
module.exports = { lockPathFor, readLock, acquireLock, releaseLock, isStale, STALE_AFTER_MS };
|