Something went wrong. Try again.
🪺 PoP Zero is my homelab
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224package main
import ( "bufio" "bytes" "fmt" "log/slog" "os" "os/exec" "path/filepath")
type db struct { name string kind string host string secret string path string db string exclude bool}
func loadDBs(p string) ([]db, error) { d, err := os.ReadDir(p) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var dbs []db for _, e := range d { if e.IsDir() { continue } fi, err := os.Stat(filepath.Join(p, e.Name())) if err != nil { return nil, fmt.Errorf("%s: %w", e.Name(), err) } if fi.IsDir() { continue } db, err := parseDB(filepath.Join(p, e.Name())) if err != nil { return nil, fmt.Errorf("%s: %w", e.Name(), err) } db.name = e.Name() dbs = append(dbs, db) } return dbs, nil}
func parseDB(p string) (db, error) { f, err := os.Open(p) if err != nil { return db{}, err } defer func() { if err := f.Close(); err != nil { slog.Error("closing db config", "err", err) } }() d := db{} s := bufio.NewScanner(f) for s.Scan() { line := s.Text() key, val, ok := splitKV(line) if !ok { continue } switch key { case "kind": d.kind = val case "host": d.host = val case "secret": d.secret = val case "path": d.path = val case "db": d.db = val case "exclude": d.exclude = val == "true" } } if err := s.Err(); err != nil { return db{}, err } if d.kind == "" { return db{}, fmt.Errorf("missing kind") } if d.secret == "" && d.kind != "sqlite" { return db{}, fmt.Errorf("missing secret") } return d, nil}
func splitKV(s string) (string, string, bool) { for i := range s { if s[i] == '=' { return s[:i], s[i+1:], true } } return "", "", false}
func envFromFile(p string) ([]string, error) { f, err := os.Open(p) if err != nil { return nil, err } defer func() { if err := f.Close(); err != nil { slog.Error("closing env file", "err", err) } }() var env []string s := bufio.NewScanner(f) for s.Scan() { line := s.Text() if line == "" || line[0] == '#' { continue } env = append(env, line) } return env, s.Err()}
func (d db) dump(dst string) error { var env []string var err error if d.kind != "sqlite" { env, err = envFromFile(d.secret) if err != nil { return fmt.Errorf("loading %s: %w", d.secret, err) } } switch d.kind { case "postgres": return d.dumpPostgres(dst, env) case "mariadb": return d.dumpMariaDB(dst, env) case "sqlite": return d.dumpSQLite(dst, env) default: return fmt.Errorf("unknown kind %q", d.kind) }}
func (d db) sqlFile() string { if d.name != "" { return d.name + ".sql" } return d.kind + ".sql"}
func (d db) dumpPostgres(dst string, env []string) error { host := d.host if host == "" { return fmt.Errorf("missing host") } user := envVal(env, "POSTGRES_USER") name := envVal(env, "POSTGRES_DB") if user == "" || name == "" { return fmt.Errorf("missing POSTGRES_USER or POSTGRES_DB in secret %s", d.secret) } out := filepath.Join(dst, d.sqlFile()) c := exec.Command("pg_dump", "-h", host, "-U", user, "-d", name, "-f", out) c.Env = append(os.Environ(), "PGPASSWORD="+envVal(env, "POSTGRES_PASSWORD")) var b bytes.Buffer c.Stderr = &b if err := c.Run(); err != nil { return fmt.Errorf("pg_dump %s: %w: %s", d.host, err, b.String()) } return nil}
func (d db) dumpMariaDB(dst string, env []string) error { host := d.host if host == "" { return fmt.Errorf("missing host") } pw := envVal(env, "MARIADB_ROOT_PASSWORD") if pw == "" { pw = envVal(env, "MYSQL_ROOT_PASSWORD") } if pw == "" { return fmt.Errorf("missing MARIADB_ROOT_PASSWORD or MYSQL_ROOT_PASSWORD in secret %s", d.secret) } out := filepath.Join(dst, d.sqlFile()) c := exec.Command("mariadb-dump", "-h", host, "-u", "root", "--all-databases", "--result-file="+out) c.Env = append(os.Environ(), "MYSQL_PWD="+pw) var b bytes.Buffer c.Stderr = &b if err := c.Run(); err != nil { return fmt.Errorf("mariadb-dump %s: %w: %s", d.host, err, b.String()) } return nil}
func (d db) dumpSQLite(dst string, env []string) error { if d.db == "" { return fmt.Errorf("missing db") } out := filepath.Join(dst, filepath.Base(d.db)) c := exec.Command("sqlite3", d.db, fmt.Sprintf(".backup %s", out)) var b bytes.Buffer c.Stderr = &b if err := c.Run(); err != nil { return fmt.Errorf("sqlite3 %s: %w: %s", d.db, err, b.String()) } return nil}
func envVal(env []string, key string) string { pref := key + "=" for _, e := range env { if len(e) > len(pref) && e[:len(pref)] == pref { return e[len(pref):] } } return ""}