v0.289.1: strip the provider rclone notice; an unreadable snapshot count is never a measured zero (found live on demo-felhom)
gates / gates (push) Successful in 29s
gates / gates (push) Successful in 29s
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:
@@ -1,6 +1,7 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
@@ -425,10 +426,32 @@ func (m *Manager) offboxSize() func(string) int64 {
|
||||
}
|
||||
|
||||
func (m *Manager) runner() offboxRunner {
|
||||
r := defaultOffboxRunner
|
||||
if m.offboxRunner != nil {
|
||||
return m.offboxRunner
|
||||
r = m.offboxRunner
|
||||
}
|
||||
return defaultOffboxRunner
|
||||
return func(ctx context.Context, env []string, args ...string) ([]byte, error) {
|
||||
out, err := r(ctx, env, args...)
|
||||
return stripRcloneNotice(out), err
|
||||
}
|
||||
}
|
||||
|
||||
// rcloneNoticeRe is the ONE line the provider's rclone prints on every connection of the pinned
|
||||
// transport (v0.289.0) — restic forwards it with an "rclone: " prefix into the combined output:
|
||||
//
|
||||
// rclone: 2026/10/03 15:05:42 NOTICE: Config file "/home/.config/rclone/rclone.conf" not found - using defaults
|
||||
//
|
||||
// MEASURED LIVE on demo-felhom 2026-10-03: left in, every `--json` parse failed ("invalid character
|
||||
// 'r'"), so the box recorded 0 snapshots AS MEASURED (StatsKnown) over a store holding 12 — the exact
|
||||
// shape R-431's detector reads as a mass deletion. Only this exact notice is removed; any other rclone
|
||||
// line (an error) stays in the output. Pinned by TestStripRcloneNotice.
|
||||
var rcloneNoticeRe = regexp.MustCompile(`(?m)^rclone: \d{4}/\d\d/\d\d \d\d:\d\d:\d\d NOTICE: Config file "[^"]*rclone\.conf" not found - using defaults\r?\n?`)
|
||||
|
||||
func stripRcloneNotice(out []byte) []byte {
|
||||
if len(out) == 0 || !bytes.Contains(out, []byte("rclone: ")) {
|
||||
return out
|
||||
}
|
||||
return rcloneNoticeRe.ReplaceAll(out, nil)
|
||||
}
|
||||
|
||||
func (m *Manager) offboxDir() string { return filepath.Join(m.cfg.Paths.DataDir, "offbox") }
|
||||
@@ -1034,9 +1057,9 @@ func (m *Manager) runOffboxBackup(ctx context.Context, withProgress bool) error
|
||||
}
|
||||
|
||||
dur := time.Since(start)
|
||||
snapshots := 0
|
||||
snapshots, countKnown := 0, false
|
||||
if runErr == nil {
|
||||
snapshots = m.offboxRecordStats(ctx, base, env)
|
||||
snapshots, countKnown = m.offboxRecordStats(ctx, base, env)
|
||||
}
|
||||
// THE LANGUAGE IS RESOLVED HERE, OUTSIDE THE CALLBACK, AND IT IS NOT A STYLE CHOICE.
|
||||
//
|
||||
@@ -1114,8 +1137,15 @@ func (m *Manager) runOffboxBackup(ctx context.Context, withProgress bool) error
|
||||
o.LastStatus = "ok"
|
||||
}
|
||||
o.LastError = ""
|
||||
o.SnapshotCount = snapshots
|
||||
o.StatsKnown = true // R-225: measured, even if the answer is zero
|
||||
if countKnown {
|
||||
o.SnapshotCount = snapshots
|
||||
o.StatsKnown = true // R-225: measured, even if the answer is zero
|
||||
} else {
|
||||
// v0.289.0: the listing FAILED — that is not a measurement of zero (R-331). Keep the
|
||||
// last measured count and say it is not known now; a reported 0 here reads as a mass
|
||||
// deletion to the hub (R-431). Pinned by TestRunOffbox_UnreadableCountIsNotZero.
|
||||
o.StatsKnown = false
|
||||
}
|
||||
o.EnlargedBlocked = blockedNames // replace each run (sorted); empty slice clears it
|
||||
var warns []string
|
||||
warnKind := ""
|
||||
@@ -1832,18 +1862,20 @@ func (m *Manager) offboxPruneOnly(ctx context.Context, base, env []string) {
|
||||
}
|
||||
|
||||
// offboxRecordStats reads the snapshot count (best-effort) for the UI; also fills repo size when stats works.
|
||||
func (m *Manager) offboxRecordStats(ctx context.Context, base, env []string) int {
|
||||
func (m *Manager) offboxRecordStats(ctx context.Context, base, env []string) (int, bool) {
|
||||
sctx, cancel := context.WithTimeout(ctx, offboxProbeTimeout)
|
||||
defer cancel()
|
||||
out, err := m.runner()(sctx, env, append(append([]string{}, base...), "snapshots", "--json")...)
|
||||
if err != nil {
|
||||
return 0
|
||||
m.logger.Printf("[WARN] [offbox] snapshot count unreadable (kept the last measured value): %v", err)
|
||||
return 0, false
|
||||
}
|
||||
var snaps []struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if json.Unmarshal(out, &snaps) != nil {
|
||||
return 0
|
||||
if jerr := json.Unmarshal(out, &snaps); jerr != nil {
|
||||
m.logger.Printf("[WARN] [offbox] snapshot count unparseable (kept the last measured value): %v", jerr)
|
||||
return 0, false
|
||||
}
|
||||
// Repo size (best-effort, RAW-DATA mode). SP-1: `--mode raw-data` reports the actual
|
||||
// deduplicated+compressed repo bytes (what the Storage Box really fills), not the modeless
|
||||
@@ -1863,7 +1895,7 @@ func (m *Manager) offboxRecordStats(ctx context.Context, base, env []string) int
|
||||
})
|
||||
}
|
||||
}
|
||||
return len(snaps)
|
||||
return len(snaps), true
|
||||
}
|
||||
|
||||
// RestoreOffbox restores an app's latest off-box snapshot to destDir (a scratch/verify location — it does
|
||||
|
||||
Reference in New Issue
Block a user