backup

package
v0.24.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Index

Constants

View Source
const (
	DefaultBootTimeout   = 60 * time.Second
	DefaultHealthTimeout = 30 * time.Second
)

Variables

View Source
var HealthChecks = map[string]HealthCheck{
	"nginx-proxy-manager": {
		Path:          "/",
		ExpectCodes:   []int{200, 301},
		ContainerPort: "81",
		BootTimeout:   DefaultBootTimeout,
		HealthTimeout: DefaultHealthTimeout,
	},
	"vaultwarden": {
		Path:          "/alive",
		ExpectCodes:   []int{200},
		ContainerPort: "80",
		BootTimeout:   DefaultBootTimeout,
		HealthTimeout: DefaultHealthTimeout,
	},
	"uptime-kuma": {
		Path:          "/",
		ExpectCodes:   []int{200},
		ContainerPort: "3001",
		BootTimeout:   DefaultBootTimeout,
		HealthTimeout: DefaultHealthTimeout,
	},
	"pi-hole": {
		Path:          "/admin",
		ExpectCodes:   []int{200, 301},
		ContainerPort: "80",
		BootTimeout:   DefaultBootTimeout,
		HealthTimeout: DefaultHealthTimeout,
	},
	"gitea": {
		Path:          "/",
		ExpectCodes:   []int{200},
		ContainerPort: "3000",
		BootTimeout:   DefaultBootTimeout,
		HealthTimeout: DefaultHealthTimeout,
	},
	"jellyfin": {
		Path:          "/health",
		ExpectCodes:   []int{200},
		ContainerPort: "8096",
		BootTimeout:   DefaultBootTimeout,
		HealthTimeout: DefaultHealthTimeout,
	},
	"plex": {
		Path:          "/web",
		ExpectCodes:   []int{200, 301, 302},
		ContainerPort: "32400",
		BootTimeout:   DefaultBootTimeout,
		HealthTimeout: DefaultHealthTimeout,
	},
	"portainer": {
		Path:          "/",
		ExpectCodes:   []int{200, 301, 302},
		ContainerPort: "9000",
		BootTimeout:   DefaultBootTimeout,
		HealthTimeout: DefaultHealthTimeout,
	},
	"homepage": {
		Path:          "/",
		ExpectCodes:   []int{200},
		ContainerPort: "3000",
		BootTimeout:   DefaultBootTimeout,
		HealthTimeout: DefaultHealthTimeout,
	},
	"adguard-home": {
		Path:          "/",
		ExpectCodes:   []int{200, 302},
		ContainerPort: "3000",
		BootTimeout:   DefaultBootTimeout,
		HealthTimeout: DefaultHealthTimeout,
	},
}

HealthChecks maps app names to their health check configuration. Apps are based on the install.Registry definitions.

Functions

func DirUsage added in v0.24.0

func DirUsage(dir string) (count int, total int64, err error)

DirUsage reports how many archives dir holds and what they total.

This is the half of the picture `doctor` was missing: it already checks that a backup is recent, which says nothing about whether the directory has been growing without a bound since the day it was created.

func FormatSize added in v0.24.0

func FormatSize(bytes int64) string

FormatSize renders a byte count the way homebutler shows sizes.

func ParseByteSize added in v0.24.0

func ParseByteSize(s string) (int64, error)

ParseByteSize reads a size written the way an operator writes one.

Both conventions are accepted for the same reason `df` and every dashboard disagree about them: "20GB" and "20GiB" both mean what the person typing meant, and refusing one of them teaches nothing.

Types

type BackupResult

type BackupResult struct {
	Archive  string   `json:"archive"`
	Services []string `json:"services"`
	Volumes  int      `json:"volumes"`
	Size     string   `json:"size"`

	// Pruned is what retention removed after this backup was written, and is
	// absent when retention is not configured — which is the default.
	Pruned *PruneResult `json:"pruned,omitempty"`
}

BackupResult is returned after a successful backup.

func Run

func Run(backupDir, service string, retention RetentionConfig) (*BackupResult, error)

Run performs a backup of all (or filtered) Docker services.

Retention is applied after the archive is written and only if writing it succeeded. Pruning before, or on the way out of a failure, would delete history to make room for a backup that does not exist.

type ComposeProject

type ComposeProject struct {
	Name       string `json:"Name"`
	Status     string `json:"Status"`
	ConfigFile string `json:"ConfigFiles"`
}

ComposeProject represents a docker compose project from `docker compose ls`.

type DrillOptions added in v0.13.0

type DrillOptions struct {
	BackupDir string // directory containing backup archives
	Archive   string // explicit archive path (overrides BackupDir lookup)
}

DrillOptions configures a drill run.

type DrillReport added in v0.13.0

type DrillReport struct {
	Results []DrillResult `json:"results"`
	Total   int           `json:"total"`
	Passed  int           `json:"passed"`
	Failed  int           `json:"failed"`
}

DrillReport holds the aggregated results of drilling multiple apps.

func RunDrillAll added in v0.13.0

func RunDrillAll(opts DrillOptions) (*DrillReport, error)

RunDrillAll executes a backup drill for every app found in the backup that has a defined health check.

func (*DrillReport) String added in v0.13.0

func (r *DrillReport) String() string

String returns human-readable output for a drill report.

type DrillResult added in v0.13.0

type DrillResult struct {
	App          string `json:"app"`
	Archive      string `json:"archive"`
	Size         string `json:"size"`
	FileCount    int    `json:"file_count"`
	Integrity    bool   `json:"integrity"`
	Booted       bool   `json:"booted"`
	BootSeconds  int    `json:"boot_seconds"`
	HealthStatus int    `json:"health_status"`
	HealthPort   string `json:"health_port"`
	Passed       bool   `json:"passed"`
	Error        string `json:"error,omitempty"`
	Logs         string `json:"logs,omitempty"`
	TotalSeconds int    `json:"total_seconds"`
}

