feat(sync): publish hostname and quiesce sync for backup
CI / Build & Test (push) Successful in 4m20s

- getSyncStatus: fire-and-forget push device hostname when missing so
  linked peers see names after opening Sync page
- Backup/restore: close sync (corestore) before copy/extract so autopass
  data is consistent; backup includes autopass/ and autopass-identity.json
- Installer: preserve sync data on re-run (comments and messages)
- Docs: BACKUP.md sync-in-backup and pause behaviour; SYNC.md backups
  include sync data
This commit is contained in:
Raven Scott
2026-03-15 02:34:44 -04:00
parent 7df3975bb0
commit 7d88cdf841
7 changed files with 75 additions and 6 deletions
+5 -1
View File
@@ -9,12 +9,16 @@ Each backup archive contains:
| Path in archive | Source |
|-----------------|--------|
| `storage/state.json` | All virtual hosts, server tunnels (with labels), service tunnels, SSH connections (with `passwordB64`), RDP connections (with `passwordB64`), and settings |
| `storage/autopass/` | Sync data (Corestore/autopass) — included when device is linked; needed to restore sync on another machine |
| `storage/autopass-identity.json` | Sync identity (key material for the sync group) — included when linked |
| `certs/ca.key.pem` | Root CA private key |
| `certs/ca.cert.pem` | Root CA certificate |
| `certs/wildcard.hole.sail/` | Wildcard cert for the default `.hole.sail` TLD |
| `certs/wildcard.<parent>/` | One directory per custom TLD parent (e.g. `wildcard.my.internal/`, `wildcard.haha.wooo/`) |
The entire `holesail-browser-certs/` directory is archived — all wildcard cert directories are included, not just the default one. Backups do **not** include the binary itself, the log file, or other backup archives. To sync state across devices without copying certificates, see [Device sync](SYNC.md).
The entire `holesail-browser-certs/` directory is archived — all wildcard cert directories are included, not just the default one. The entire `holesail-browser-storage/` directory is archived except the `backups/` subdirectory (so `state.json`, `autopass/`, and `autopass-identity.json` are included when present). Backups do **not** include the binary itself, the log file, or other backup archives.
**Sync and backup:** Creating or restoring a backup temporarily **pauses sync** (closes the Corestore so nothing is writing to `autopass/`). This ensures a consistent copy of sync data. Backup will fail if sync cannot be paused. After the backup or restore finishes, the next time you open the Sync page (or any sync operation runs), sync reconnects automatically. To sync state across devices without copying certificates, see [Device sync](SYNC.md).
The default backup retention is **5 backups** (configurable in Settings via `backupRetention`).
+1 -1
View File
@@ -54,4 +54,4 @@ To stop syncing, remove or rename the `autopass` directory and `autopass-identit
## Backups and sync
Backups (Dashboard → Backups) do not include the sync identity (`autopass-identity.json`) or the `autopass/` directory. Use backups for local snapshots (state + certificates); use sync for keeping state in sync across devices (state only, no certs).
Backups (Dashboard → Backups) **include** sync data when linked: `autopass/` and `autopass-identity.json` are in the archive so you can restore a full snapshot (state, certs, and sync identity) on the same or another machine. Creating or restoring a backup temporarily pauses sync so the Corestore is not written to during the copy; sync reconnects automatically afterward. Use backups for full local or cross-machine snapshots; use sync for live replication across devices (state only, no certs). See [Backups](BACKUP.md).
+1
View File
@@ -22,6 +22,7 @@ const backupManager = require('../managers/backup-manager.js');
backupManager.setStoragePath(STORAGE_PATH);
backupManager.setCertsPath(certificateAuthority.getCertsDir());
const syncManager = require('../managers/sync-manager.js');
backupManager.setSyncManager(syncManager);
syncManager.setStoragePath(STORAGE_PATH);
syncManager.setDeps({
holesailManager,
+28
View File
@@ -22,6 +22,7 @@ const DEFAULT_RETENTION = 5;
let storageDir = null;
let certsDir = null;
let syncManagerRef = null;
function setStoragePath(dir) {
storageDir = dir;
@@ -31,6 +32,14 @@ function setCertsPath(dir) {
certsDir = dir;
}
/**
* Set the sync manager so backup can quiesce sync (close corestore) before copying.
* Backup must not run while sync is writing to autopass/ or the copy may be inconsistent.
*/
function setSyncManager(sm) {
syncManagerRef = sm;
}
function getBackupDir() {
if (!storageDir) throw new Error('Storage path not set');
return path.join(storageDir, BACKUP_DIR_NAME);
@@ -71,6 +80,15 @@ async function createBackup() {
if (!storageDir) return { ok: false, error: 'Storage path not set' };
if (!spawn) return { ok: false, error: 'child_process.spawn not available — cannot run tar' };
// Quiesce sync so corestore/autopass is not being written to; ensures a consistent copy of autopass/ and autopass-identity.json
if (syncManagerRef && typeof syncManagerRef.closeSyncForBackup === 'function') {
try {
await syncManagerRef.closeSyncForBackup();
} catch (e) {
return { ok: false, error: 'Sync could not be paused for backup: ' + ((e && e.message) || String(e)) };
}
}
const backupDir = ensureBackupDir();
const ts = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
const filename = 'holesail-backup-' + ts + '.tar.gz';
@@ -251,6 +269,15 @@ async function restoreBackup(filename) {
return { ok: false, error: 'Backup not found: ' + safe };
}
// Quiesce sync before extracting over storage so we do not overwrite corestore while it is in use
if (syncManagerRef && typeof syncManagerRef.closeSyncForBackup === 'function') {
try {
await syncManagerRef.closeSyncForBackup();
} catch (e) {
return { ok: false, error: 'Sync could not be paused for restore: ' + ((e && e.message) || String(e)) };
}
}
// List top-level entries in the archive to detect layout
let topEntries = [];
try {
@@ -386,6 +413,7 @@ module.exports = {
createBackup,
listBackups,
restoreBackup,
setSyncManager,
deleteBackup,
pruneOldBackups,
DEFAULT_RETENTION
+32
View File
@@ -99,6 +99,7 @@ function clearIdentity() {
}
async function closePass() {
initPromise = null;
if (pass) {
try {
await pass.close();
@@ -118,6 +119,14 @@ async function closePass() {
currentInvite = null;
}
/**
* Close sync (corestore/autopass) so backup can copy storage safely without active writes.
* Next getSyncStatus or other sync operation will re-open.
*/
async function closeSyncForBackup() {
await closePass();
}
async function ensureInitialized() {
if (initPromise) return initPromise;
initPromise = (async () => {
@@ -308,6 +317,28 @@ async function getSyncStatus() {
if (out.linkedDevices.length === 0 && out.deviceId) {
out.linkedDevices = [{ id: out.deviceId, isCurrent: true, name: out.deviceName }];
}
// Ensure our hostname is published so other devices see our name (fire-and-forget)
const myId = out.deviceId;
const currentName = out.deviceName;
if (myId && currentName) {
(async () => {
try {
const statePath = getStateFilePath();
if (!statePath || !fs.existsSync(statePath) || !pass) return;
const raw = fs.readFileSync(statePath, 'utf8');
const data = JSON.parse(raw);
const deviceNames = (data.deviceNames && typeof data.deviceNames === 'object') ? { ...data.deviceNames } : {};
if (deviceNames[myId] === currentName) return;
deviceNames[myId] = currentName;
data.deviceNames = deviceNames;
fs.writeFileSync(statePath, JSON.stringify(data, null, 2), 'utf8');
if (!pass) return;
const value = JSON.stringify(data);
await pass.add(STATE_KEY, value);
lastSyncedAt = Date.now();
} catch (_) {}
})();
}
} catch (e) {
out.deviceId = null;
out.syncGroupId = null;
@@ -410,5 +441,6 @@ module.exports = {
pairWithInvite,
onStateSaved,
applySyncedState,
closeSyncForBackup,
cleanup
};
+4 -2
View File
@@ -25,13 +25,15 @@ Write-Host "Stopping any running native host..."
Get-Process | Where-Object { $_.Path -like "*holesail-browser-host*" } | Stop-Process -Force -ErrorAction SilentlyContinue
# ── Preserve user data from any previous installation ─────────────────────────
# holesail-browser-storage includes state.json, backups/, and when linked:
# autopass/ and autopass-identity.json (sync data). Preserving the whole dir keeps sync identity across re-runs.
$StashDir = Join-Path $env:TEMP "holesail-browser-stash-$([System.Guid]::NewGuid().ToString('N'))"
$StashStorage = Join-Path $StashDir "holesail-browser-storage"
$StashCerts = Join-Path $StashDir "holesail-browser-certs"
$HadPrevious = $false
if (Test-Path $InstallDir) {
Write-Host "Preserving existing settings and certificates..."
Write-Host "Preserving existing settings, certificates, and sync data..."
New-Item -ItemType Directory -Path $StashDir -Force | Out-Null
$StorageSrc = Join-Path $InstallDir "holesail-browser-storage"
$CertsSrc = Join-Path $InstallDir "holesail-browser-certs"
@@ -79,7 +81,7 @@ Write-Host " Binary: $HostBin"
# ── Restore preserved user data ────────────────────────────────────────────────
if ($HadPrevious) {
Write-Host "Restoring settings and certificates..."
Write-Host "Restoring settings, certificates, and sync data..."
if (Test-Path $StashStorage) {
Copy-Item $StashStorage (Join-Path $InstallDir "holesail-browser-storage") -Recurse -Force
Write-Host " Restored: holesail-browser-storage"
+4 -2
View File
@@ -40,13 +40,15 @@ pkill -f "holesail-browser/native-host/index.mjs" 2>/dev/null || true
pkill -f "holesail-browser-host" 2>/dev/null || true
# ── Preserve user data from any previous installation ─────────────────────────
# holesail-browser-storage includes state.json, backups/, and when linked:
# autopass/ and autopass-identity.json (sync data). Preserving the whole dir keeps sync identity across re-runs.
STASH_DIR="$(mktemp -d)"
STASH_STORAGE="${STASH_DIR}/holesail-browser-storage"
STASH_CERTS="${STASH_DIR}/holesail-browser-certs"
HAD_PREVIOUS=false
if [[ -d "$INSTALL_DIR" ]]; then
echo "Preserving existing settings and certificates..."
echo "Preserving existing settings, certificates, and sync data..."
[[ -d "${INSTALL_DIR}/holesail-browser-storage" ]] && \
cp -a "${INSTALL_DIR}/holesail-browser-storage" "$STASH_STORAGE" && \
echo " Saved: holesail-browser-storage"
@@ -89,7 +91,7 @@ chmod +x "$HOST_BIN"
# ── Restore preserved user data ────────────────────────────────────────────────
if [[ "$HAD_PREVIOUS" == "true" ]]; then
echo "Restoring settings and certificates..."
echo "Restoring settings, certificates, and sync data..."
[[ -d "$STASH_STORAGE" ]] && \
cp -a "$STASH_STORAGE" "${INSTALL_DIR}/holesail-browser-storage" && \
echo " Restored: holesail-browser-storage"