R-729 + R-545: the household can remove its own off-site target („Távoli mentési cél törlése")
POST /backup/offbox/clear (reveal-then-confirm) forgets the target and deletes ssh_key + known_hosts; nothing on the target is touched. repo_password is kept whenever anything could depend on it (hub sealed package — the R-241 rule; escrowed; a successful run; snapshots) and deleted only otherwise. Refused for the Felhom tier (rclone-pinned), while the single-flight is held, and while an abandonment countdown runs. i18n parity fixtures of backups_remote gain the additive block only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user