/** * State persistence for the native host. * Loads and saves all persistent data (tunnels, settings, connections) to state.json. * Handles migration from the legacy holesail-persist.json format. */ const path = require('bare-path'); const fs = require('bare-fs'); const STATE_FILENAME = 'state.json'; const LEGACY_PERSIST_FILENAME = 'holesail-persist.json'; const SETTINGS_DEFAULTS = { proxyPort: 8443, connectProxyPort: 8442, readyTimeoutMs: 30000, notifyOnDisconnect: true, notifyOnTunnelError: true, debug: false, disableOnFileUrls: false, backupRetention: 5, backupIntervalHours: 0, tunnelAutoReconnect: true, latencyPingEnabled: false, latencyPingIntervalMs: 5000, dashboardRefreshIntervalMs: 2000 }; const DEBUG = process.env.HOLESAIL_DEBUG === '1' || process.env.HOLESAIL_DEBUG === 'true'; function debugLog(...args) { if (!DEBUG) return; const msg = '[holesail-manager:debug] ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '); if (process.stderr) process.stderr.write(msg + '\n'); } let stateFilePath = null; function setStoragePath(baseDir) { if (baseDir && typeof baseDir === 'string') { stateFilePath = path.join(baseDir, STATE_FILENAME); } } function getStatePath() { if (stateFilePath) return stateFilePath; const base = process.env.HOLESAIL_BROWSER_STORAGE || path.join(__dirname, '..', 'holesail-browser-storage'); stateFilePath = path.join(base, STATE_FILENAME); return stateFilePath; } /** Create the storage directory if it does not exist (e.g. fresh install on Windows). */ function ensureStorageDir() { const dir = path.dirname(getStatePath()); try { fs.mkdirSync(dir, { recursive: true }); } catch (e) { if (process.stderr) process.stderr.write('[holesail-manager] could not create storage dir: ' + e.message + '\n'); } } function getLegacyPersistPath() { const dir = path.dirname(getStatePath()); return path.join(dir, LEGACY_PERSIST_FILENAME); } function buildDefaultState() { return { settings: { ...SETTINGS_DEFAULTS }, servers: [], virtualHosts: [], serviceTunnels: [], sshConnections: [], rdpConnections: [], nextServerId: 0, nextServiceTunnelId: 0 }; } function saveStateSync(data) { const file = getStatePath(); const json = JSON.stringify(data, null, 2); try { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, json, 'utf8'); debugLog('state saved path=', file); } catch (e) { if (process.stderr) process.stderr.write('[holesail-manager] state save failed: ' + e.message + ' (path: ' + file + ')\n'); } } function loadState() { const file = getStatePath(); try { const raw = fs.readFileSync(file, 'utf8'); const trimmed = raw && typeof raw === 'string' ? raw.trim() : ''; if (!trimmed) return buildDefaultState(); const data = JSON.parse(raw); if (!data || typeof data !== 'object') return buildDefaultState(); const out = { settings: (data.settings && typeof data.settings === 'object') ? { ...SETTINGS_DEFAULTS, ...data.settings } : { ...SETTINGS_DEFAULTS }, servers: Array.isArray(data.servers) ? data.servers : [], virtualHosts: Array.isArray(data.virtualHosts) ? data.virtualHosts : [], serviceTunnels: Array.isArray(data.serviceTunnels) ? data.serviceTunnels : [], sshConnections: Array.isArray(data.sshConnections) ? data.sshConnections : [], rdpConnections: Array.isArray(data.rdpConnections) ? data.rdpConnections : [], nextServerId: typeof data.nextServerId === 'number' ? data.nextServerId : 0, nextServiceTunnelId: typeof data.nextServiceTunnelId === 'number' ? data.nextServiceTunnelId : 0 }; debugLog('state loaded path=', file, 'servers=', out.servers.length, 'vhosts=', out.virtualHosts.length); if (process.stderr && (out.servers.length || out.virtualHosts.length)) { process.stderr.write('[holesail-manager] state loaded from ' + file + ' (servers=' + out.servers.length + ' vhosts=' + out.virtualHosts.length + ')\n'); } return out; } catch (e) { if (e && e.code !== 'ENOENT') { if (process.stderr) process.stderr.write('[holesail-manager] state load failed: ' + e.message + '\n'); return buildDefaultState(); } } // state.json not found — try migrating from legacy holesail-persist.json const legacyFile = getLegacyPersistPath(); try { const raw = fs.readFileSync(legacyFile, 'utf8'); const trimmed = raw && typeof raw === 'string' ? raw.trim() : ''; if (!trimmed) return buildDefaultState(); const data = JSON.parse(raw); if (process.stderr) process.stderr.write('[holesail-manager] migrating from ' + legacyFile + ' to ' + file + '\n'); const out = { settings: { ...SETTINGS_DEFAULTS }, servers: Array.isArray(data.servers) ? data.servers : [], virtualHosts: Array.isArray(data.virtualHosts) ? data.virtualHosts : [], serviceTunnels: Array.isArray(data.serviceTunnels) ? data.serviceTunnels : [], sshConnections: Array.isArray(data.sshConnections) ? data.sshConnections : [], rdpConnections: Array.isArray(data.rdpConnections) ? data.rdpConnections : [], nextServerId: typeof data.nextServerId === 'number' ? data.nextServerId : 0, nextServiceTunnelId: typeof data.nextServiceTunnelId === 'number' ? data.nextServiceTunnelId : 0 }; try { fs.unlinkSync(legacyFile); } catch (_) {} if (process.stderr) process.stderr.write('[holesail-manager] migration complete, legacy file removed\n'); return out; } catch (e) { if (e && e.code !== 'ENOENT') { if (process.stderr) process.stderr.write('[holesail-manager] legacy persist load failed: ' + e.message + '\n'); } } return buildDefaultState(); } module.exports = { SETTINGS_DEFAULTS, setStoragePath, loadState, saveStateSync, buildDefaultState, ensureStorageDir };