From 896a5d9def2f2f3270301a30e9ffa203db4e2ce3 Mon Sep 17 00:00:00 2001 From: delta Date: Sat, 11 May 2024 11:42:48 +0900 Subject: [PATCH] =?UTF-8?q?[=E2=99=BB=EF=B8=8F]=20refactor:=20improved=20p?= =?UTF-8?q?roject=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 8 +- README.md | 20 ++-- cmd/backup.go | 21 ++++ cmd/main.go | 34 ------- cmd/root.go | 54 ++++++++++ cmd/schedule/run.go | 37 +++++++ cmd/schedule/schedule.go | 19 ++++ cmd/upload.go | 56 ----------- go.mod | 23 +++-- go.sum | 52 +++++----- internal/backup.go | 47 +++++++++ internal/compress.go | 43 ++++++++ internal/compress/compress.go | 21 ---- internal/config/compress.go | 40 ++++++++ internal/config/config.go | 30 ++---- internal/config/notify/discord.go | 1 + internal/config/notify/notify.go | 13 +++ internal/config/notify/telegram.go | 18 ++++ internal/config/storage/local.go | 5 + internal/config/{upload => storage}/s3.go | 7 +- internal/config/storage/storage.go | 6 ++ internal/config/upload/upload.go | 5 - internal/dump.go | 68 +++++++++++++ internal/dump/dump.go | 117 ---------------------- internal/storage/local/local.go | 1 + internal/storage/s3/upload.go | 60 +++++++++++ internal/upload/s3/upload.go | 54 ---------- main.go | 18 ++++ 28 files changed, 517 insertions(+), 361 deletions(-) create mode 100644 cmd/backup.go delete mode 100644 cmd/main.go create mode 100644 cmd/root.go create mode 100644 cmd/schedule/run.go create mode 100644 cmd/schedule/schedule.go delete mode 100644 cmd/upload.go create mode 100644 internal/backup.go create mode 100644 internal/compress.go delete mode 100644 internal/compress/compress.go create mode 100644 internal/config/compress.go create mode 100644 internal/config/notify/discord.go create mode 100644 internal/config/notify/notify.go create mode 100644 internal/config/notify/telegram.go create mode 100644 internal/config/storage/local.go rename internal/config/{upload => storage}/s3.go (76%) create mode 100644 internal/config/storage/storage.go delete mode 100644 internal/config/upload/upload.go create mode 100644 internal/dump.go delete mode 100644 internal/dump/dump.go create mode 100644 internal/storage/local/local.go create mode 100644 internal/storage/s3/upload.go delete mode 100644 internal/upload/s3/upload.go create mode 100644 main.go diff --git a/Dockerfile b/Dockerfile index deba32b..c315b18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ COPY go.mod go.sum ./ RUN go mod tidy COPY . . -RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /app/app ./cmd +RUN CGO_ENABLED=0 go build -a -trimpath -ldflags="-s -w" -o /app/app . FROM bitnami/minideb:latest @@ -14,8 +14,8 @@ WORKDIR /app RUN install_packages wget gnupg2 lsb-release ca-certificates && \ echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ - apt remove -y wget gnupg2 lsb-release && apt autoremove -y && apt update + apt remove -y wget gnupg2 lsb-release && apt autoremove -y && apt update && install_packages postgresql-client-16 -COPY --from=build /app/app /bin/postgres-backup +COPY --from=build /app/app /usr/local/bin/postgres-backup -CMD ["postgres-backup", "upload-schedule"] +CMD ["postgres-backup", "backup"] diff --git a/README.md b/README.md index b86f5c3..1076806 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ docker run -v ./config.hcl:/etc/postgres_backup/config.hcl ghcr.io/deltalaborato ## docker compose ```yaml backup: - image: ghcr.io/deltalaboratory/postgres-backup:v0.0.1 + image: ghcr.io/deltalaboratory/postgres-backup:latest volumes: - ./postgres-backup.hcl:/etc/postgres_backup/config.hcl restart: unless-stopped @@ -38,7 +38,7 @@ postgres { } # backup storage configuration -upload { +storage { # S3 storage configuration s3 { # S3 endpoint @@ -59,18 +59,20 @@ upload { } } -# backup schedule, required when using `upload-schedule` command +compress { + # algorithm, support `zstd`, optional + algorithm = "zstd" + # compress level, optional + # for zstd, see https://github.com/klauspost/compress/tree/master/zstd#compressor for more information, default 3 + compress_level = 12 +} + +# backup schedule, required when using `storage-schedule` command # see https://pkg.go.dev/github.com/robfig/cron#hdr-CRON_Expression_Format for more information schedule = [ "0 1 * * *", ] -# compress algorithm, support `zstd`, optional -compress_algorithm = "zstd" -# compress level, optional -# for zstd, see https://github.com/klauspost/compress/tree/master/zstd#compressor for more information, default 3 -compress_level = 12 - # verbose mode verbose = false ``` diff --git a/cmd/backup.go b/cmd/backup.go new file mode 100644 index 0000000..7fb8fed --- /dev/null +++ b/cmd/backup.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "github.com/spf13/cobra" + + "github.com/DeltaLaboratory/postgres-backup/internal" +) + +// backupCmd represents the backup command +var backupCmd = &cobra.Command{ + Use: "backup", + Short: "Backup a PostgreSQL database", + Long: `Backup a PostgreSQL database one and now`, + Run: func(cmd *cobra.Command, args []string) { + internal.Backup() + }, +} + +func init() { + RootCmd.AddCommand(backupCmd) +} diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index 678b642..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import ( - "os" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - - "postgres-backup/internal/config" -) - -func main() { - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Str("caller", "postgres-backup").Logger() - - if err := config.LoadConfig(); err != nil { - log.Fatal().Err(err).Msg("failed to load config") - } - - if len(os.Args) < 2 { - log.Info().Msg("no command provided") - log.Info().Msg("available commands: upload, upload-schedule") - os.Exit(1) - } - - switch os.Args[1] { - case "upload": - CommandUpload() - case "upload-schedule": - CommandUploadSchedule() - default: - log.Error().Str("command", os.Args[1]).Msg("unknown command") - os.Exit(1) - } -} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..b964c44 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" + + "github.com/DeltaLaboratory/postgres-backup/internal/config" +) + +var configFile string + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "postgres-backup", + Short: "Backup PostgreSQL databases to somewhere", + Long: `Backup PostgreSQL databases to somewhere. + +postgresl-backup is a tool to backup PostgreSQL databases to somewhere. It can backup to local filesystem, S3. + +example: + "postgres-backup backup" to backup a PostgreSQL database now + "postgres-backup schedule run" to run the backup schedule defined in the configuration file +`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the RootCmd. +func Execute() { + err := RootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(func() { + if configFile != "" { + if err := config.LoadConfig(configFile); err != nil { + panic(err) + } + return + } + + if err := config.LoadConfig(""); err != nil { + panic(err) + } + }) + + RootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "config file (default is /etc/postgres_backup/config.hcl)") +} diff --git a/cmd/schedule/run.go b/cmd/schedule/run.go new file mode 100644 index 0000000..926d014 --- /dev/null +++ b/cmd/schedule/run.go @@ -0,0 +1,37 @@ +package schedule + +import ( + "github.com/robfig/cron/v3" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "github.com/DeltaLaboratory/postgres-backup/internal" + "github.com/DeltaLaboratory/postgres-backup/internal/config" +) + +// runCmd represents the run command +var runCmd = &cobra.Command{ + Use: "run", + Short: "Run the backup schedule", + Long: `Run the backup schedule defined in the configuration file.`, + Run: func(cmd *cobra.Command, args []string) { + if len(config.Loaded.Schedule) == 0 { + log.Fatal().Msg("no schedule provided") + } + + c := cron.New() + for _, schedule := range config.Loaded.Schedule { + if id, err := c.AddFunc(schedule, internal.Backup); err != nil { + log.Fatal().Err(err).Str("schedule", schedule).Msg("failed to add schedule") + } else { + log.Info().Str("schedule", schedule).Str("next", c.Entry(id).Next.String()).Msg("schedule added") + } + } + log.Info().Msg("starting cron") + c.Run() + }, +} + +func init() { + scheduleCmd.AddCommand(runCmd) +} diff --git a/cmd/schedule/schedule.go b/cmd/schedule/schedule.go new file mode 100644 index 0000000..6cd80b5 --- /dev/null +++ b/cmd/schedule/schedule.go @@ -0,0 +1,19 @@ +package schedule + +import ( + "github.com/spf13/cobra" + + "github.com/DeltaLaboratory/postgres-backup/cmd" +) + +// scheduleCmd represents the schedule command +var scheduleCmd = &cobra.Command{ + Use: "schedule", + Short: "Run, manage and monitor backup schedules.", + Long: `Run, manage and monitor backup schedules. +Schedules are defined in the configuration file.`, +} + +func init() { + cmd.RootCmd.AddCommand(scheduleCmd) +} diff --git a/cmd/upload.go b/cmd/upload.go deleted file mode 100644 index 7554350..0000000 --- a/cmd/upload.go +++ /dev/null @@ -1,56 +0,0 @@ -package main - -import ( - "context" - - "github.com/robfig/cron/v3" - "github.com/rs/zerolog/log" - - "postgres-backup/internal/compress" - "postgres-backup/internal/config" - "postgres-backup/internal/dump" - "postgres-backup/internal/upload/s3" -) - -func CommandUpload() { - dumped, err := dump.Dump() - if err != nil { - log.Error().Err(err).Msg("failed to dump database") - return - } - - log.Info().Int("size", len(dumped)).Msg("database dumped") - - if config.Loaded.CompressAlgorithm != nil { - log.Info().Str("algorithm", *config.Loaded.CompressAlgorithm).Int("compress_level", *config.Loaded.CompressLevel).Msg("compressing dump") - if compressed, err := compress.Compress(dumped); err != nil { - log.Error().Err(err).Msg("failed to compress dump") - return - } else { - log.Info().Int("size", len(compressed)).Msg("dump compressed") - dumped = compressed - } - } - - if config.Loaded.Upload.S3 != nil { - if err := s3.Upload(context.TODO(), dumped); err != nil { - log.Error().Err(err).Msg("failed to upload dump to s3") - return - } - } -} - -func CommandUploadSchedule() { - if len(config.Loaded.Schedule) == 0 { - log.Fatal().Msg("no schedule provided") - } - c := cron.New() - for _, schedule := range config.Loaded.Schedule { - if _, err := c.AddFunc(schedule, CommandUpload); err != nil { - log.Fatal().Err(err).Str("schedule", schedule).Msg("failed to add schedule") - } - log.Info().Str("schedule", schedule).Msg("schedule added") - } - log.Info().Msg("starting cron") - c.Run() -} diff --git a/go.mod b/go.mod index 37f3958..0bb887a 100644 --- a/go.mod +++ b/go.mod @@ -1,38 +1,39 @@ -module postgres-backup +module github.com/DeltaLaboratory/postgres-backup go 1.22.2 require ( github.com/hashicorp/hcl/v2 v2.20.1 github.com/klauspost/compress v1.17.8 - github.com/minio/minio-go/v7 v7.0.69 + github.com/minio/minio-go/v7 v7.0.70 github.com/robfig/cron/v3 v3.0.1 github.com/rs/zerolog v1.32.0 + github.com/spf13/cobra v1.8.0 ) require ( github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/md5-simd v1.1.2 // indirect - github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/xid v1.5.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/zclconf/go-cty v1.14.4 // indirect - golang.org/x/crypto v0.22.0 // indirect + golang.org/x/crypto v0.23.0 // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/net v0.25.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.20.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + golang.org/x/tools v0.21.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) diff --git a/go.sum b/go.sum index de2628c..7472c18 100644 --- a/go.sum +++ b/go.sum @@ -3,30 +3,27 @@ github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl/v2 v2.20.1 h1:M6hgdyz7HYt1UN9e61j+qKJBqR3orTWbI1HKBJEdxtc= github.com/hashicorp/hcl/v2 v2.20.1/go.mod h1:TZDqQ4kNKCbh1iJp99FdPiUaVDDUPivbqxZulxDYqL4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= -github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= -github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -37,17 +34,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0= -github.com/minio/minio-go/v7 v7.0.69/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ= -github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= -github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/minio/minio-go/v7 v7.0.70 h1:1u9NtMgfK1U42kUxcsl5v0yj6TEOPR497OAQxpJnn2g= +github.com/minio/minio-go/v7 v7.0.70/go.mod h1:4yBA8v80xGA30cfM3fz0DKYMXunWl/AV/6tWEs9ryzo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -57,30 +47,36 @@ github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/backup.go b/internal/backup.go new file mode 100644 index 0000000..1d047bd --- /dev/null +++ b/internal/backup.go @@ -0,0 +1,47 @@ +package internal + +import ( + "context" + "io" + + "github.com/rs/zerolog/log" + + "github.com/DeltaLaboratory/postgres-backup/internal/config" + "github.com/DeltaLaboratory/postgres-backup/internal/storage/s3" +) + +func Backup() { + process, err := Dump() + if err != nil { + log.Error().Err(err).Msg("failed to dump database") + return + } + + if err := process.Start(); err != nil { + log.Error().Err(err).Msg("failed to start backup process") + return + } + + var reader io.Reader = process + + if config.Loaded.Compress != nil { + log.Info().Str("algorithm", config.Loaded.Compress.Algorithm).Int("compress_level", *config.Loaded.Compress.CompressLevel).Msg("start compress steam") + reader, err = Compress(reader) + if err != nil { + log.Error().Err(err).Msg("failed to compress dump") + return + } + } + + if config.Loaded.Storage.S3 != nil { + if err := s3.Upload(context.Background(), reader); err != nil { + log.Error().Err(err).Msg("failed to upload dump") + } + } + + if err := process.Wait(); err != nil { + log.Error().Err(err).Msg("failed to run backup process") + } + + log.Info().Msg("backup completed") +} diff --git a/internal/compress.go b/internal/compress.go new file mode 100644 index 0000000..70e99ba --- /dev/null +++ b/internal/compress.go @@ -0,0 +1,43 @@ +package internal + +import ( + "errors" + "io" + + "github.com/klauspost/compress/gzip" + "github.com/klauspost/compress/zstd" + + "github.com/DeltaLaboratory/postgres-backup/internal/config" +) + +func Compress(input io.Reader) (io.Reader, error) { + r, w := io.Pipe() + + switch config.Loaded.Compress.Algorithm { + case "zstd": + encoder, err := zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(*config.Loaded.Compress.CompressLevel))) + if err != nil { + return nil, err + } + + go func() { + encoder.ReadFrom(input) + encoder.Close() + w.Close() + }() + + return r, nil + case "gzip": + writer, _ := gzip.NewWriterLevel(w, *config.Loaded.Compress.CompressLevel) + + go func() { + io.Copy(writer, input) + writer.Close() + w.Close() + }() + + return r, nil + default: + return nil, errors.New("unsupported compress algorithm") + } +} diff --git a/internal/compress/compress.go b/internal/compress/compress.go deleted file mode 100644 index c205d83..0000000 --- a/internal/compress/compress.go +++ /dev/null @@ -1,21 +0,0 @@ -package compress - -import ( - "strings" - - "github.com/klauspost/compress/zstd" - - "postgres-backup/internal/config" -) - -func Compress(input []byte) ([]byte, error) { - if config.Loaded.CompressAlgorithm == nil { - return input, nil - } - switch strings.ToLower(*config.Loaded.CompressAlgorithm) { - case "zstd": - encoder, _ := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(*config.Loaded.CompressLevel))) - return encoder.EncodeAll(input, make([]byte, 0, len(input))), nil - } - return input, nil -} diff --git a/internal/config/compress.go b/internal/config/compress.go new file mode 100644 index 0000000..797c76f --- /dev/null +++ b/internal/config/compress.go @@ -0,0 +1,40 @@ +package config + +import ( + "fmt" + "slices" + + "github.com/klauspost/compress/gzip" +) + +var zstdDefaultCompressLevel = 3 +var gzipDefaultCompressLevel = gzip.DefaultCompression + +var compressAlgorithm = []string{ + "zstd", + "gzip", +} + +type CompressConfig struct { + Algorithm string `hcl:"algorithm"` + CompressLevel *int `hcl:"compress_level"` +} + +func (c *CompressConfig) Validate() error { + if !slices.Contains(compressAlgorithm, c.Algorithm) { + return fmt.Errorf("compress.algorithm: unsupported algorithm") + } + + if c.CompressLevel == nil { + switch c.Algorithm { + case "zstd": + c.CompressLevel = &zstdDefaultCompressLevel + case "gzip": + c.CompressLevel = &gzipDefaultCompressLevel + default: + return fmt.Errorf("compress.compress_level: no default level specified for algorithm %s", c.Algorithm) + } + } + + return nil +} diff --git a/internal/config/config.go b/internal/config/config.go index 61d98f4..a90e73a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,25 +1,22 @@ package config import ( - "fmt" "os" "github.com/hashicorp/hcl/v2/hclsimple" - "postgres-backup/internal/config/upload" + "github.com/DeltaLaboratory/postgres-backup/internal/config/storage" ) var Loaded *Config type Config struct { - Postgres PostgresConfig `hcl:"postgres,block"` - Upload upload.Upload `hcl:"upload,block"` + Postgres PostgresConfig `hcl:"postgres,block"` + Storage storage.Storage `hcl:"storage,block"` + Compress *CompressConfig `hcl:"compress,block"` Schedule []string `hcl:"schedule"` - CompressAlgorithm *string `hcl:"compress_algorithm"` - CompressLevel *int `hcl:"compress_level"` - Verbose *bool `hcl:"verbose"` } @@ -32,22 +29,13 @@ func (c Config) IsVerbose() bool { } func (c Config) Validate() error { - if c.CompressAlgorithm != nil { - if *c.CompressAlgorithm != "zstd" { - return fmt.Errorf("invalid compress algorithm: %s", *c.CompressAlgorithm) - } - - if c.CompressLevel == nil { - if *c.CompressAlgorithm == "zstd" { - c.CompressLevel = new(int) - *c.CompressLevel = 3 - } - } + if err := c.Compress.Validate(); err != nil { + return err } return nil } -func LoadConfig() error { +func LoadConfig(location string) error { var cfg Config configLocation := "/etc/postgres_backup/config.hcl" @@ -55,6 +43,10 @@ func LoadConfig() error { configLocation = env } + if location != "" { + configLocation = location + } + err := hclsimple.DecodeFile(configLocation, nil, &cfg) if err != nil { return err diff --git a/internal/config/notify/discord.go b/internal/config/notify/discord.go new file mode 100644 index 0000000..a3131f1 --- /dev/null +++ b/internal/config/notify/discord.go @@ -0,0 +1 @@ +package notify diff --git a/internal/config/notify/notify.go b/internal/config/notify/notify.go new file mode 100644 index 0000000..c5660fd --- /dev/null +++ b/internal/config/notify/notify.go @@ -0,0 +1,13 @@ +package notify + +type Event string + +const ( + EventTriggerSchedule Event = "trigger" + EventSuccess Event = "success" + EventError Event = "error" +) + +type Notify struct { + Events []Event `hcl:"events"` +} diff --git a/internal/config/notify/telegram.go b/internal/config/notify/telegram.go new file mode 100644 index 0000000..daac9cd --- /dev/null +++ b/internal/config/notify/telegram.go @@ -0,0 +1,18 @@ +package notify + +type Telegram struct { + APIUrl *string `hcl:"api_url"` + + Token string `hcl:"token"` + ChatID string `hcl:"chat_id"` + + Events []Event `hcl:"events"` +} + +func (c Telegram) GetAPIUrl() string { + if c.APIUrl == nil { + return "https://api.telegram.org" + } + + return *c.APIUrl +} diff --git a/internal/config/storage/local.go b/internal/config/storage/local.go new file mode 100644 index 0000000..b4028a2 --- /dev/null +++ b/internal/config/storage/local.go @@ -0,0 +1,5 @@ +package storage + +type LocalStorage struct { + Directory string `hcl:"directory"` +} diff --git a/internal/config/upload/s3.go b/internal/config/storage/s3.go similarity index 76% rename from internal/config/upload/s3.go rename to internal/config/storage/s3.go index bde86c3..3b11ea5 100644 --- a/internal/config/upload/s3.go +++ b/internal/config/storage/s3.go @@ -1,6 +1,6 @@ -package upload +package storage -type S3Config struct { +type S3Storage struct { Endpoint string `hcl:"endpoint"` AccessKey string `hcl:"access_key"` SecretKey string `hcl:"secret_key"` @@ -11,9 +11,10 @@ type S3Config struct { Prefix *string `hcl:"prefix"` } -func (c S3Config) GetRegion() string { +func (c S3Storage) GetRegion() string { if c.Region == nil { return "" } + return *c.Region } diff --git a/internal/config/storage/storage.go b/internal/config/storage/storage.go new file mode 100644 index 0000000..a1f1dda --- /dev/null +++ b/internal/config/storage/storage.go @@ -0,0 +1,6 @@ +package storage + +type Storage struct { + S3 *S3Storage `hcl:"s3,block"` + Local *LocalStorage `hcl:"local,block"` +} diff --git a/internal/config/upload/upload.go b/internal/config/upload/upload.go deleted file mode 100644 index 4e07929..0000000 --- a/internal/config/upload/upload.go +++ /dev/null @@ -1,5 +0,0 @@ -package upload - -type Upload struct { - S3 *S3Config `hcl:"s3,block"` -} diff --git a/internal/dump.go b/internal/dump.go new file mode 100644 index 0000000..22e067b --- /dev/null +++ b/internal/dump.go @@ -0,0 +1,68 @@ +package internal + +import ( + "fmt" + "io" + "os" + "os/exec" + + "github.com/DeltaLaboratory/postgres-backup/internal/config" +) + +type Process struct { + cmd *exec.Cmd + + stdout io.ReadCloser + done chan struct{} +} + +func (p *Process) Start() error { + p.stdout, _ = p.cmd.StdoutPipe() + if err := p.cmd.Start(); err != nil { + return err + } + return nil +} + +func (p *Process) Wait() error { + err := p.cmd.Wait() + p.stdout.Close() + return err +} + +func (p *Process) Read(pb []byte) (int, error) { + if p.stdout == nil { + return 0, fmt.Errorf("process is not started yet") + } + return p.stdout.Read(pb) +} + +// TODO: dump multiple databases + +func Dump() (*Process, error) { + process := new(Process) + + argument := []string{ + "--format", "custom", + "--host", config.Loaded.Postgres.Host, + } + + if config.Loaded.Postgres.Port != nil { + argument = append(argument, "--port", fmt.Sprintf("%d", *config.Loaded.Postgres.Port)) + } + + if config.Loaded.Postgres.User != nil { + argument = append(argument, "--username", *config.Loaded.Postgres.User) + } + + if config.Loaded.Postgres.Database != nil { + argument = append(argument, "--dbname", *config.Loaded.Postgres.Database) + } + + process.cmd = exec.Command("pg_dump", argument...) + if config.Loaded.Postgres.Password != nil { + process.cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", *config.Loaded.Postgres.Password)) + } + + return process, nil +} diff --git a/internal/dump/dump.go b/internal/dump/dump.go deleted file mode 100644 index 5aa7ecd..0000000 --- a/internal/dump/dump.go +++ /dev/null @@ -1,117 +0,0 @@ -package dump - -import ( - "fmt" - "io" - "os" - "os/exec" - - "github.com/rs/zerolog/log" - - "postgres-backup/internal/config" -) - -const MinVersion = 9 -const MaxVersion = 16 - -func InstallClient(version int) error { - if version < MinVersion || version > MaxVersion { - return fmt.Errorf("unsupported version %d", version) - } - - // check if the client is already installed - cmd := exec.Command("dpkg", "-l", fmt.Sprintf("postgresql-client-%d", version)) - if err := cmd.Run(); err == nil { - return nil - } - - logger := log.Logger.With().Str("caller", "install_client").Logger() - - cmd = exec.Command("install_packages", fmt.Sprintf("postgresql-client-%d", version)) - - if config.Loaded.IsVerbose() { - stdout, _ := cmd.StdoutPipe() - stderr, _ := cmd.StderrPipe() - - go func() { - _, err := io.Copy(os.Stdout, stdout) - if err != nil { - logger.Warn().Err(err).Msg("failed to copy stdout") - } - }() - go func() { - _, err := io.Copy(os.Stderr, stderr) - if err != nil { - logger.Warn().Err(err).Msg("failed to copy stderr") - } - }() - } - - logger.Info().Str("command", cmd.String()).Msg("installing dumper") - - err := cmd.Run() - if err != nil { - logger.Error().Err(err).Str("command", cmd.String()).Msg("failed to install postgres client") - return err - } - - logger.Info().Msg("postgres client installed") - - return nil -} - -// TODO: dump multiple databases - -func Dump() ([]byte, error) { - logger := log.Logger.With().Str("caller", "dump_database").Logger() - - if err := InstallClient(config.Loaded.Postgres.Version); err != nil { - return nil, err - } - - argument := []string{ - "--format", "custom", - "--host", config.Loaded.Postgres.Host, - } - - if config.Loaded.Postgres.Port != nil { - argument = append(argument, "--port", fmt.Sprintf("%d", *config.Loaded.Postgres.Port)) - } - - if config.Loaded.Postgres.User != nil { - argument = append(argument, "--username", *config.Loaded.Postgres.User) - } - - if config.Loaded.Postgres.Database != nil { - argument = append(argument, "--dbname", *config.Loaded.Postgres.Database) - } - - cmd := exec.Command("pg_dump", argument...) - if config.Loaded.Postgres.Password != nil { - cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", *config.Loaded.Postgres.Password)) - } - - stdout, _ := cmd.StdoutPipe() - - logger.Info().Msg("dumping database") - - err := cmd.Start() - if err != nil { - logger.Error().Err(err).Str("command", cmd.String()).Msg("failed to dump database") - return nil, err - } - - backup, err := io.ReadAll(stdout) - if err != nil { - logger.Error().Err(err).Msg("failed to read backup") - return nil, err - } - - err = cmd.Wait() - if err != nil { - logger.Error().Err(err).Str("command", cmd.String()).Msg("failed to dump database") - return nil, err - } - - return backup, nil -} diff --git a/internal/storage/local/local.go b/internal/storage/local/local.go new file mode 100644 index 0000000..469c3dc --- /dev/null +++ b/internal/storage/local/local.go @@ -0,0 +1 @@ +package local diff --git a/internal/storage/s3/upload.go b/internal/storage/s3/upload.go new file mode 100644 index 0000000..7a0e263 --- /dev/null +++ b/internal/storage/s3/upload.go @@ -0,0 +1,60 @@ +package s3 + +import ( + "context" + "fmt" + "io" + "time" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/rs/zerolog/log" + + "github.com/DeltaLaboratory/postgres-backup/internal/config" +) + +func Upload(ctx context.Context, reader io.Reader) error { + logger := log.Logger.With().Str("caller", "upload_s3").Logger() + + if config.Loaded.Storage.S3 == nil { + return fmt.Errorf("s3: config is not present") + } + + client, err := minio.New(config.Loaded.Storage.S3.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(config.Loaded.Storage.S3.AccessKey, config.Loaded.Storage.S3.SecretKey, ""), + Region: config.Loaded.Storage.S3.GetRegion(), + Secure: true, + }) + + if err != nil { + return fmt.Errorf("s3: failed to create client: %w", err) + } + + if config.Loaded.IsVerbose() { + client.TraceOn(log.Logger) + } + + logger.Info().Msg("uploading dump to s3") + + objectName := time.Now().Format("2006-01-02T15:04:05") + + if config.Loaded.Storage.S3.Prefix != nil { + objectName = fmt.Sprintf("%s/%s", *config.Loaded.Storage.S3.Prefix, objectName) + } + + if config.Loaded.Compress != nil { + objectName = fmt.Sprintf("%s.%s", objectName, config.Loaded.Compress.Algorithm) + } + + objectName = fmt.Sprintf("%s.sql", objectName) + + info, err := client.PutObject(ctx, config.Loaded.Storage.S3.Bucket, objectName, reader, -1, minio.PutObjectOptions{ + SendContentMd5: true, + }) + if err != nil { + return fmt.Errorf("s3: failed to storage dump: %w", err) + } + + logger.Info().Str("key", info.Key).Msg("dump uploaded") + return nil +} diff --git a/internal/upload/s3/upload.go b/internal/upload/s3/upload.go deleted file mode 100644 index 5531276..0000000 --- a/internal/upload/s3/upload.go +++ /dev/null @@ -1,54 +0,0 @@ -package s3 - -import ( - "bytes" - "context" - "fmt" - "time" - - "github.com/minio/minio-go/v7" - "github.com/minio/minio-go/v7/pkg/credentials" - "github.com/rs/zerolog/log" - - "postgres-backup/internal/config" -) - -func Upload(ctx context.Context, data []byte) error { - logger := log.Logger.With().Str("caller", "upload_s3").Logger() - - if config.Loaded.Upload.S3 == nil { - return fmt.Errorf("s3: config is not present") - } - - client, err := minio.New(config.Loaded.Upload.S3.Endpoint, &minio.Options{ - Creds: credentials.NewStaticV4(config.Loaded.Upload.S3.AccessKey, config.Loaded.Upload.S3.SecretKey, ""), - Region: config.Loaded.Upload.S3.GetRegion(), - Secure: true, - }) - - if err != nil { - return fmt.Errorf("s3: failed to create client: %w", err) - } - - logger.Info().Msg("uploading dump to s3") - - objectName := time.Now().Format("2006-01-02T15:04:05") - - if config.Loaded.Upload.S3.Prefix != nil { - objectName = fmt.Sprintf("%s/%s", *config.Loaded.Upload.S3.Prefix, objectName) - } - - if config.Loaded.CompressAlgorithm != nil { - objectName = fmt.Sprintf("%s.%s", objectName, *config.Loaded.CompressAlgorithm) - } - - objectName = fmt.Sprintf("%s.%s", objectName, "sql") - - info, err := client.PutObject(ctx, config.Loaded.Upload.S3.Bucket, objectName, bytes.NewReader(data), int64(len(data)), minio.PutObjectOptions{}) - if err != nil { - return fmt.Errorf("s3: failed to upload dump: %w", err) - } - - logger.Info().Str("key", info.Key).Msg("dump uploaded") - return nil -} diff --git a/main.go b/main.go new file mode 100644 index 0000000..3ac939e --- /dev/null +++ b/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "os" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "github.com/DeltaLaboratory/postgres-backup/cmd" + + _ "github.com/DeltaLaboratory/postgres-backup/cmd/schedule" +) + +func main() { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Str("caller", "postgres-backup").Logger() + + cmd.Execute() +}