fix: read KEY_ENCRYPTION_KEY in archive-only verify; never leave a partial archive
This commit is contained in:
+56
-7
@@ -42,11 +42,16 @@ func newBackupCmd() *cobra.Command {
|
||||
}
|
||||
defer client.Disconnect(context.Background())
|
||||
|
||||
w, closeOut, name, err := backupDestination(out, g.Database)
|
||||
w, dest, name, err := backupDestination(out, g.Database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer closeOut()
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
dest.Cleanup()
|
||||
}
|
||||
}()
|
||||
|
||||
m, err := backup.Dump(ctx, backup.DumpOptions{
|
||||
Client: client,
|
||||
@@ -60,6 +65,10 @@ func newBackupCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dest.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
committed = true
|
||||
|
||||
// Progress goes to stderr so --out - stays a clean pipe.
|
||||
var docs int64
|
||||
@@ -86,19 +95,59 @@ func newBackupCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
// backupDestination resolves --out to a writer, a closer and a name to print.
|
||||
func backupDestination(out, database string) (io.Writer, func(), string, error) {
|
||||
func backupDestination(out, database string) (io.Writer, destination, string, error) {
|
||||
if out == "-" {
|
||||
return os.Stdout, func() {}, "stdout", nil
|
||||
return os.Stdout, stdoutDestination{}, "stdout", nil
|
||||
}
|
||||
name := archiveName(database, time.Now().UTC())
|
||||
path := filepath.Join(out, name)
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
|
||||
// Written under a temporary name and renamed on success, the same
|
||||
// discipline the agent uses for authorized_keys: a failed backup must not
|
||||
// leave a partial file named exactly like a good archive.
|
||||
tmp := path + ".partial"
|
||||
f, err := os.OpenFile(tmp, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return nil, nil, "", fmt.Errorf("create %s: %w", path, err)
|
||||
return nil, nil, "", fmt.Errorf("create %s: %w", tmp, err)
|
||||
}
|
||||
return f, func() { f.Close() }, path, nil
|
||||
d := &fileDestination{f: f, tmp: tmp, final: path}
|
||||
return f, d, path, nil
|
||||
}
|
||||
|
||||
// fileDestination finishes a file-backed backup. Commit renames the temporary
|
||||
// file into place; Cleanup removes it if Commit was never called.
|
||||
type fileDestination struct {
|
||||
f *os.File
|
||||
tmp string
|
||||
final string
|
||||
}
|
||||
|
||||
func (d *fileDestination) Commit() error {
|
||||
if err := d.f.Close(); err != nil {
|
||||
return fmt.Errorf("close %s: %w", d.tmp, err)
|
||||
}
|
||||
if err := os.Rename(d.tmp, d.final); err != nil {
|
||||
return fmt.Errorf("rename %s: %w", d.tmp, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *fileDestination) Cleanup() {
|
||||
d.f.Close()
|
||||
os.Remove(d.tmp)
|
||||
}
|
||||
|
||||
// destination is how the two --out modes finish. stdout commits by doing
|
||||
// nothing; there is no partial file to clean up either.
|
||||
type destination interface {
|
||||
Commit() error
|
||||
Cleanup()
|
||||
}
|
||||
|
||||
type stdoutDestination struct{}
|
||||
|
||||
func (stdoutDestination) Commit() error { return nil }
|
||||
func (stdoutDestination) Cleanup() {}
|
||||
|
||||
// archiveName is sortable and carries no colon, because an operator will copy
|
||||
// these onto a Windows share sooner or later and a colon is not a legal
|
||||
// filename character there.
|
||||
|
||||
Reference in New Issue
Block a user