### Fixed
- Backup failures caused by low disk space on the Home Assistant host are now
  reported as such, e.g. "not enough disk space on Home Assistant server
  (1.2 GB free, threshold 2.0 GB)", instead of only "failed to extract backup slug".
- A failed `ha backup new` now includes Home Assistant's own error message in
  the notification and log.

### Changed
- Free space is re-checked after the remote cleanup runs. If it is still below
  `STORAGE_THRESHOLD_KB`, a warning is sent to ntfy/Discord before the backup
  is attempted.
- The remote cleanup now logs how many backups it found, and says when none are
  older than `REMOTE_BACKUP_CLEANUP` days.
This commit is contained in:
phil committed 2026-11-06 22:53:27 +00:00
1 parent 0444e0d45c
commit 1af0a20429
1 file changed
+55 -7
+55 -7
View File
@@ -1,4 +1,4 @@
// ha-backup v3.2.0: creates a Home Assistant backup over SSH, copies it locally, // ha-backup v3.2.1: creates a Home Assistant backup over SSH, copies it locally,
// prunes old backups (remote and local), logs to /var/log and sends optional // prunes old backups (remote and local), logs to /var/log and sends optional
// ntfy/Discord notifications. Go port of home_assistant_backup.sh. // ntfy/Discord notifications. Go port of home_assistant_backup.sh.
// //
@@ -36,8 +36,8 @@ const defaultLogFile = "/var/log/ha-backup.log"
// version is the release version. It can be overridden at build time with: // version is the release version. It can be overridden at build time with:
// //
// go build -ldflags "-X main.version=3.2.0" // go build -ldflags "-X main.version=3.2.1"
var version = "3.2.0" var version = "3.2.1"
type Config struct { type Config struct {
SSHKey string SSHKey string
@@ -349,6 +349,7 @@ func removeOldRemoteBackups(r Remote, n *Notifier, days int) {
return return
} }
logf("Found %d backup(s) on Home Assistant server.", len(backups))
cutoff := time.Now().Add(-time.Duration(days) * 24 * time.Hour) cutoff := time.Now().Add(-time.Duration(days) * 24 * time.Hour)
removed := 0 removed := 0
for _, b := range backups { for _, b := range backups {
@@ -374,21 +375,41 @@ func removeOldRemoteBackups(r Remote, n *Notifier, days int) {
n.Send("Old backup removed: " + b.Slug) n.Send("Old backup removed: " + b.Slug)
} }
if removed == 0 { if removed == 0 {
logf("No old backups found to remove.") logf("No old backups found to remove (none are older than %d days).", days)
} }
} }
type newBackupResp struct { type newBackupResp struct {
Data struct { Result string `json:"result"`
Message string `json:"message"`
Data struct {
Slug string `json:"slug"` Slug string `json:"slug"`
} `json:"data"` } `json:"data"`
Slug string `json:"slug"` Slug string `json:"slug"`
} }
// describeOutput turns the output of an `ha` command into a short message for
// error reports: the "message" field of a JSON response if there is one,
// otherwise the raw text (collapsed to one line and truncated).
func describeOutput(out string) string {
var resp newBackupResp
if json.Unmarshal([]byte(out), &resp) == nil && resp.Message != "" {
return resp.Message
}
text := strings.Join(strings.Fields(out), " ")
if r := []rune(text); len(r) > 300 {
text = string(r[:300]) + "..."
}
return text
}
// createBackup creates a backup and returns its slug. // createBackup creates a backup and returns its slug.
func (r Remote) createBackup(name string) (string, error) { func (r Remote) createBackup(name string) (string, error) {
out, err := r.run("ha backup new --name=" + name + " --raw-json") out, err := r.run("ha backup new --name=" + name + " --raw-json")
if err != nil { if err != nil {
if d := describeOutput(out); d != "" {
return "", fmt.Errorf("failed to create backup on Home Assistant server: %w (Home Assistant said: %s)", err, d)
}
return "", fmt.Errorf("failed to create backup on Home Assistant server: %w", err) return "", fmt.Errorf("failed to create backup on Home Assistant server: %w", err)
} }
@@ -400,6 +421,9 @@ func (r Remote) createBackup(name string) (string, error) {
if resp.Slug != "" { if resp.Slug != "" {
return strings.TrimSpace(resp.Slug), nil return strings.TrimSpace(resp.Slug), nil
} }
if resp.Result == "error" && resp.Message != "" {
return "", fmt.Errorf("backup creation failed: Home Assistant said: %s", resp.Message)
}
} }
// Fallback: plain-text "slug: xxxx" line (what the original script parsed). // Fallback: plain-text "slug: xxxx" line (what the original script parsed).
@@ -411,6 +435,9 @@ func (r Remote) createBackup(name string) (string, error) {
} }
} }
} }
if d := describeOutput(out); d != "" {
return "", fmt.Errorf("failed to extract backup slug (Home Assistant said: %s)", d)
}
return "", fmt.Errorf("failed to extract backup slug") return "", fmt.Errorf("failed to extract backup slug")
} }
@@ -629,9 +656,25 @@ func main() {
fail("failed to check disk space on Home Assistant server: " + err.Error()) fail("failed to check disk space on Home Assistant server: " + err.Error())
} }
logf("Free space: %s (threshold: %s)", humanBytes(avail*1024), humanBytes(cfg.StorageThresholdKB*1024)) logf("Free space: %s (threshold: %s)", humanBytes(avail*1024), humanBytes(cfg.StorageThresholdKB*1024))
if avail < cfg.StorageThresholdKB { lowSpace := avail < cfg.StorageThresholdKB
if lowSpace {
logf("Disk space is below the threshold. Running remote backup cleanup...") logf("Disk space is below the threshold. Running remote backup cleanup...")
removeOldRemoteBackups(remote, notify, cfg.RemoteCleanupDays) removeOldRemoteBackups(remote, notify, cfg.RemoteCleanupDays)
// Re-check: cleanup may not have freed anything.
if a, err := remote.availableKB(); err != nil {
logf("Could not re-check free space after cleanup: %v", err)
} else {
avail = a
lowSpace = avail < cfg.StorageThresholdKB
logf("Free space after cleanup: %s", humanBytes(avail*1024))
}
if lowSpace {
warn := fmt.Sprintf("Warning: low disk space on Home Assistant server (%s free, threshold %s) and cleanup did not free enough space.",
humanBytes(avail*1024), humanBytes(cfg.StorageThresholdKB*1024))
logf("%s", warn)
notify.Send(warn)
}
} else { } else {
logf("Sufficient disk space available. Skipping remote backup cleanup.") logf("Sufficient disk space available. Skipping remote backup cleanup.")
} }
@@ -644,7 +687,12 @@ func main() {
return e return e
}) })
if err != nil { if err != nil {
fail(err.Error() + ".") msg := err.Error()
if lowSpace {
msg = fmt.Sprintf("not enough disk space on Home Assistant server (%s free, threshold %s); %s",
humanBytes(avail*1024), humanBytes(cfg.StorageThresholdKB*1024), msg)
}
fail(msg + ".")
} }
if !slugRe.MatchString(slug) { if !slugRe.MatchString(slug) {
fail(fmt.Sprintf("unexpected backup slug %q.", slug)) fail(fmt.Sprintf("unexpected backup slug %q.", slug))