diff --git a/.env.dist b/.env.dist index 84241a1..238a749 100644 --- a/.env.dist +++ b/.env.dist @@ -1,12 +1,6 @@ -S3_ENDPOINT= -S3_BUCKET=postgres-backups -S3_ACCESS_KEY= -S3_SECRET_KEY= - -POSTGRES_HOST=postgres -POSTGRES_PORT=5432 -POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres -POSTGRES_DB=postgres - -EVERY=24h \ No newline at end of file +URLS="postgres://user:password@host:port/dbname,mysql://user:password@host:port/dbname" +S3_ENDPOINT="your_s3_endpoint" +S3_BUCKET="your_s3_bucket" +S3_ACCESS_KEY="your_s3_access_key" +S3_SECRET_KEY="your_s3_secret_key" +INTERVAL="24h" diff --git a/.github/workflows/ghcr.yml b/.github/workflows/ghcr.yml index 6f1919b..423fd9f 100644 --- a/.github/workflows/ghcr.yml +++ b/.github/workflows/ghcr.yml @@ -12,7 +12,7 @@ on: env: REGISTRY: ghcr.io - FQDN: "ghcr.io/thedevminertv/postgres-s3-backup" + FQDN: "ghcr.io/thedevminertv/database-s3-backup" jobs: build: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59d446e..2250ca4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: env: REGISTRY: ghcr.io - FQDN: "ghcr.io/thedevminertv/postgres-s3-backup" + FQDN: "ghcr.io/thedevminertv/database-s3-backup" jobs: build: diff --git a/Dockerfile b/Dockerfile index 51e4273..337cf74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,8 +10,8 @@ RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . FROM alpine:latest WORKDIR /root/ -RUN apk --no-cache add ca-certificates postgresql-client +RUN apk --no-cache add ca-certificates postgresql-client mysql-client -COPY --from=builder /app/main /bin/postgres-s3-backup +COPY --from=builder /app/main /bin/database-s3-backup -CMD ["/bin/postgres-s3-backup"] +CMD ["/bin/database-s3-backup"] diff --git a/README.md b/README.md index dadb930..ecb2bfd 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# PostgreSQL Backup to S3 with Docker +# Database Backup to S3 with Docker -This application automates the process of backing up PostgreSQL databases and uploading them to an S3-compatible storage service, utilizing Docker for easy deployment and scheduling. +This application automates the process of backing up PostgreSQL and MySQL databases and uploading them to an S3-compatible storage service, utilizing Docker for easy deployment and scheduling. ## Features - Easy deployment with Docker and Docker Compose. -- Support for multiple PostgreSQL databases. +- Support for multiple PostgreSQL and MySQL databases. - Customizable backup intervals. - Direct upload of backups to an S3-compatible storage bucket. - Environment variable and command-line configuration for flexibility. @@ -14,7 +14,7 @@ This application automates the process of backing up PostgreSQL databases and up ## Prerequisites - Docker and Docker Compose installed on your system. -- Access to a PostgreSQL database. +- Access to PostgreSQL and/or MySQL databases. - Access to an S3-compatible storage service. ## Configuration @@ -25,7 +25,7 @@ Before running the application, you need to configure it either by setting envir Create a `.env` file in the project directory with the following variables: -- `URLS`: Comma-separated list of PostgreSQL database URLs to backup. Format: `postgres://:@[:]/` +- `URLS`: Comma-separated list of database URLs to backup. Format for PostgreSQL: `postgres://:@[:]/` and for MySQL: `mysql://:@[:]/` - `S3_ENDPOINT`: The endpoint URL of your S3-compatible storage service. - `S3_BUCKET`: The name of the bucket where backups will be stored. - `S3_ACCESS_KEY`: Your S3 access key. @@ -41,7 +41,7 @@ services: app: build: . environment: - URLS: "postgres://user:password@host:port/dbname" + URLS: "postgres://user:password@host:port/dbname,mysql://user:password@host:port/dbname" S3_ENDPOINT: "your_s3_endpoint" S3_BUCKET: "your_s3_bucket" S3_ACCESS_KEY: "your_s3_access_key" @@ -51,7 +51,7 @@ services: ## Running the Application with Docker -There is an image available on `ghcr.io/thedevminertv/postgres_s3_backup` that you can use. +There is an image available on `ghcr.io/thedevminertv/database-s3-backup` that you can use. Alternatively, you can build the image yourself: @@ -67,7 +67,7 @@ Alternatively, you can build the image yourself: docker compose up -d ``` -This will start the application in the background. It will automatically perform backups based on the configured interval and upload them to the specified S3 bucket. +This will start the application in the background. It will automatically perform backups for both PostgreSQL and MySQL databases based on the configured interval and upload them to the specified S3 bucket. ## Monitoring and Logs diff --git a/dump.go b/dump.go new file mode 100644 index 0000000..a3d2ced --- /dev/null +++ b/dump.go @@ -0,0 +1,118 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "os/exec" + "strconv" + "time" +) + +type connectionOptions struct { + Host string + DbType string + Port int + Database string + Username string + Password string +} + +var ( + PGDumpCmd = "pg_dump" + pgDumpStdOpts = []string{"--no-owner", "--no-acl", "--clean", "--blobs", "-v"} + pgDumpDefaultFormat = "c" + ErrPgDumpNotFound = errors.New("pg_dump not found") + + MysqlDumpCmd = "mysqldump" + mysqlDumpStdOpts = []string{"--compact", "--skip-add-drop-table", "--skip-add-locks", "--skip-disable-keys", "--skip-set-charset", "-v"} + ErrMySqlDumpNotFound = errors.New("mysqldump not found") + + ErrUnsupportedType = errors.New("unsupported database type") +) + +func RunDump(connectionOpts *connectionOptions, outFile string) error { + cmd, err := buildDumpCommand(connectionOpts, outFile) + if err != nil { + return err + } + + return executeCommand(cmd) +} + +func buildDumpCommand(opts *connectionOptions, outFile string) (*exec.Cmd, error) { + switch opts.DbType { + case "postgres": + if !commandExist(PGDumpCmd) { + return nil, ErrPgDumpNotFound + } + options := append( + pgDumpStdOpts, + fmt.Sprintf("-f%s", outFile), + fmt.Sprintf("--dbname=%s", opts.Database), + fmt.Sprintf("--host=%s", opts.Host), + fmt.Sprintf("--port=%d", opts.Port), + fmt.Sprintf("--username=%s", opts.Username), + fmt.Sprintf("--format=%s", pgDumpDefaultFormat), + ) + return exec.Command(PGDumpCmd, options...), nil + + case "mysql": + mysqldumpCmd := "mysqldump" + if !commandExist(mysqldumpCmd) { + return nil, ErrMySqlDumpNotFound + } + options := append( + mysqlDumpStdOpts, + "-h", opts.Host, + "-P", strconv.Itoa(opts.Port), + "-u", opts.Username, + fmt.Sprintf("--password=%s", opts.Password), + "--databases", opts.Database, + "-r", outFile, + ) + + return exec.Command(mysqldumpCmd, options...), nil + + default: + return nil, ErrUnsupportedType + } +} + +func executeCommand(cmd *exec.Cmd) error { + stderr, err := cmd.StderrPipe() + if err != nil { + return err + } + + if err := cmd.Start(); err != nil { + return err + } + + go func() { + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + fmt.Println(scanner.Text()) + } + }() + + if err := cmd.Wait(); err != nil { + return err + } + return nil +} + +func commandExist(command string) bool { + _, err := exec.LookPath(command) + return err == nil +} + +func newFileName(db string, dbType string) string { + switch dbType { + case "postgres": + return fmt.Sprintf(`%v_%v.pgdump`, db, time.Now().Unix()) + case "mysql": + return fmt.Sprintf(`%v_%v.sql`, db, time.Now().Unix()) + } + return fmt.Sprintf(`%v_%v`, db, time.Now().Unix()) +} diff --git a/go.mod b/go.mod index 48c7f60..6ae3af6 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/rs/xid v1.5.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/stretchr/testify v1.7.0 // indirect golang.org/x/crypto v0.19.0 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.17.0 // indirect diff --git a/go.sum b/go.sum index f33e5b2..949ccd0 100644 --- a/go.sum +++ b/go.sum @@ -4,41 +4,19 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 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/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= -github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 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/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.63 h1:GbZ2oCvaUdgT5640WJOpyDhhDxvknAJU2/T3yurwcbQ= -github.com/minio/minio-go/v7 v7.0.63/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4= -github.com/minio/minio-go/v7 v7.0.64 h1:Zdza8HwOzkld0ZG/og50w56fKi6AAyfqfifmasD9n2Q= -github.com/minio/minio-go/v7 v7.0.64/go.mod h1:R4WVUR6ZTedlCcGwZRauLMIKjgyaWxhs4Mqi/OMPmEc= -github.com/minio/minio-go/v7 v7.0.65 h1:sOlB8T3nQK+TApTpuN3k4WD5KasvZIE3vVFzyyCa0go= -github.com/minio/minio-go/v7 v7.0.65/go.mod h1:R4WVUR6ZTedlCcGwZRauLMIKjgyaWxhs4Mqi/OMPmEc= -github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw= -github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs= -github.com/minio/minio-go/v7 v7.0.67 h1:BeBvZWAS+kRJm1vGTMJYVjKUNoo0FoEt/wUWdUtfmh8= -github.com/minio/minio-go/v7 v7.0.67/go.mod h1:+UXocnUeZ3wHvVh5s95gcrA4YjMIbccT6ubB+1m054A= -github.com/minio/minio-go/v7 v7.0.68 h1:hTqSIfLlpXaKuNy4baAp4Jjy2sqZEN9hRxD0M4aOfrQ= -github.com/minio/minio-go/v7 v7.0.68/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ= 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= @@ -52,36 +30,17 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index 7a315b3..d573bf5 100644 --- a/main.go +++ b/main.go @@ -3,15 +3,16 @@ package main import ( "context" "flag" - "github.com/joho/godotenv" - "github.com/minio/minio-go/v7" - "github.com/minio/minio-go/v7/pkg/credentials" "log" "net/url" "os" "strconv" "strings" "time" + + "github.com/joho/godotenv" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" ) func main() { @@ -42,7 +43,13 @@ func main() { log.Fatalf("Failed to parse url %s: %s", rawUrl, err) } - port := 5432 + port := 0 + switch parsedUrl.Scheme { + case "postgres": + port = 5432 + case "mysql": + port = 3306 + } rawPort := parsedUrl.Port() if rawPort != "" { port, err = strconv.Atoi(rawPort) @@ -58,6 +65,7 @@ func main() { urls[i] = connectionOptions{ Host: parsedUrl.Hostname(), + DbType: parsedUrl.Scheme, Port: port, Database: strings.TrimPrefix(parsedUrl.Path, "/"), Username: parsedUrl.User.Username(), @@ -79,8 +87,7 @@ func main() { for { for _, u := range urls { log.Printf("Backing up %s", u.Database) - - file := newFileName(u.Database) + file := newFileName(u.Database, u.DbType) if err = RunDump(&u, file); err != nil { log.Printf("WARNING: Failed to dump database: %s", err) diff --git a/pgdump.go b/pgdump.go deleted file mode 100644 index 3dc70ce..0000000 --- a/pgdump.go +++ /dev/null @@ -1,77 +0,0 @@ -package main - -import ( - "bufio" - "errors" - "fmt" - "os" - "os/exec" - "time" -) - -type connectionOptions struct { - Host string - Port int - Database string - Username string - Password string -} - -var ( - PGDumpCmd = "pg_dump" - pgDumpStdOpts = []string{"--no-owner", "--no-acl", "--clean", "--blobs", "-v"} - pgDumpDefaultFormat = "c" - - ErrPgDumpNotFound = errors.New("pg_dump not found") -) - -func RunDump(pg *connectionOptions, outFile string) error { - if !commandExist(PGDumpCmd) { - return ErrPgDumpNotFound - } - - options := append( - pgDumpStdOpts, - fmt.Sprintf(`-f%s`, outFile), - fmt.Sprintf(`--dbname=%v`, pg.Database), - fmt.Sprintf(`--host=%v`, pg.Host), - fmt.Sprintf(`--port=%v`, pg.Port), - fmt.Sprintf(`--username=%v`, pg.Username), - fmt.Sprintf(`--format=%v`, pgDumpDefaultFormat), - ) - - cmd := exec.Command(PGDumpCmd, options...) - cmd.Env = append(os.Environ(), fmt.Sprintf(`PGPASSWORD=%v`, pg.Password)) - - stderr, err := cmd.StderrPipe() - if err != nil { - return err - } - - if err = cmd.Start(); err != nil { - return err - } - - go func() { - scanner := bufio.NewScanner(stderr) - scanner.Split(bufio.ScanLines) - for scanner.Scan() { - fmt.Println(scanner.Text()) - } - }() - - if err = cmd.Wait(); err != nil { - return err - } - - return nil -} - -func commandExist(command string) bool { - _, err := exec.LookPath(command) - return err == nil -} - -func newFileName(db string) string { - return fmt.Sprintf(`%v_%v.pgdump`, db, time.Now().Unix()) -}