package backup import ( "errors" "fmt" "os" ) // CLEARING AN OFF-SITE TARGET — R-729 / R-545 (the household's „Távoli mentési cél törlése" press). // // Before this, a target once saved could be edited or switched off but never removed: the page // refuses an empty address, and the only other route (`/backup/offbox/reset`) means "start a new // remote backup, set the old history aside", and only for an ORPHANED store. A household that tried // its own NAS and gave up kept the host, user, path, SSH key and known-host line for ever. // // WHAT THE PRESS DOES: forgets the target (settings) and deletes the transport secrets (`ssh_key`, // `known_hosts`) from `data/offbox/`. It NEVER touches the repository on the target — not one remote // command runs (R-729: "never the repository"). // // WHAT IT DOES WITH THE REPOSITORY PASSWORD — the one secret that is not transport: // it is KEPT whenever anything could depend on it, and deleted only when nothing can: // - the hub holds a sealed recovery package for this box (the R-241 rule: a box without a password // and with a held package refuses to mint, so deleting the key here would strand the next target // in the „awaiting recovery key" holding state — and the history the package protects would lose // its on-box key); // - the escrow was confirmed (`escrowed`) — the hub's package seals exactly this key; // - the target ever completed a run or holds snapshots — history exists on the target that only // this key opens. // Keeping it is harmless: a later target reuses it (WriteOffboxSecrets never re-mints over an // existing file), and the escrow then matches by hash with nothing to redo. // // WHAT IT REFUSES (each leaves every file and setting untouched): // - the hub-provisioned Felhom tier (Transport rclone-pinned) — that target is Felhom's to remove, // not the page's; the hub would re-apply it anyway; // - while a backup/restore/check holds the single-flight — a run reading the key file mid-delete; // - while an abandonment countdown is running or awaiting the hub (R-241 / decision 74) — the // countdown's state lives on the target record, and clearing it would strand the scheduled // deletion of the set-aside history with nobody left to run or cancel it. // // Pinned by internal/backup/offbox_clear_r729_test.go. // Refusal sentinels — the handler maps each to its own household sentence. var ( ErrOffboxClearNoTarget = errors.New("offbox clear: no off-site target is configured") ErrOffboxClearHubTier = errors.New("offbox clear: the Felhom-provided off-site tier cannot be removed from the box") ErrOffboxClearBusy = errors.New("offbox clear: a backup operation is running") ErrOffboxClearAbandonOn = errors.New("offbox clear: a set-aside history deletion is scheduled") ) // OffboxClearResult says what the press did with the one secret it may keep. type OffboxClearResult struct { // KeptRepoPassword is true when the repository password stayed on disk (see the header for why). KeptRepoPassword bool } // offboxRepoPasswordNeeded reports whether anything could still depend on the on-box repository // password. Fail-safe direction: every input that MIGHT mean "something depends on it" keeps it. func (m *Manager) offboxRepoPasswordNeeded(escrowState, lastSuccess string, snapshots int) bool { return m.sealedPackageHeld() || escrowState == "escrowed" || lastSuccess != "" || snapshots > 0 } // ClearOffboxTarget forgets the household's off-site target. See the file header. func (m *Manager) ClearOffboxTarget() (OffboxClearResult, error) { var res OffboxClearResult t := m.settings.GetOffboxTarget() if t == nil { return res, ErrOffboxClearNoTarget } if t.Pinned() { return res, ErrOffboxClearHubTier } if ab := m.AbandonStatus(); ab.Active || ab.PurgeRequested || ab.HubPending { return res, ErrOffboxClearAbandonOn } // Hold the single-flight for the whole clear, so no run can start between the check and the delete. if err := m.acquireRunning(); err != nil { return res, ErrOffboxClearBusy } defer m.releaseRunning() res.KeptRepoPassword = m.offboxRepoPasswordNeeded(t.EscrowState, t.LastSuccess, t.SnapshotCount) // Files FIRST, settings LAST: a failure part-way leaves a target that is still listed (and no // longer runnable — OffboxConfigured needs the key file), so the press can simply be repeated. // The reverse order would leave secrets on disk with no target left to clear them through. files := []string{m.offboxKeyPath(), m.offboxKnownHosts()} if !res.KeptRepoPassword { files = append(files, m.offboxPwPath()) } for _, f := range files { if err := os.Remove(f); err != nil && !os.IsNotExist(err) { return res, fmt.Errorf("offbox clear: remove %s: %w", f, err) } } if err := m.settings.SetOffboxTarget(nil); err != nil { return res, fmt.Errorf("offbox clear: save settings: %w", err) } m.logger.Printf("[INFO] [offbox] off-site target %s@%s:%s CLEARED by the household (transport secrets deleted; repository password kept=%v; nothing on the target was touched)", t.User, t.Host, t.RepoPath, res.KeptRepoPassword) return res, nil }