DrillResult holds the outcome of drilling a single app.

func RunDrill added in v0.13.0

func RunDrill(appName string, opts DrillOptions) (result *DrillResult, err error)

RunDrill executes a backup drill for a single app. Returns (nil, error) if the drill cannot start at all. Returns (*DrillResult, nil) once the drill runs — check result.Passed.

func (*DrillResult) String added in v0.13.0

func (r *DrillResult) String() string

String returns human-readable output for a single drill result.

type HealthCheck added in v0.13.0

type HealthCheck struct {
	Path          string        // HTTP path to check (e.g. "/", "/health")
	ExpectCodes   []int         // acceptable HTTP status codes
	ContainerPort string        // container-side port to map
	BootTimeout   time.Duration // max wait for container to start
	HealthTimeout time.Duration // max wait for health endpoint to respond
}

HealthCheck defines how to verify that a restored app is working.

type ListEntry

type ListEntry struct {
	Name      string `json:"name"`
	Path      string `json:"path"`
	Size      string `json:"size"`
	CreatedAt string `json:"created_at"`
}

ListEntry represents a single backup in the list.

func List

func List(backupDir string) ([]ListEntry, error)

List returns all backups in the backup directory.

type Manifest

type Manifest struct {
	Version   string        `json:"version"`
	CreatedAt string        `json:"created_at"`
	Services  []ServiceInfo `json:"services"`
}

Manifest describes a backup archive.

type Mount

type Mount struct {
	Type        string `json:"type"`        // "volume" or "bind"
	Name        string `json:"name"`        // volume name or host path
	Source      string `json:"source"`      // host path
	Destination string `json:"destination"` // container path
}

Mount represents a Docker volume or bind mount.

type PruneResult added in v0.24.0

type PruneResult struct {
	Removed []string `json:"removed,omitempty"`
	Freed   int64    `json:"freed_bytes,omitempty"`
	Kept    int      `json:"kept"`
	KeptSum int64    `json:"kept_bytes"`
}

PruneResult describes what a prune removed.

func Prune added in v0.24.0

func Prune(dir string, cfg RetentionConfig) (*PruneResult, error)

Prune deletes the oldest archives until dir is inside cfg.

The newest archive is never deleted, whatever the limits say — including when it exceeds MaxBytes by itself. A limit that empties the directory has misunderstood what it was asked to bound, and the operator is left with nothing to restore from.

type RefusedMount added in v0.22.1

type RefusedMount struct {
	Service string `json:"service"`
	Type    string `json:"type"`
	Target  string `json:"target"`
	Reason  string `json:"reason"`
}

RefusedMount records one mount that was declined, and why.

type RestoreOptions added in v0.22.1

type RestoreOptions struct {
	// Service restores only the named service when set.
	Service string
	// AllowBind lists host paths the operator has explicitly permitted as
	// bind-mount targets. Empty means no bind mount is restored.
	AllowBind []string
}

RestoreOptions carries what the operator asked for, as distinct from what the archive declares. Every filesystem target restore writes to has to be traceable to this struct rather than to manifest.json.

type RestoreResult

type RestoreResult struct {
	Archive  string   `json:"archive"`
	Services []string `json:"services"`
	Volumes  int      `json:"volumes"`
	// Refused lists mounts the archive asked for and restore declined to
	// perform. A refusal is never silent: an operator who sees fewer volumes
	// than expected must be able to find out why from the result alone.
	Refused []RefusedMount `json:"refused,omitempty"`
}

RestoreResult is returned after a successful restore.

func Restore

func Restore(archivePath string, opts RestoreOptions) (*RestoreResult, error)

Restore extracts an archive and restores volumes.

Paths declared in manifest.json are attacker-controlled whenever the archive came from somewhere else, which is the normal case for a tool built around portable backups. Nothing in the manifest selects a filesystem target on its own: volume names must look like volume names, and bind targets must have been named by the operator in opts.AllowBind.

type RetentionConfig added in v0.24.0

type RetentionConfig struct {
	// MaxArchives is how many archives to keep, newest first. Zero, the
	// default, keeps every archive.
	MaxArchives int `yaml:"max_archives,omitempty" json:"max_archives"`

	// MaxBytes caps the total size of the directory, as a size with a unit —
	// "500MB", "20GB". Empty, the default, means no cap.
	//
	// Count alone is not a disk guarantee, and a disk guarantee is what someone
	// running backup from cron actually wants: ten archives can be 200MB or
	// 200GB depending on what was running when they were taken.
	MaxBytes string `yaml:"max_bytes,omitempty" json:"max_bytes"`
}

RetentionConfig bounds how much backup history is kept on disk.

Both limits default to unlimited, which is the opposite of the incident directory and is deliberate. A pruned incident costs some history; a pruned backup can be the only remaining copy of data that no longer exists anywhere else. Deleting that is not a default anyone chose, so retention is something an operator turns on. What homebutler does without being asked is report the size, through `doctor`.

func (RetentionConfig) IsZero added in v0.24.0

func (r RetentionConfig) IsZero() bool

IsZero reports whether retention is unconfigured, so callers can skip the work and say nothing rather than reporting a prune that removed nothing.

type ServiceInfo

type ServiceInfo struct {
	Name      string  `json:"name"`
	Container string  `json:"container"`
	Image     string  `json:"image"`
	Mounts    []Mount `json:"mounts"`
}

ServiceInfo holds container and mount info for a compose service.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL