diff --git a/V3/main.go b/V3/main.go index bed1aca..94f9dd7 100644 --- a/V3/main.go +++ b/V3/main.go @@ -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 // 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: // -// go build -ldflags "-X main.version=3.2.0" -var version = "3.2.0" +// go build -ldflags "-X main.version=3.2.1" +var version = "3.2.1" type Config struct { SSHKey string @@ -349,6 +349,7 @@ func removeOldRemoteBackups(r Remote, n *Notifier, days int) { return } + logf("Found %d backup(s) on Home Assistant server.", len(backups)) cutoff := time.Now().Add(-time.Duration(days) * 24 * time.Hour) removed := 0 for _, b := range backups { @@ -374,21 +375,41 @@ func removeOldRemoteBackups(r Remote, n *Notifier, days int) { n.Send("Old backup removed: " + b.Slug) } 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 { - Data struct { + Result string `json:"result"` + Message string `json:"message"` + Data struct { Slug string `json:"slug"` } `json:"data"` 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. func (r Remote) createBackup(name string) (string, error) { out, err := r.run("ha backup new --name=" + name + " --raw-json") 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) } @@ -400,6 +421,9 @@ func (r Remote) createBackup(name string) (string, error) { if resp.Slug != "" { 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). @@ -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") } @@ -629,9 +656,25 @@ func main() { 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)) - if avail < cfg.StorageThresholdKB { + lowSpace := avail < cfg.StorageThresholdKB + if lowSpace { logf("Disk space is below the threshold. Running remote backup cleanup...") 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 { logf("Sufficient disk space available. Skipping remote backup cleanup.") } @@ -644,7 +687,12 @@ func main() { return e }) 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) { fail(fmt.Sprintf("unexpected backup slug %q.", slug))