16
go.mod
16
go.mod
@@ -3,31 +3,29 @@ module github.com/jkaninda/pg-bkup
|
|||||||
go 1.23.2
|
go 1.23.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/ProtonMail/gopenpgp/v2 v2.7.5
|
||||||
|
github.com/aws/aws-sdk-go v1.55.5
|
||||||
|
github.com/bramvdbogaerde/go-scp v1.5.0
|
||||||
github.com/go-mail/mail v2.3.1+incompatible
|
github.com/go-mail/mail v2.3.1+incompatible
|
||||||
github.com/jkaninda/encryptor v0.0.0-20241013064832-ed4bd6a1b221
|
github.com/jkaninda/encryptor v0.0.0-20241013064832-ed4bd6a1b221
|
||||||
github.com/jkaninda/go-storage v0.1.1
|
github.com/jlaffaye/ftp v0.2.0
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
|
golang.org/x/crypto v0.28.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.5 // indirect
|
|
||||||
github.com/aws/aws-sdk-go v1.55.5 // indirect
|
|
||||||
github.com/bramvdbogaerde/go-scp v1.5.0 // indirect
|
|
||||||
github.com/cloudflare/circl v1.5.0 // indirect
|
github.com/cloudflare/circl v1.5.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jlaffaye/ftp v0.2.0 // indirect
|
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
golang.org/x/crypto v0.29.0 // indirect
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
golang.org/x/sys v0.27.0 // indirect
|
golang.org/x/text v0.19.0 // indirect
|
||||||
golang.org/x/text v0.20.0 // indirect
|
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
gopkg.in/mail.v2 v2.3.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -56,6 +56,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
|
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||||
|
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
@@ -78,6 +80,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||||
|
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
@@ -93,6 +97,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||||
|
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ package internal
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/jkaninda/encryptor"
|
"github.com/jkaninda/encryptor"
|
||||||
"github.com/jkaninda/go-storage/pkg/ftp"
|
|
||||||
"github.com/jkaninda/go-storage/pkg/local"
|
|
||||||
"github.com/jkaninda/go-storage/pkg/s3"
|
|
||||||
"github.com/jkaninda/go-storage/pkg/ssh"
|
|
||||||
"github.com/jkaninda/pg-bkup/pkg/logger"
|
"github.com/jkaninda/pg-bkup/pkg/logger"
|
||||||
|
"github.com/jkaninda/pg-bkup/pkg/storage/ftp"
|
||||||
|
"github.com/jkaninda/pg-bkup/pkg/storage/local"
|
||||||
|
"github.com/jkaninda/pg-bkup/pkg/storage/s3"
|
||||||
|
"github.com/jkaninda/pg-bkup/pkg/storage/ssh"
|
||||||
"github.com/jkaninda/pg-bkup/utils"
|
"github.com/jkaninda/pg-bkup/utils"
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -51,7 +50,7 @@ func scheduledMode(db *dbConfig, config *BackupConfig) {
|
|||||||
|
|
||||||
// Test backup
|
// Test backup
|
||||||
logger.Info("Testing backup configurations...")
|
logger.Info("Testing backup configurations...")
|
||||||
BackupTask(db, config)
|
testDatabaseConnection(db)
|
||||||
logger.Info("Testing backup configurations...done")
|
logger.Info("Testing backup configurations...done")
|
||||||
logger.Info("Creating backup job...")
|
logger.Info("Creating backup job...")
|
||||||
// Create a new cron instance
|
// Create a new cron instance
|
||||||
@@ -116,6 +115,9 @@ func startMultiBackup(bkConfig *BackupConfig, configFile string) {
|
|||||||
if conf.CronExpression != "" {
|
if conf.CronExpression != "" {
|
||||||
bkConfig.cronExpression = conf.CronExpression
|
bkConfig.cronExpression = conf.CronExpression
|
||||||
}
|
}
|
||||||
|
if len(conf.Databases) == 0 {
|
||||||
|
logger.Fatal("No databases found")
|
||||||
|
}
|
||||||
// Check if cronExpression is defined
|
// Check if cronExpression is defined
|
||||||
if bkConfig.cronExpression == "" {
|
if bkConfig.cronExpression == "" {
|
||||||
multiBackupTask(conf.Databases, bkConfig)
|
multiBackupTask(conf.Databases, bkConfig)
|
||||||
@@ -129,7 +131,9 @@ func startMultiBackup(bkConfig *BackupConfig, configFile string) {
|
|||||||
|
|
||||||
// Test backup
|
// Test backup
|
||||||
logger.Info("Testing backup configurations...")
|
logger.Info("Testing backup configurations...")
|
||||||
multiBackupTask(conf.Databases, bkConfig)
|
for _, db := range conf.Databases {
|
||||||
|
testDatabaseConnection(getDatabase(db))
|
||||||
|
}
|
||||||
logger.Info("Testing backup configurations...done")
|
logger.Info("Testing backup configurations...done")
|
||||||
logger.Info("Creating backup job...")
|
logger.Info("Creating backup job...")
|
||||||
// Create a new cron instance
|
// Create a new cron instance
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func intro() {
|
func intro() {
|
||||||
logger.Info("Starting PostgreSQL Backup...")
|
fmt.Println("Starting PostgreSQL Backup...")
|
||||||
logger.Info("Copyright (c) 2024 Jonas Kaninda ")
|
fmt.Println("Copyright (c) 2024 Jonas Kaninda ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyToTmp copy file to temporary directory
|
// copyToTmp copy file to temporary directory
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/jkaninda/pg-bkup/pkg/logger"
|
"github.com/jkaninda/pg-bkup/pkg/logger"
|
||||||
|
"github.com/jkaninda/pg-bkup/pkg/storage/ftp"
|
||||||
|
"github.com/jkaninda/pg-bkup/pkg/storage/local"
|
||||||
|
"github.com/jkaninda/pg-bkup/pkg/storage/s3"
|
||||||
|
"github.com/jkaninda/pg-bkup/pkg/storage/ssh"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/jkaninda/encryptor"
|
"github.com/jkaninda/encryptor"
|
||||||
"github.com/jkaninda/go-storage/pkg/ftp"
|
|
||||||
"github.com/jkaninda/go-storage/pkg/local"
|
|
||||||
"github.com/jkaninda/go-storage/pkg/s3"
|
|
||||||
"github.com/jkaninda/go-storage/pkg/ssh"
|
|
||||||
"github.com/jkaninda/pg-bkup/utils"
|
"github.com/jkaninda/pg-bkup/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|||||||
142
pkg/storage/ftp/ftp.go
Normal file
142
pkg/storage/ftp/ftp.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package ftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
pkg "github.com/jkaninda/pg-bkup/pkg/storage"
|
||||||
|
"github.com/jlaffaye/ftp"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ftpStorage struct {
|
||||||
|
*pkg.Backend
|
||||||
|
client *ftp.ServerConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config holds the SSH connection details
|
||||||
|
type Config struct {
|
||||||
|
Host string
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
Port string
|
||||||
|
LocalPath string
|
||||||
|
RemotePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// createClient creates FTP Client
|
||||||
|
func createClient(conf Config) (*ftp.ServerConn, error) {
|
||||||
|
ftpClient, err := ftp.Dial(fmt.Sprintf("%s:%s", conf.Host, conf.Port), ftp.DialWithTimeout(5*time.Second))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to FTP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ftpClient.Login(conf.User, conf.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to log in to FTP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ftpClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStorage creates new Storage
|
||||||
|
func NewStorage(conf Config) (pkg.Storage, error) {
|
||||||
|
client, err := createClient(conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ftpStorage{
|
||||||
|
client: client,
|
||||||
|
Backend: &pkg.Backend{
|
||||||
|
RemotePath: conf.RemotePath,
|
||||||
|
LocalPath: conf.LocalPath,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies file to the remote server
|
||||||
|
func (s ftpStorage) Copy(fileName string) error {
|
||||||
|
ftpClient := s.client
|
||||||
|
defer func(ftpClient *ftp.ServerConn) {
|
||||||
|
err := ftpClient.Quit()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(ftpClient)
|
||||||
|
|
||||||
|
filePath := filepath.Join(s.LocalPath, fileName)
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open file %s: %w", fileName, err)
|
||||||
|
}
|
||||||
|
defer func(file *os.File) {
|
||||||
|
err := file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(file)
|
||||||
|
|
||||||
|
remoteFilePath := filepath.Join(s.RemotePath, fileName)
|
||||||
|
err = ftpClient.Stor(remoteFilePath, file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to upload file %s: %w", filepath.Join(s.LocalPath, fileName), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyFrom copies a file from the remote server to local storage
|
||||||
|
func (s ftpStorage) CopyFrom(fileName string) error {
|
||||||
|
ftpClient := s.client
|
||||||
|
|
||||||
|
defer func(ftpClient *ftp.ServerConn) {
|
||||||
|
err := ftpClient.Quit()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(ftpClient)
|
||||||
|
|
||||||
|
remoteFilePath := filepath.Join(s.RemotePath, fileName)
|
||||||
|
r, err := ftpClient.Retr(remoteFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve file %s: %w", fileName, err)
|
||||||
|
}
|
||||||
|
defer func(r *ftp.Response) {
|
||||||
|
err := r.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(r)
|
||||||
|
|
||||||
|
localFilePath := filepath.Join(s.LocalPath, fileName)
|
||||||
|
outFile, err := os.Create(localFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create local file %s: %w", fileName, err)
|
||||||
|
}
|
||||||
|
defer func(outFile *os.File) {
|
||||||
|
err := outFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(outFile)
|
||||||
|
|
||||||
|
_, err = io.Copy(outFile, r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to copy data to local file %s: %w", fileName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune deletes old backup created more than specified days
|
||||||
|
func (s ftpStorage) Prune(retentionDays int) error {
|
||||||
|
fmt.Println("Deleting old backup from a remote server is not implemented yet")
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the storage name
|
||||||
|
func (s ftpStorage) Name() string {
|
||||||
|
return "ftp"
|
||||||
|
}
|
||||||
116
pkg/storage/local/local.go
Normal file
116
pkg/storage/local/local.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
pkg "github.com/jkaninda/pg-bkup/pkg/storage"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type localStorage struct {
|
||||||
|
*pkg.Backend
|
||||||
|
}
|
||||||
|
type Config struct {
|
||||||
|
LocalPath string
|
||||||
|
RemotePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStorage creates new Storage
|
||||||
|
func NewStorage(conf Config) pkg.Storage {
|
||||||
|
return &localStorage{
|
||||||
|
Backend: &pkg.Backend{
|
||||||
|
LocalPath: conf.LocalPath,
|
||||||
|
RemotePath: conf.RemotePath,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies file to the local destination path
|
||||||
|
func (l localStorage) Copy(file string) error {
|
||||||
|
if _, err := os.Stat(filepath.Join(l.LocalPath, file)); os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err := copyFile(filepath.Join(l.LocalPath, file), filepath.Join(l.RemotePath, file))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyFrom copies file from a Path to local path
|
||||||
|
func (l localStorage) CopyFrom(file string) error {
|
||||||
|
if _, err := os.Stat(filepath.Join(l.RemotePath, file)); os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err := copyFile(filepath.Join(l.RemotePath, file), filepath.Join(l.LocalPath, file))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune deletes old backup created more than specified days
|
||||||
|
func (l localStorage) Prune(retentionDays int) error {
|
||||||
|
currentTime := time.Now()
|
||||||
|
// Delete file
|
||||||
|
deleteFile := func(filePath string) error {
|
||||||
|
err := os.Remove(filePath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Walk through the directory and delete files modified more than specified days ago
|
||||||
|
err := filepath.Walk(l.RemotePath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Check if it's a regular file and if it was modified more than specified days ago
|
||||||
|
if fileInfo.Mode().IsRegular() {
|
||||||
|
timeDiff := currentTime.Sub(fileInfo.ModTime())
|
||||||
|
if timeDiff.Hours() > 24*float64(retentionDays) {
|
||||||
|
err := deleteFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the storage name
|
||||||
|
func (l localStorage) Name() string {
|
||||||
|
return "local"
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyFile copies file
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func(in *os.File) {
|
||||||
|
err := in.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(in)
|
||||||
|
|
||||||
|
out, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(out, in)
|
||||||
|
if err != nil {
|
||||||
|
err := out.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return out.Close()
|
||||||
|
}
|
||||||
66
pkg/storage/local/local_test.go
Normal file
66
pkg/storage/local/local_test.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const content = "Lorem ipsum dolor sit amet. Eum eius voluptas sit vitae vitae aut sequi molestias hic accusamus consequatur"
|
||||||
|
const inputFile = "file.txt"
|
||||||
|
const localPath = "./tests/local"
|
||||||
|
const RemotePath = "./tests/remote"
|
||||||
|
|
||||||
|
func TestCopy(t *testing.T) {
|
||||||
|
|
||||||
|
err := os.MkdirAll(localPath, 0777)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
err = os.MkdirAll(RemotePath, 0777)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = createFile(filepath.Join(localPath, inputFile), content)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l := NewStorage(Config{
|
||||||
|
LocalPath: "./tests/local",
|
||||||
|
RemotePath: "./tests/remote",
|
||||||
|
})
|
||||||
|
err = l.Copy(inputFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("File copied to %s\n", filepath.Join(RemotePath, inputFile))
|
||||||
|
}
|
||||||
|
func createFile(fileName, content string) ([]byte, error) {
|
||||||
|
// Create a file named hello.txt
|
||||||
|
file, err := os.Create(fileName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error creating file:", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func(file *os.File) {
|
||||||
|
err := file.Close()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error closing file:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(file)
|
||||||
|
|
||||||
|
// Write the message to the file
|
||||||
|
_, err = file.WriteString(content)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error writing to file:", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Successfully wrote to %s\n", fileName)
|
||||||
|
fileBytes, err := os.ReadFile(fileName)
|
||||||
|
return fileBytes, err
|
||||||
|
}
|
||||||
176
pkg/storage/s3/s3.go
Normal file
176
pkg/storage/s3/s3.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package s3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
"github.com/aws/aws-sdk-go/service/s3"
|
||||||
|
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||||
|
pkg "github.com/jkaninda/pg-bkup/pkg/storage"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type s3Storage struct {
|
||||||
|
*pkg.Backend
|
||||||
|
client *session.Session
|
||||||
|
bucket string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config holds the AWS S3 config
|
||||||
|
type Config struct {
|
||||||
|
Endpoint string
|
||||||
|
Bucket string
|
||||||
|
AccessKey string
|
||||||
|
SecretKey string
|
||||||
|
Region string
|
||||||
|
DisableSsl bool
|
||||||
|
ForcePathStyle bool
|
||||||
|
LocalPath string
|
||||||
|
RemotePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSession creates a new AWS session
|
||||||
|
func createSession(conf Config) (*session.Session, error) {
|
||||||
|
s3Config := &aws.Config{
|
||||||
|
Credentials: credentials.NewStaticCredentials(conf.AccessKey, conf.SecretKey, ""),
|
||||||
|
Endpoint: aws.String(conf.Endpoint),
|
||||||
|
Region: aws.String(conf.Region),
|
||||||
|
DisableSSL: aws.Bool(conf.DisableSsl),
|
||||||
|
S3ForcePathStyle: aws.Bool(conf.ForcePathStyle),
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.NewSession(s3Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStorage creates new Storage
|
||||||
|
func NewStorage(conf Config) (pkg.Storage, error) {
|
||||||
|
sess, err := createSession(conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &s3Storage{
|
||||||
|
client: sess,
|
||||||
|
bucket: conf.Bucket,
|
||||||
|
Backend: &pkg.Backend{
|
||||||
|
RemotePath: conf.RemotePath,
|
||||||
|
LocalPath: conf.LocalPath,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies file to S3 storage
|
||||||
|
func (s s3Storage) Copy(fileName string) error {
|
||||||
|
svc := s3.New(s.client)
|
||||||
|
file, err := os.Open(filepath.Join(s.LocalPath, fileName))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func(file *os.File) {
|
||||||
|
err := file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(file)
|
||||||
|
|
||||||
|
fileInfo, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
objectKey := filepath.Join(s.RemotePath, fileName)
|
||||||
|
buffer := make([]byte, fileInfo.Size())
|
||||||
|
_, err = file.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fileBytes := bytes.NewReader(buffer)
|
||||||
|
fileType := http.DetectContentType(buffer)
|
||||||
|
|
||||||
|
_, err = svc.PutObject(&s3.PutObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(objectKey),
|
||||||
|
Body: fileBytes,
|
||||||
|
ContentLength: aws.Int64(fileInfo.Size()),
|
||||||
|
ContentType: aws.String(fileType),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyFrom copies a file from S3 to local storage
|
||||||
|
func (s s3Storage) CopyFrom(fileName string) error {
|
||||||
|
file, err := os.Create(filepath.Join(s.LocalPath, fileName))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func(file *os.File) {
|
||||||
|
err := file.Close()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error closing file: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(file)
|
||||||
|
|
||||||
|
objectKey := filepath.Join(s.RemotePath, fileName)
|
||||||
|
|
||||||
|
downloader := s3manager.NewDownloader(s.client)
|
||||||
|
_, err = downloader.Download(file,
|
||||||
|
&s3.GetObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(objectKey),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune deletes old backup created more than specified days
|
||||||
|
func (s s3Storage) Prune(retentionDays int) error {
|
||||||
|
svc := s3.New(s.client)
|
||||||
|
|
||||||
|
// Get the current time
|
||||||
|
now := time.Now()
|
||||||
|
backupRetentionDays := now.AddDate(0, 0, -retentionDays)
|
||||||
|
|
||||||
|
// List objects in the bucket
|
||||||
|
listObjectsInput := &s3.ListObjectsV2Input{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Prefix: aws.String(s.RemotePath),
|
||||||
|
}
|
||||||
|
err := svc.ListObjectsV2Pages(listObjectsInput, func(page *s3.ListObjectsV2Output, lastPage bool) bool {
|
||||||
|
for _, object := range page.Contents {
|
||||||
|
if object.LastModified.Before(backupRetentionDays) {
|
||||||
|
// Object is older than retention days, delete it
|
||||||
|
_, err := svc.DeleteObject(&s3.DeleteObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: object.Key,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to delete object %s: %v", *object.Key, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Deleted object %s", *object.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !lastPage
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list objects: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the storage name
|
||||||
|
func (s s3Storage) Name() string {
|
||||||
|
return "s3"
|
||||||
|
}
|
||||||
124
pkg/storage/ssh/ssh.go
Normal file
124
pkg/storage/ssh/ssh.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/bramvdbogaerde/go-scp"
|
||||||
|
"github.com/bramvdbogaerde/go-scp/auth"
|
||||||
|
pkg "github.com/jkaninda/pg-bkup/pkg/storage"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sshStorage struct {
|
||||||
|
*pkg.Backend
|
||||||
|
client scp.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config holds the SSH connection details
|
||||||
|
type Config struct {
|
||||||
|
Host string
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
Port string
|
||||||
|
IdentifyFile string
|
||||||
|
LocalPath string
|
||||||
|
RemotePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// createClient creates SSH Client
|
||||||
|
func createClient(conf Config) (scp.Client, error) {
|
||||||
|
if _, err := os.Stat(conf.IdentifyFile); os.IsNotExist(err) {
|
||||||
|
clientConfig, err := auth.PrivateKey(conf.User, conf.IdentifyFile, ssh.InsecureIgnoreHostKey())
|
||||||
|
return scp.NewClient(fmt.Sprintf("%s:%s", conf.Host, conf.Port), &clientConfig), err
|
||||||
|
} else {
|
||||||
|
if conf.Password == "" {
|
||||||
|
return scp.Client{}, errors.New("ssh password required")
|
||||||
|
}
|
||||||
|
clientConfig, err := auth.PasswordKey(conf.User, conf.Password, ssh.InsecureIgnoreHostKey())
|
||||||
|
return scp.NewClient(fmt.Sprintf("%s:%s", conf.Host, conf.Port), &clientConfig), err
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStorage creates new Storage
|
||||||
|
func NewStorage(conf Config) (pkg.Storage, error) {
|
||||||
|
client, err := createClient(conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sshStorage{
|
||||||
|
client: client,
|
||||||
|
Backend: &pkg.Backend{
|
||||||
|
RemotePath: conf.RemotePath,
|
||||||
|
LocalPath: conf.LocalPath,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies file to the remote server
|
||||||
|
func (s sshStorage) Copy(fileName string) error {
|
||||||
|
client := s.client
|
||||||
|
// Connect to the remote server
|
||||||
|
err := client.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("couldn't establish a connection to the remote server")
|
||||||
|
}
|
||||||
|
// Open the local file
|
||||||
|
filePath := filepath.Join(s.LocalPath, fileName)
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open file %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
// Copy file to the remote server
|
||||||
|
err = client.CopyFromFile(context.Background(), *file, filepath.Join(s.RemotePath, fileName), "0655")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to copy file to remote server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyFrom copies a file from the remote server to local storage
|
||||||
|
func (s sshStorage) CopyFrom(fileName string) error {
|
||||||
|
// Create a new SCP client
|
||||||
|
client := s.client
|
||||||
|
// Connect to the remote server
|
||||||
|
err := client.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("couldn't establish a connection to the remote server")
|
||||||
|
}
|
||||||
|
// Close client connection after the file has been copied
|
||||||
|
defer client.Close()
|
||||||
|
file, err := os.OpenFile(filepath.Join(s.LocalPath, fileName), os.O_RDWR|os.O_CREATE, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("couldn't open the output file")
|
||||||
|
}
|
||||||
|
defer func(file *os.File) {
|
||||||
|
err := file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(file)
|
||||||
|
|
||||||
|
err = client.CopyFromRemote(context.Background(), file, filepath.Join(s.RemotePath, fileName))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune deletes old backup created more than specified days
|
||||||
|
func (s sshStorage) Prune(retentionDays int) error {
|
||||||
|
fmt.Println("Deleting old backup from a remote server is not implemented yet")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the storage name
|
||||||
|
func (s sshStorage) Name() string {
|
||||||
|
return "ssh"
|
||||||
|
}
|
||||||
14
pkg/storage/storage.go
Normal file
14
pkg/storage/storage.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package pkg
|
||||||
|
|
||||||
|
type Storage interface {
|
||||||
|
Copy(fileName string) error
|
||||||
|
CopyFrom(fileName string) error
|
||||||
|
Prune(retentionDays int) error
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
type Backend struct {
|
||||||
|
// Local Path
|
||||||
|
LocalPath string
|
||||||
|
// Remote path or Destination path
|
||||||
|
RemotePath string
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
[✅ Database Backup Notification – {{.Database}}
|
✅ Database Backup Notification – {{.Database}}
|
||||||
Hi,
|
Hi,
|
||||||
Backup of the {{.Database}} database has been successfully completed on {{.EndTime}}.
|
Backup of the {{.Database}} database has been successfully completed on {{.EndTime}}.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user