From c1dcb89682069331e83a388f2999e70256a3254a Mon Sep 17 00:00:00 2001 From: stephane-klein Date: Thu, 17 Apr 2025 12:16:26 +0200 Subject: [PATCH] Add --uniform-timestamp option (#146) This option apply a single consistent timestamp to all filenames generated by pg_back instead of using individual file creation times. --- config.go | 7 ++++++- main.go | 39 +++++++++++++++++++++++++++------------ pg_back.conf | 4 ++++ 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/config.go b/config.go index a8c8f40..1cb0b90 100644 --- a/config.go +++ b/config.go @@ -82,6 +82,7 @@ type options struct { Decrypt bool WithRolePasswords bool DumpOnly bool + UniformTimestamp bool Upload string // values are none, b2, s3, sftp, gcs UploadPrefix string @@ -300,6 +301,7 @@ func parseCli(args []string) (options, []string, error) { pflag.StringVarP(&purgeKeep, "purge-min-keep", "K", "0", "minimum number of dumps to keep when purging or 'all' to keep\neverything") pflag.StringVar(&opts.PreHook, "pre-backup-hook", "", "command to run before taking dumps") pflag.StringVar(&opts.PostHook, "post-backup-hook", "", "command to run after taking dumps\n") + pflag.BoolVar(&opts.UniformTimestamp, "uniform-timestamp", false, "Use the same timestamp for all pg_back files instead of individual creation times") pflag.BoolVar(&opts.Encrypt, "encrypt", false, "encrypt the dumps") NoEncrypt := pflag.Bool("no-encrypt", false, "do not encrypt the dumps") @@ -559,7 +561,7 @@ func validateConfigurationFile(cfg *ini.File) error { "sftp_port", "sftp_user", "sftp_password", "sftp_directory", "sftp_identity", "sftp_ignore_hostkey", "gcs_bucket", "gcs_endpoint", "gcs_keyfile", "azure_container", "azure_account", "azure_key", "azure_endpoint", "pg_dump_options", - "dump_role_passwords", "dump_only", "upload_prefix", "delete_uploaded", + "dump_role_passwords", "dump_only", "upload_prefix", "delete_uploaded", "uniform_timestamp", } gkLoop: @@ -653,6 +655,7 @@ func loadConfigurationFile(path string) (options, error) { opts.CipherPublicKey = s.Key("cipher_public_key").MustString("") opts.CipherPrivateKey = s.Key("cipher_private_key").MustString("") opts.EncryptKeepSrc = s.Key("encrypt_keep_source").MustBool(false) + opts.UniformTimestamp = s.Key("uniform_timestamp").MustBool(false) opts.Upload = s.Key("upload").MustString("none") opts.UploadPrefix = s.Key("upload_prefix").MustString("") @@ -988,6 +991,8 @@ func mergeCliAndConfigOptions(cliOpts options, configOpts options, onCli []strin opts.Username = cliOpts.Username case "dbname": opts.ConnDb = cliOpts.ConnDb + case "uniform-timestamp": + opts.UniformTimestamp = cliOpts.UniformTimestamp } } diff --git a/main.go b/main.go index fb3518d..a54f7f1 100644 --- a/main.go +++ b/main.go @@ -303,6 +303,12 @@ func run() (retVal error) { } defer db.Close() + // Generate a single datetime that will be used in all files generated by pg_back + var uniformTimestamp time.Time + if opts.UniformTimestamp { + uniformTimestamp = time.Now() + } + if !opts.DumpOnly { if !db.superuser { l.Infoln("connection user is not superuser, some information will not be dumped") @@ -316,7 +322,7 @@ func run() (retVal error) { } else { l.Infoln("dumping globals without role passwords") } - if err := dumpGlobals(opts.Directory, opts.Mode, opts.TimeFormat, dumpRolePasswords, conninfo, producedFiles); err != nil { + if err := dumpGlobals(opts.Directory, opts.Mode, opts.TimeFormat, dumpRolePasswords, conninfo, producedFiles, uniformTimestamp); err != nil { return fmt.Errorf("pg_dumpall of globals failed: %w", err) } @@ -326,7 +332,7 @@ func run() (retVal error) { perr *pgPrivError ) - if err := dumpSettings(opts.Directory, opts.Mode, opts.TimeFormat, db, producedFiles); err != nil { + if err := dumpSettings(opts.Directory, opts.Mode, opts.TimeFormat, db, producedFiles, uniformTimestamp); err != nil { if errors.As(err, &verr) || errors.As(err, &perr) { l.Warnln(err) } else { @@ -334,7 +340,7 @@ func run() (retVal error) { } } - if err := dumpConfigFiles(opts.Directory, opts.Mode, opts.TimeFormat, db, producedFiles); err != nil { + if err := dumpConfigFiles(opts.Directory, opts.Mode, opts.TimeFormat, db, producedFiles, uniformTimestamp); err != nil { return fmt.Errorf("could not dump configuration files: %w", err) } } @@ -386,6 +392,7 @@ func run() (retVal error) { CipherPassphrase: passphrase, CipherPublicKey: publicKey, EncryptKeepSrc: opts.EncryptKeepSrc, + When: uniformTimestamp, ExitCode: -1, PgDumpVersion: pgDumpVersion, } @@ -613,8 +620,6 @@ func (d *dump) dump(fc chan<- sumFileJob) error { return fmt.Errorf("could not acquire lock for %s", dbname) } - d.When = time.Now() - var fileEnd string switch d.Options.Format { case 'p': @@ -631,6 +636,10 @@ func (d *dump) dump(fc chan<- sumFileJob) error { fileEnd = "d" } + if d.When.IsZero() { + d.When = time.Now() + } + file := formatDumpPath(d.Directory, d.TimeFormat, fileEnd, dbname, d.When, d.Options.CompressLevel) formatOpt := fmt.Sprintf("-F%c", d.Options.Format) @@ -935,7 +944,7 @@ func pgToolVersion(tool string) int { return numver } -func dumpGlobals(dir string, mode int, timeFormat string, withRolePasswords bool, conninfo *ConnInfo, fc chan<- sumFileJob) error { +func dumpGlobals(dir string, mode int, timeFormat string, withRolePasswords bool, conninfo *ConnInfo, fc chan<- sumFileJob, uniformTimestamp time.Time) error { command := execPath("pg_dumpall") args := []string{"-g", "-w"} @@ -967,7 +976,11 @@ func dumpGlobals(dir string, mode int, timeFormat string, withRolePasswords bool args = append(args, "--no-role-passwords") } - file := formatDumpPath(dir, timeFormat, "sql", "pg_globals", time.Now(), 0) + if uniformTimestamp.IsZero() { + uniformTimestamp = time.Now() + } + + file := formatDumpPath(dir, timeFormat, "sql", "pg_globals", uniformTimestamp, 0) args = append(args, "-f", file) if err := os.MkdirAll(filepath.Dir(file), 0700); err != nil { @@ -1008,9 +1021,8 @@ func dumpGlobals(dir string, mode int, timeFormat string, withRolePasswords bool return nil } -func dumpSettings(dir string, mode int, timeFormat string, db *pg, fc chan<- sumFileJob) error { - - file := formatDumpPath(dir, timeFormat, "out", "pg_settings", time.Now(), 0) +func dumpSettings(dir string, mode int, timeFormat string, db *pg, fc chan<- sumFileJob, uniformTimestamp time.Time) error { + file := formatDumpPath(dir, timeFormat, "out", "pg_settings", uniformTimestamp, 0) if err := os.MkdirAll(filepath.Dir(file), 0o700); err != nil { return err @@ -1044,9 +1056,12 @@ func dumpSettings(dir string, mode int, timeFormat string, db *pg, fc chan<- sum return nil } -func dumpConfigFiles(dir string, mode int, timeFormat string, db *pg, fc chan<- sumFileJob) error { +func dumpConfigFiles(dir string, mode int, timeFormat string, db *pg, fc chan<- sumFileJob, uniformTimestamp time.Time) error { for _, param := range []string{"hba_file", "ident_file"} { - file := formatDumpPath(dir, timeFormat, "out", param, time.Now(), 0) + if uniformTimestamp.IsZero() { + uniformTimestamp = time.Now() + } + file := formatDumpPath(dir, timeFormat, "out", param, uniformTimestamp, 0) if err := os.MkdirAll(filepath.Dir(file), 0700); err != nil { return err diff --git a/pg_back.conf b/pg_back.conf index 908b54b..a016097 100644 --- a/pg_back.conf +++ b/pg_back.conf @@ -53,6 +53,10 @@ with_templates = false # Dump only databases, excluding configuration and globals dump_only = false +# Apply the same consistent timestamp to all filenames generated +# by pg_back instead of using individual file creation times +uniform_timestamp = false + # Format of the dump, understood by pg_dump. Possible values are # plain, custom, tar or directory. format = custom