feat(sync): publish hostname and quiesce sync for backup
CI / Build & Test (push) Successful in 4m20s
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:
+5
-1
@@ -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
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user