Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1af0a20429 |
+54
-6
@@ -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 {
|
||||||
|
Result string `json:"result"`
|
||||||
|
Message string `json:"message"`
|
||||||
Data struct {
|
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))
|
||||||
|
|||||||
Reference in new issue
Block a user