'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 };