Compare commits

...

31 Commits

Author SHA1 Message Date
abf1aeba7c chore: add convert bytes to a human-readable string with the appropriate unit (bytes, MiB, or GiB)
All checks were successful
Lint / Run on Ubuntu (push) Successful in 20m39s
Lint / Run on Ubuntu (pull_request) Successful in 20m14s
2024-12-12 12:43:06 +01:00
ad2f3a1990 Merge pull request #151 from jkaninda/refactor
docs: update features
2024-12-10 10:15:34 +01:00
0edee0b703 docs: update features 2024-12-10 10:14:43 +01:00
fdb79d3cef Merge pull request #150 from jkaninda/refactor
chore: add convert backup size from bytes to Mib
2024-12-10 10:13:30 +01:00
01915039e7 Merge branch 'main' of github.com:jkaninda/pg-bkup into refactor 2024-12-10 10:12:27 +01:00
f9dbd0b85c Merge pull request #148 from jkaninda/develop
feat: add support Database source jdbc uri format
2024-12-10 10:11:31 +01:00
409f9e91f2 Merge pull request #149 from jkaninda/dependabot/docker/golang-1.23.4
chore(deps): bump golang from 1.23.3 to 1.23.4
2024-12-10 10:11:07 +01:00
93f9595464 chore: remove db port from required vars 2024-12-10 10:10:33 +01:00
c89411cfa6 chore: add convert backup size from bytes to Mib 2024-12-10 09:58:39 +01:00
6ec480acda Merge branch 'main' of github.com:jkaninda/pg-bkup into refactor 2024-12-10 09:48:07 +01:00
dependabot[bot]
0501c151a4 chore(deps): bump golang from 1.23.3 to 1.23.4
Bumps golang from 1.23.3 to 1.23.4.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 06:56:30 +00:00
91a440035d refactor: refactoring of code
All checks were successful
Lint / Run on Ubuntu (push) Successful in 20m9s
2024-12-08 13:39:54 +01:00
1a6e2e4ffc feat: add Database source jdbc uri format 2024-12-08 13:33:32 +01:00
3009747874 Merge pull request #147 from jkaninda/refactor
Fix grammar issues in azure.go
2024-12-07 23:59:27 +01:00
99f76eb5d6 Fix grammar issues in azure.go 2024-12-07 23:58:48 +01:00
5a65c51f54 Merge pull request #146 from jkaninda/develop
fix: SSH storage key identify file
2024-12-07 20:38:38 +01:00
324c5df69c fix: SSH storage key identitify file 2024-12-07 20:27:09 +01:00
6f854d5165 Merge pull request #145 from jkaninda/develop
chore: update notification template
2024-12-07 18:00:53 +01:00
5cca957009 chore: update notification template 2024-12-07 16:50:08 +01:00
05003b386e Merge pull request #144 from jkaninda/refactor
chore: correct pg name
2024-12-07 03:48:50 +01:00
Jonas Kaninda
f7989a865d chore: correct pg name 2024-12-07 03:47:55 +01:00
47c7afda4e Merge pull request #143 from jkaninda/refactor
chore: update .env.example
2024-12-07 03:28:34 +01:00
Jonas Kaninda
a9196e6e51 chore: update .env.example 2024-12-07 03:28:12 +01:00
fc6325e5b6 Merge pull request #142 from jkaninda/refactor
chore: update .env.example
2024-12-07 03:27:40 +01:00
Jonas Kaninda
b8fabf2bd8 chore: update .env.example 2024-12-07 03:26:45 +01:00
cbeb8b0ba2 Merge pull request #141 from jkaninda/refactor
chore: update base image
2024-12-07 02:32:14 +01:00
Jonas Kaninda
c644aa60d2 chore: update base image 2024-12-07 02:31:45 +01:00
4904e8d0a6 Merge pull request #140 from jkaninda/refactor
refactor: clean up code
2024-12-07 02:30:39 +01:00
Jonas Kaninda
1eb57044ad refactor: clean up code 2024-12-07 02:28:16 +01:00
803b077b3d Merge pull request #139 from jkaninda/refactor
fix: fix s3 remote path when backing up multiple databases
2024-12-07 02:11:34 +01:00
6503e4b5bd Merge pull request #138 from jkaninda/refactor
feat: add Azure Blob storage
2024-12-06 21:36:05 +01:00
24 changed files with 379 additions and 169 deletions

View File

@@ -15,6 +15,7 @@ TZ=Europe/Paris
### Backup restoration ### Backup restoration
#FILE_NAME= #FILE_NAME=
### AWS S3 Storage ### AWS S3 Storage
#ACCESS_KEY= #ACCESS_KEY=
#SECRET_KEY= #SECRET_KEY=
@@ -43,19 +44,30 @@ TZ=Europe/Paris
#FTP_USER= #FTP_USER=
#FTP_PORT=21 #FTP_PORT=21
#REMOTE_PATH= #REMOTE_PATH=
## Azure Blob Storage
AZURE_STORAGE_CONTAINER_NAME=
AZURE_STORAGE_ACCOUNT_NAME=
AZURE_STORAGE_ACCOUNT_KEY=
#### Backup encryption #### Backup encryption
#GPG_PUBLIC_KEY=/config/public_key.asc #GPG_PUBLIC_KEY=/config/public_key.asc
#GPG_PRIVATE_KEY=/config/private_key.asc #GPG_PRIVATE_KEY=/config/private_key.asc
#GPG_PASSPHRASE=Your strong passphrase #GPG_PASSPHRASE=Your strong passphrase
## For multiple database backup on Docker or Docker in Swarm mode ## For multiple database backup on Docker or Docker in Swarm mode
#BACKUP_CONFIG_FILE=/config/config.yaml #BACKUP_CONFIG_FILE=/config/config.yaml
### Database restoration ### Database restoration
#FILE_NAME= #FILE_NAME=
### Notification ### Notification
#BACKUP_REFERENCE=K8s/Paris cluster #BACKUP_REFERENCE=K8s/Paris cluster
## Telegram ## Telegram
#TG_TOKEN= #TG_TOKEN=
#TG_CHAT_ID= #TG_CHAT_ID=
### Email ### Email
#MAIL_HOST= #MAIL_HOST=
#MAIL_PORT= #MAIL_PORT=

View File

@@ -1,4 +1,4 @@
FROM golang:1.23.3 AS build FROM golang:1.23.4 AS build
WORKDIR /app WORKDIR /app
ARG appVersion="" ARG appVersion=""
# Copy the source code. # Copy the source code.
@@ -9,7 +9,7 @@ RUN go mod download
# Build # Build
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-X 'github.com/jkaninda/pg-bkup/utils.Version=${appVersion}'" -o /app/pg-bkup RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-X 'github.com/jkaninda/pg-bkup/utils.Version=${appVersion}'" -o /app/pg-bkup
FROM alpine:3.20.3 FROM alpine:3.21.0
ENV TZ=UTC ENV TZ=UTC
ARG WORKDIR="/config" ARG WORKDIR="/config"
ARG BACKUPDIR="/backup" ARG BACKUPDIR="/backup"

View File

@@ -38,6 +38,8 @@ services:
- DB_NAME=database - DB_NAME=database
- DB_USERNAME=username - DB_USERNAME=username
- DB_PASSWORD=password - DB_PASSWORD=password
# You can also use JDBC format
#- DB_URL=jdbc:postgresql://postgres:5432/database?user=user&password=password
# pg-bkup container must be connected to the same network with your database # pg-bkup container must be connected to the same network with your database
networks: networks:
- web - web
@@ -67,7 +69,7 @@ services:
# for a list of available releases. # for a list of available releases.
image: jkaninda/pg-bkup image: jkaninda/pg-bkup
container_name: pg-bkup container_name: pg-bkup
command: backup -d database --cron-expression "0 1 * * *" command: backup -d database --cron-expression @midnight
volumes: volumes:
- ./backup:/backup - ./backup:/backup
environment: environment:
@@ -76,7 +78,9 @@ services:
- DB_NAME=database - DB_NAME=database
- DB_USERNAME=username - DB_USERNAME=username
- DB_PASSWORD=password - DB_PASSWORD=password
- BACKUP_CRON_EXPRESSION=0 1 * * * - BACKUP_CRON_EXPRESSION=@midnight
# You can also use JDBC format
#- DB_URL=jdbc:postgresql://postgres:5432/database?user=user&password=password
#Delete old backup created more than specified days ago #Delete old backup created more than specified days ago
#- BACKUP_RETENTION_DAYS=7 #- BACKUP_RETENTION_DAYS=7
# pg-bkup container must be connected to the same network with your database # pg-bkup container must be connected to the same network with your database

View File

@@ -38,12 +38,16 @@ services:
- DB_NAME=database - DB_NAME=database
- DB_USERNAME=username - DB_USERNAME=username
- DB_PASSWORD=password - DB_PASSWORD=password
# You can also use JDBC format
#- DB_URL=jdbc:postgresql://postgres:5432/database?user=username&password=password
## Target database ## Target database
- TARGET_DB_HOST=target-postgres - TARGET_DB_HOST=target-postgres
- TARGET_DB_PORT=5432 - TARGET_DB_PORT=5432
- TARGET_DB_NAME=dbname - TARGET_DB_NAME=dbname
- TARGET_DB_USERNAME=username - TARGET_DB_USERNAME=username
- TARGET_DB_PASSWORD=password - TARGET_DB_PASSWORD=password
# You can also use JDBC format
#- TARGET_DB_URL=jdbc:postgresql://target-postgres:5432/dbname?user=username&password=password
# mysql-bkup container must be connected to the same network with your database # mysql-bkup container must be connected to the same network with your database
networks: networks:
- web - web

View File

@@ -17,6 +17,7 @@ It supports a variety of storage options and ensures data security through GPG e
- AWS S3 or any S3-compatible object storage - AWS S3 or any S3-compatible object storage
- FTP - FTP
- SSH-compatible storage - SSH-compatible storage
- Azure Blob storage
- **Data Security:** - **Data Security:**
- Backups can be encrypted using **GPG** to ensure confidentiality. - Backups can be encrypted using **GPG** to ensure confidentiality.

View File

@@ -32,43 +32,48 @@ Backup, restore and migrate targets, schedule and retention are configured using
## Environment variables ## Environment variables
| Name | Requirement | Description | | Name | Requirement | Description |
|------------------------|---------------------------------------------------------------|-----------------------------------------------------------------| |------------------------------|---------------------------------------------------------------|-----------------------------------------------------------------|
| DB_PORT | Optional, default 5432 | Database port number | | DB_PORT | Optional, default 5432 | Database port number |
| DB_HOST | Required | Database host | | DB_HOST | Required | Database host |
| DB_NAME | Optional if it was provided from the -d flag | Database name | | DB_NAME | Optional if it was provided from the -d flag | Database name |
| DB_USERNAME | Required | Database user name | | DB_USERNAME | Required | Database user name |
| DB_PASSWORD | Required | Database password | | DB_PASSWORD | Required | Database password |
| AWS_ACCESS_KEY | Optional, required for S3 storage | AWS S3 Access Key | | DB_URL | Optional | Database URL in JDBC URI format |
| AWS_SECRET_KEY | Optional, required for S3 storage | AWS S3 Secret Key | | AWS_ACCESS_KEY | Optional, required for S3 storage | AWS S3 Access Key |
| AWS_BUCKET_NAME | Optional, required for S3 storage | AWS S3 Bucket Name | | AWS_SECRET_KEY | Optional, required for S3 storage | AWS S3 Secret Key |
| AWS_BUCKET_NAME | Optional, required for S3 storage | AWS S3 Bucket Name | | AWS_BUCKET_NAME | Optional, required for S3 storage | AWS S3 Bucket Name |
| AWS_REGION | Optional, required for S3 storage | AWS Region | | AWS_BUCKET_NAME | Optional, required for S3 storage | AWS S3 Bucket Name |
| AWS_DISABLE_SSL | Optional, required for S3 storage | Disable SSL | | AWS_REGION | Optional, required for S3 storage | AWS Region |
| AWS_FORCE_PATH_STYLE | Optional, required for S3 storage | Force path style | | AWS_DISABLE_SSL | Optional, required for S3 storage | Disable SSL |
| FILE_NAME | Optional if it was provided from the --file flag | Database file to restore (extensions: .sql, .sql.gz) | | AWS_FORCE_PATH_STYLE | Optional, required for S3 storage | Force path style |
| GPG_PASSPHRASE | Optional, required to encrypt and restore backup | GPG passphrase | | FILE_NAME | Optional if it was provided from the --file flag | Database file to restore (extensions: .sql, .sql.gz) |
| GPG_PUBLIC_KEY | Optional, required to encrypt backup | GPG public key, used to encrypt backup (/config/public_key.asc) | | GPG_PASSPHRASE | Optional, required to encrypt and restore backup | GPG passphrase |
| BACKUP_CRON_EXPRESSION | Optional if it was provided from the `--cron-expression` flag | Backup cron expression for docker in scheduled mode | | GPG_PUBLIC_KEY | Optional, required to encrypt backup | GPG public key, used to encrypt backup (/config/public_key.asc) |
| BACKUP_RETENTION_DAYS | Optional | Delete old backup created more than specified days ago | | BACKUP_CRON_EXPRESSION | Optional if it was provided from the `--cron-expression` flag | Backup cron expression for docker in scheduled mode |
| SSH_HOST | Optional, required for SSH storage | ssh remote hostname or ip | | BACKUP_RETENTION_DAYS | Optional | Delete old backup created more than specified days ago |
| SSH_USER | Optional, required for SSH storage | ssh remote user | | SSH_HOST | Optional, required for SSH storage | ssh remote hostname or ip |
| SSH_PASSWORD | Optional, required for SSH storage | ssh remote user's password | | SSH_USER | Optional, required for SSH storage | ssh remote user |
| SSH_IDENTIFY_FILE | Optional, required for SSH storage | ssh remote user's private key | | SSH_PASSWORD | Optional, required for SSH storage | ssh remote user's password |
| SSH_PORT | Optional, required for SSH storage | ssh remote server port | | SSH_IDENTIFY_FILE | Optional, required for SSH storage | ssh remote user's private key |
| REMOTE_PATH | Optional, required for SSH or FTP storage | remote path (/home/toto/backup) | | SSH_PORT | Optional, required for SSH storage | ssh remote server port |
| FTP_HOST | Optional, required for FTP storage | FTP host name | | REMOTE_PATH | Optional, required for SSH or FTP storage | remote path (/home/toto/backup) |
| FTP_PORT | Optional, required for FTP storage | FTP server port number | | FTP_HOST | Optional, required for FTP storage | FTP host name |
| FTP_USER | Optional, required for FTP storage | FTP user | | FTP_PORT | Optional, required for FTP storage | FTP server port number |
| FTP_PASSWORD | Optional, required for FTP storage | FTP user password | | FTP_USER | Optional, required for FTP storage | FTP user |
| TARGET_DB_HOST | Optional, required for database migration | Target database host | | FTP_PASSWORD | Optional, required for FTP storage | FTP user password |
| TARGET_DB_PORT | Optional, required for database migration | Target database port | | TARGET_DB_HOST | Optional, required for database migration | Target database host |
| TARGET_DB_NAME | Optional, required for database migration | Target database name | | TARGET_DB_PORT | Optional, required for database migration | Target database port |
| TARGET_DB_USERNAME | Optional, required for database migration | Target database username | | TARGET_DB_NAME | Optional, required for database migration | Target database name |
| TARGET_DB_PASSWORD | Optional, required for database migration | Target database password | | TARGET_DB_USERNAME | Optional, required for database migration | Target database username |
| TG_TOKEN | Optional, required for Telegram notification | Telegram token (`BOT-ID:BOT-TOKEN`) | | TARGET_DB_URL | Optional | Database URL in JDBC URI format |
| TG_CHAT_ID | Optional, required for Telegram notification | Telegram Chat ID | | TARGET_DB_PASSWORD | Optional, required for database migration | Target database password |
| TZ | Optional | Time Zone | | TG_TOKEN | Optional, required for Telegram notification | Telegram token (`BOT-ID:BOT-TOKEN`) |
| TG_CHAT_ID | Optional, required for Telegram notification | Telegram Chat ID |
| TZ | Optional | Time Zone |
| AZURE_STORAGE_CONTAINER_NAME | Optional, required for Azure Blob Storage storage | Azure storage container name |
| AZURE_STORAGE_ACCOUNT_NAME | Optional, required for Azure Blob Storage storage | Azure storage account name |
| AZURE_STORAGE_ACCOUNT_KEY | Optional, required for Azure Blob Storage storage | Azure storage account key |
--- ---
## Run in Scheduled mode ## Run in Scheduled mode

2
go.mod
View File

@@ -5,7 +5,7 @@ go 1.23.2
require ( require (
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.2 github.com/jkaninda/go-storage v0.1.3
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
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1

2
go.sum
View File

@@ -46,6 +46,8 @@ github.com/jkaninda/encryptor v0.0.0-20241013064832-ed4bd6a1b221 h1:AwkCf7el1kze
github.com/jkaninda/encryptor v0.0.0-20241013064832-ed4bd6a1b221/go.mod h1:9F8ZJ+ZXE8DZBo77+aneGj8LMjrYXX6eFUCC/uqZOUo= github.com/jkaninda/encryptor v0.0.0-20241013064832-ed4bd6a1b221/go.mod h1:9F8ZJ+ZXE8DZBo77+aneGj8LMjrYXX6eFUCC/uqZOUo=
github.com/jkaninda/go-storage v0.1.2 h1:d7+TRPjmHXdSqO0wne3KAB8zt9ih8lf5D8aL4n7/Dds= github.com/jkaninda/go-storage v0.1.2 h1:d7+TRPjmHXdSqO0wne3KAB8zt9ih8lf5D8aL4n7/Dds=
github.com/jkaninda/go-storage v0.1.2/go.mod h1:zVRnLprBk/9AUz2+za6Y03MgoNYrqKLy3edVtjqMaps= github.com/jkaninda/go-storage v0.1.2/go.mod h1:zVRnLprBk/9AUz2+za6Y03MgoNYrqKLy3edVtjqMaps=
github.com/jkaninda/go-storage v0.1.3 h1:lEpHVgFLKSvjsi/6tAek96Y07za3vxmsXF2/+jiCMZU=
github.com/jkaninda/go-storage v0.1.3/go.mod h1:zVRnLprBk/9AUz2+za6Y03MgoNYrqKLy3edVtjqMaps=
github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg= github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI= github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=

View File

@@ -1,4 +1,3 @@
// Package main /
/* /*
MIT License MIT License
@@ -22,6 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
*/ */
package main package main
import "github.com/jkaninda/pg-bkup/cmd" import "github.com/jkaninda/pg-bkup/cmd"

View File

@@ -10,7 +10,7 @@ import (
) )
func azureBackup(db *dbConfig, config *BackupConfig) { func azureBackup(db *dbConfig, config *BackupConfig) {
utils.Info("Backup database to the remote FTP server") utils.Info("Backup database to Azure Blob Storage")
startTime = time.Now().Format(utils.TimeFormat()) startTime = time.Now().Format(utils.TimeFormat())
// Backup database // Backup database
@@ -57,13 +57,14 @@ func azureBackup(db *dbConfig, config *BackupConfig) {
} }
} }
utils.Info("Backup name is %s", finalFileName)
utils.Info("Backup size: %s", utils.ConvertBytes(uint64(backupSize)))
utils.Info("Uploading backup archive to Azure Blob storage ... done ") utils.Info("Uploading backup archive to Azure Blob storage ... done ")
// Send notification // Send notification
utils.NotifySuccess(&utils.NotificationData{ utils.NotifySuccess(&utils.NotificationData{
File: finalFileName, File: finalFileName,
BackupSize: backupSize, BackupSize: utils.ConvertBytes(uint64(backupSize)),
Database: db.dbName, Database: db.dbName,
Storage: config.storage, Storage: config.storage,
BackupLocation: filepath.Join(config.remotePath, finalFileName), BackupLocation: filepath.Join(config.remotePath, finalFileName),

View File

@@ -277,7 +277,6 @@ func localBackup(db *dbConfig, config *BackupConfig) {
utils.Error("Error: %s", err) utils.Error("Error: %s", err)
} }
backupSize = fileInfo.Size() backupSize = fileInfo.Size()
utils.Info("Backup name is %s", finalFileName)
localStorage := local.NewStorage(local.Config{ localStorage := local.NewStorage(local.Config{
LocalPath: tmpPath, LocalPath: tmpPath,
RemotePath: storagePath, RemotePath: storagePath,
@@ -286,11 +285,13 @@ func localBackup(db *dbConfig, config *BackupConfig) {
if err != nil { if err != nil {
utils.Fatal("Error copying backup file: %s", err) utils.Fatal("Error copying backup file: %s", err)
} }
utils.Info("Backup name is %s", finalFileName)
utils.Info("Backup size: %s", utils.ConvertBytes(uint64(backupSize)))
utils.Info("Backup saved in %s", filepath.Join(storagePath, finalFileName)) utils.Info("Backup saved in %s", filepath.Join(storagePath, finalFileName))
// Send notification // Send notification
utils.NotifySuccess(&utils.NotificationData{ utils.NotifySuccess(&utils.NotificationData{
File: finalFileName, File: finalFileName,
BackupSize: backupSize, BackupSize: utils.ConvertBytes(uint64(backupSize)),
Database: db.dbName, Database: db.dbName,
Storage: config.storage, Storage: config.storage,
BackupLocation: filepath.Join(storagePath, finalFileName), BackupLocation: filepath.Join(storagePath, finalFileName),

View File

@@ -80,7 +80,7 @@ type FTPConfig struct {
host string host string
user string user string
password string password string
port string port int
remotePath string remotePath string
} }
type AzureConfig struct { type AzureConfig struct {
@@ -94,7 +94,7 @@ type SSHConfig struct {
user string user string
password string password string
hostName string hostName string
port string port int
identifyFile string identifyFile string
} }
type AWSConfig struct { type AWSConfig struct {
@@ -109,6 +109,14 @@ type AWSConfig struct {
} }
func initDbConfig(cmd *cobra.Command) *dbConfig { func initDbConfig(cmd *cobra.Command) *dbConfig {
jdbcUri := os.Getenv("DB_URL")
if len(jdbcUri) != 0 {
config, err := convertJDBCToDbConfig(jdbcUri)
if err != nil {
utils.Fatal("Error: %v", err.Error())
}
return config
}
// Set env // Set env
utils.GetEnv(cmd, "dbname", "DB_NAME") utils.GetEnv(cmd, "dbname", "DB_NAME")
dConf := dbConfig{} dConf := dbConfig{}
@@ -149,7 +157,7 @@ func loadSSHConfig() (*SSHConfig, error) {
user: os.Getenv("SSH_USER"), user: os.Getenv("SSH_USER"),
password: os.Getenv("SSH_PASSWORD"), password: os.Getenv("SSH_PASSWORD"),
hostName: os.Getenv("SSH_HOST"), hostName: os.Getenv("SSH_HOST"),
port: os.Getenv("SSH_PORT"), port: utils.GetIntEnv("SSH_PORT"),
identifyFile: os.Getenv("SSH_IDENTIFY_FILE"), identifyFile: os.Getenv("SSH_IDENTIFY_FILE"),
}, nil }, nil
} }
@@ -159,7 +167,7 @@ func loadFtpConfig() *FTPConfig {
fConfig.host = utils.GetEnvVariable("FTP_HOST", "FTP_HOST_NAME") fConfig.host = utils.GetEnvVariable("FTP_HOST", "FTP_HOST_NAME")
fConfig.user = os.Getenv("FTP_USER") fConfig.user = os.Getenv("FTP_USER")
fConfig.password = os.Getenv("FTP_PASSWORD") fConfig.password = os.Getenv("FTP_PASSWORD")
fConfig.port = os.Getenv("FTP_PORT") fConfig.port = utils.GetIntEnv("FTP_PORT")
fConfig.remotePath = os.Getenv("REMOTE_PATH") fConfig.remotePath = os.Getenv("REMOTE_PATH")
err := utils.CheckEnvVars(ftpVars) err := utils.CheckEnvVars(ftpVars)
if err != nil { if err != nil {
@@ -293,6 +301,20 @@ func initRestoreConfig(cmd *cobra.Command) *RestoreConfig {
return &rConfig return &rConfig
} }
func initTargetDbConfig() *targetDbConfig { func initTargetDbConfig() *targetDbConfig {
jdbcUri := os.Getenv("TARGET_DB_URL")
if len(jdbcUri) != 0 {
config, err := convertJDBCToDbConfig(jdbcUri)
if err != nil {
utils.Fatal("Error: %v", err.Error())
}
return &targetDbConfig{
targetDbHost: config.dbHost,
targetDbPort: config.dbPort,
targetDbName: config.dbName,
targetDbPassword: config.dbPassword,
targetDbUserName: config.dbUserName,
}
}
tdbConfig := targetDbConfig{} tdbConfig := targetDbConfig{}
tdbConfig.targetDbHost = os.Getenv("TARGET_DB_HOST") tdbConfig.targetDbHost = os.Getenv("TARGET_DB_HOST")
tdbConfig.targetDbPort = utils.EnvWithDefault("TARGET_DB_PORT", "5432") tdbConfig.targetDbPort = utils.EnvWithDefault("TARGET_DB_PORT", "5432")

View File

@@ -29,6 +29,7 @@ import (
"fmt" "fmt"
"github.com/jkaninda/pg-bkup/utils" "github.com/jkaninda/pg-bkup/utils"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"net/url"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -36,7 +37,7 @@ import (
) )
func intro() { func intro() {
fmt.Println("Starting PosgreSQL Backup...") fmt.Println("Starting PostgresSQL Backup...")
fmt.Printf("Version: %s\n", utils.Version) fmt.Printf("Version: %s\n", utils.Version)
fmt.Println("Copyright (c) 2024 Jonas Kaninda") fmt.Println("Copyright (c) 2024 Jonas Kaninda")
} }
@@ -69,6 +70,9 @@ func deleteTemp() {
func testDatabaseConnection(db *dbConfig) { func testDatabaseConnection(db *dbConfig) {
utils.Info("Connecting to %s database ...", db.dbName) utils.Info("Connecting to %s database ...", db.dbName)
// Set database name for notification error
utils.DatabaseName = db.dbName
// Test database connection // Test database connection
query := "SELECT version();" query := "SELECT version();"
@@ -193,3 +197,34 @@ func RemoveLastExtension(filename string) string {
} }
return filename return filename
} }
func convertJDBCToDbConfig(jdbcURI string) (*dbConfig, error) {
// Remove the "jdbc:" prefix
jdbcURI = strings.TrimPrefix(jdbcURI, "jdbc:")
// Parse the URI
u, err := url.Parse(jdbcURI)
if err != nil {
return &dbConfig{}, fmt.Errorf("failed to parse JDBC URI: %v", err)
}
// Extract components
host := u.Hostname()
port := u.Port()
if port == "" {
port = "5432" // Default PostgreSQL port
}
database := strings.TrimPrefix(u.Path, "/")
params, _ := url.ParseQuery(u.RawQuery)
username := params.Get("user")
password := params.Get("password")
// Validate essential fields
if host == "" || database == "" || username == "" {
return &dbConfig{}, fmt.Errorf("incomplete JDBC URI: missing host, database, or username")
}
return &dbConfig{
dbHost: host,
dbPort: port,
dbName: database,
dbUserName: username,
dbPassword: password,
}, nil
}

View File

@@ -52,12 +52,13 @@ func sshBackup(db *dbConfig, config *BackupConfig) {
} }
sshStorage, err := ssh.NewStorage(ssh.Config{ sshStorage, err := ssh.NewStorage(ssh.Config{
Host: sshConfig.hostName, Host: sshConfig.hostName,
Port: sshConfig.port, Port: sshConfig.port,
User: sshConfig.user, User: sshConfig.user,
Password: sshConfig.password, Password: sshConfig.password,
RemotePath: config.remotePath, IdentifyFile: sshConfig.identifyFile,
LocalPath: tmpPath, RemotePath: config.remotePath,
LocalPath: tmpPath,
}) })
if err != nil { if err != nil {
utils.Fatal("Error creating SSH storage: %s", err) utils.Fatal("Error creating SSH storage: %s", err)
@@ -87,11 +88,13 @@ func sshBackup(db *dbConfig, config *BackupConfig) {
} }
} }
utils.Info("Backup name is %s", finalFileName)
utils.Info("Backup size: %s", utils.ConvertBytes(uint64(backupSize)))
utils.Info("Uploading backup archive to remote storage ... done ") utils.Info("Uploading backup archive to remote storage ... done ")
// Send notification // Send notification
utils.NotifySuccess(&utils.NotificationData{ utils.NotifySuccess(&utils.NotificationData{
File: finalFileName, File: finalFileName,
BackupSize: backupSize, BackupSize: utils.ConvertBytes(uint64(backupSize)),
Database: db.dbName, Database: db.dbName,
Storage: config.storage, Storage: config.storage,
BackupLocation: filepath.Join(config.remotePath, finalFileName), BackupLocation: filepath.Join(config.remotePath, finalFileName),
@@ -103,72 +106,6 @@ func sshBackup(db *dbConfig, config *BackupConfig) {
utils.Info("Backup completed successfully") utils.Info("Backup completed successfully")
} }
func ftpBackup(db *dbConfig, config *BackupConfig) {
utils.Info("Backup database to the remote FTP server")
startTime = time.Now().Format(utils.TimeFormat())
// Backup database
BackupDatabase(db, config.backupFileName, disableCompression)
finalFileName := config.backupFileName
if config.encryption {
encryptBackup(config)
finalFileName = fmt.Sprintf("%s.%s", config.backupFileName, "gpg")
}
utils.Info("Uploading backup archive to the remote FTP server ... ")
utils.Info("Backup name is %s", finalFileName)
ftpConfig := loadFtpConfig()
ftpStorage, err := ftp.NewStorage(ftp.Config{
Host: ftpConfig.host,
Port: ftpConfig.port,
User: ftpConfig.user,
Password: ftpConfig.password,
RemotePath: config.remotePath,
LocalPath: tmpPath,
})
if err != nil {
utils.Fatal("Error creating SSH storage: %s", err)
}
err = ftpStorage.Copy(finalFileName)
if err != nil {
utils.Fatal("Error copying backup file: %s", err)
}
utils.Info("Backup saved in %s", filepath.Join(config.remotePath, finalFileName))
// Get backup info
fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName))
if err != nil {
utils.Error("Error: %s", err)
}
backupSize = fileInfo.Size()
// Delete backup file from tmp folder
err = utils.DeleteFile(filepath.Join(tmpPath, finalFileName))
if err != nil {
utils.Error("Error deleting file: %v", err)
}
if config.prune {
err := ftpStorage.Prune(config.backupRetention)
if err != nil {
utils.Fatal("Error deleting old backup from %s storage: %s ", config.storage, err)
}
}
utils.Info("Uploading backup archive to the remote FTP server ... done ")
// Send notification
utils.NotifySuccess(&utils.NotificationData{
File: finalFileName,
BackupSize: backupSize,
Database: db.dbName,
Storage: config.storage,
BackupLocation: filepath.Join(config.remotePath, finalFileName),
StartTime: startTime,
EndTime: time.Now().Format(utils.TimeFormat()),
})
// Delete temp
deleteTemp()
utils.Info("Backup completed successfully")
}
func remoteRestore(db *dbConfig, conf *RestoreConfig) { func remoteRestore(db *dbConfig, conf *RestoreConfig) {
utils.Info("Restore database from remote server") utils.Info("Restore database from remote server")
sshConfig, err := loadSSHConfig() sshConfig, err := loadSSHConfig()
@@ -214,3 +151,69 @@ func ftpRestore(db *dbConfig, conf *RestoreConfig) {
} }
RestoreDatabase(db, conf) RestoreDatabase(db, conf)
} }
func ftpBackup(db *dbConfig, config *BackupConfig) {
utils.Info("Backup database to the remote FTP server")
startTime = time.Now().Format(utils.TimeFormat())
// Backup database
BackupDatabase(db, config.backupFileName, disableCompression)
finalFileName := config.backupFileName
if config.encryption {
encryptBackup(config)
finalFileName = fmt.Sprintf("%s.%s", config.backupFileName, "gpg")
}
utils.Info("Uploading backup archive to the remote FTP server ... ")
ftpConfig := loadFtpConfig()
ftpStorage, err := ftp.NewStorage(ftp.Config{
Host: ftpConfig.host,
Port: ftpConfig.port,
User: ftpConfig.user,
Password: ftpConfig.password,
RemotePath: config.remotePath,
LocalPath: tmpPath,
})
if err != nil {
utils.Fatal("Error creating SSH storage: %s", err)
}
err = ftpStorage.Copy(finalFileName)
if err != nil {
utils.Fatal("Error copying backup file: %s", err)
}
utils.Info("Backup saved in %s", filepath.Join(config.remotePath, finalFileName))
// Get backup info
fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName))
if err != nil {
utils.Error("Error: %s", err)
}
backupSize = fileInfo.Size()
// Delete backup file from tmp folder
err = utils.DeleteFile(filepath.Join(tmpPath, finalFileName))
if err != nil {
utils.Error("Error deleting file: %v", err)
}
if config.prune {
err := ftpStorage.Prune(config.backupRetention)
if err != nil {
utils.Fatal("Error deleting old backup from %s storage: %s ", config.storage, err)
}
}
utils.Info("Backup name is %s", finalFileName)
utils.Info("Backup size: %s", utils.ConvertBytes(uint64(backupSize)))
utils.Info("Uploading backup archive to the remote FTP server ... done ")
// Send notification
utils.NotifySuccess(&utils.NotificationData{
File: finalFileName,
BackupSize: utils.ConvertBytes(uint64(backupSize)),
Database: db.dbName,
Storage: config.storage,
BackupLocation: filepath.Join(config.remotePath, finalFileName),
StartTime: startTime,
EndTime: time.Now().Format(utils.TimeFormat()),
})
// Delete temp
deleteTemp()
utils.Info("Backup completed successfully")
}

View File

@@ -1,4 +1,3 @@
// Package internal /
/* /*
MIT License MIT License

View File

@@ -89,11 +89,12 @@ func s3Backup(db *dbConfig, config *BackupConfig) {
} }
} }
utils.Info("Backup saved in %s", filepath.Join(config.remotePath, finalFileName)) utils.Info("Backup saved in %s", filepath.Join(config.remotePath, finalFileName))
utils.Info("Backup size: %s", utils.ConvertBytes(uint64(backupSize)))
utils.Info("Uploading backup archive to remote storage S3 ... done ") utils.Info("Uploading backup archive to remote storage S3 ... done ")
// Send notification // Send notification
utils.NotifySuccess(&utils.NotificationData{ utils.NotifySuccess(&utils.NotificationData{
File: finalFileName, File: finalFileName,
BackupSize: backupSize, BackupSize: utils.ConvertBytes(uint64(backupSize)),
Database: db.dbName, Database: db.dbName,
Storage: config.storage, Storage: config.storage,
BackupLocation: filepath.Join(config.remotePath, finalFileName), BackupLocation: filepath.Join(config.remotePath, finalFileName),

View File

@@ -45,14 +45,12 @@ var (
// dbHVars Required environment variables for database // dbHVars Required environment variables for database
var dbHVars = []string{ var dbHVars = []string{
"DB_HOST", "DB_HOST",
"DB_PORT",
"DB_PASSWORD", "DB_PASSWORD",
"DB_USERNAME", "DB_USERNAME",
"DB_NAME", "DB_NAME",
} }
var tdbRVars = []string{ var tdbRVars = []string{
"TARGET_DB_HOST", "TARGET_DB_HOST",
"TARGET_DB_PORT",
"TARGET_DB_NAME", "TARGET_DB_NAME",
"TARGET_DB_USERNAME", "TARGET_DB_USERNAME",
"TARGET_DB_PASSWORD", "TARGET_DB_PASSWORD",

View File

@@ -1,18 +1,69 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>🔴 Urgent: Database Backup Failure Notification</title> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🔴 Urgent: Database Backup Failure</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f8f9fa;
color: #333;
margin: 0;
padding: 20px;
}
h2 {
color: #d9534f;
}
.details {
background-color: #ffffff;
border: 1px solid #ddd;
padding: 15px;
border-radius: 5px;
margin-top: 10px;
}
.details ul {
list-style-type: none;
padding: 0;
}
.details li {
margin: 5px 0;
}
a {
color: #0275d8;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
footer {
margin-top: 20px;
font-size: 0.9em;
color: #6c757d;
}
</style>
</head> </head>
<body> <body>
<h2>Hi,</h2> <h2>🔴 Urgent: Database Backup Failure Notification</h2>
<p>An error occurred during database backup.</p> <p>Dear Team,</p>
<h3>Failure Details:</h3> <p>An error occurred during the database backup process. Please review the details below and take the necessary actions:</p>
<ul>
<li>Error Message: {{.Error}}</li> <div class="details">
<li>Date: {{.EndTime}}</li> <h3>Failure Details:</h3>
<li>Backup Reference: {{.BackupReference}} </li> <ul>
</ul> <li><strong>Database Name:</strong> {{.DatabaseName}}</li>
<p>©2024 <a href="https://github.com/jkaninda/pg-bkup">pg-bkup</a></p> <li><strong>Date:</strong> {{.EndTime}}</li>
<li><strong>Backup Reference:</strong> {{.BackupReference}}</li>
<li><strong>Error Message:</strong> {{.Error}}</li>
</ul>
</div>
<p>We recommend investigating the issue as soon as possible to prevent potential data loss or service disruptions.</p>
<p>For more information, visit the <a href="https://jkaninda.github.io/pg-bkup">pg-bkup documentation</a>.</p>
<footer>
&copy; 2024 <a href="https://github.com/jkaninda/pg-bkup">pg-bkup</a> | Automated Backup System
</footer>
</body> </body>
</html> </html>

View File

@@ -1,23 +1,70 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>✅ Database Backup Notification {{.Database}}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>✅ Database Backup Successful {{.Database}}</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f8f9fa;
color: #333;
margin: 0;
padding: 20px;
}
h2 {
color: #5cb85c;
}
.details {
background-color: #ffffff;
border: 1px solid #ddd;
padding: 15px;
border-radius: 5px;
margin-top: 10px;
}
.details ul {
list-style-type: none;
padding: 0;
}
.details li {
margin: 5px 0;
}
a {
color: #0275d8;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
footer {
margin-top: 20px;
font-size: 0.9em;
color: #6c757d;
}
</style>
</head> </head>
<body> <body>
<h2>Hi,</h2> <h2>✅ Database Backup Successful</h2>
<p>Backup of the {{.Database}} database has been successfully completed on {{.EndTime}}.</p> <p>Hi,</p>
<h3>Backup Details:</h3> <p>The backup process for the <strong>{{.Database}}</strong> database was successfully completed. Please find the details below:</p>
<ul>
<li>Database Name: {{.Database}}</li> <div class="details">
<li>Backup Start Time: {{.StartTime}}</li> <h3>Backup Details:</h3>
<li>Backup End Time: {{.EndTime}}</li> <ul>
<li>Backup Storage: {{.Storage}}</li> <li><strong>Database Name:</strong> {{.Database}}</li>
<li>Backup Location: {{.BackupLocation}}</li> <li><strong>Backup Start Time:</strong> {{.StartTime}}</li>
<li>Backup Size: {{.BackupSize}} bytes</li> <li><strong>Backup End Time:</strong> {{.EndTime}}</li>
<li>Backup Reference: {{.BackupReference}} </li> <li><strong>Backup Storage:</strong> {{.Storage}}</li>
</ul> <li><strong>Backup Location:</strong> {{.BackupLocation}}</li>
<p>Best regards,</p> <li><strong>Backup Size:</strong> {{.BackupSize}}</li>
<p>©2024 <a href="https://github.com/jkaninda/pg-bkup">pg-bkup</a></p> <li><strong>Backup Reference:</strong> {{.BackupReference}}</li>
</ul>
</div>
<p>You can access the backup at the specified location if needed. Thank you for using <a href="https://jkaninda.github.io/pg-bkup/">pg-bkup</a>.</p>
<footer>
&copy; 2024 <a href="https://github.com/jkaninda/pg-bkup">pg-bkup</a> | Automated Backup System
</footer>
</body> </body>
</html> </html>

View File

@@ -1,8 +1,11 @@
🔴 Urgent: Database Backup Failure Notification 🔴 Urgent: Database Backup Failure Notification
Hi,
An error occurred during database backup. Dear Team,
An error occurred during the database backup process.
Please review the details below and take the necessary actions:
Failure Details: Failure Details:
- Database Name: {{.DatabaseName}}
- Date: {{.EndTime}} - Date: {{.EndTime}}
- Backup Reference: {{.BackupReference}} - Backup Reference: {{.BackupReference}}
- Error Message: {{.Error}} - Error Message: {{.Error}}
We recommend investigating the issue as soon as possible to prevent potential data loss or service disruptions.

View File

@@ -1,6 +1,8 @@
Database Backup Notification {{.Database}} ✅ Database Backup Successful
Hi, Hi,
Backup of the {{.Database}} database has been successfully completed on {{.EndTime}}. The backup process for the {{.Database}} database was successfully completed.
Please find the details below:
Backup Details: Backup Details:
- Database Name: {{.Database}} - Database Name: {{.Database}}
@@ -8,5 +10,7 @@ Backup Details:
- Backup EndTime: {{.EndTime}} - Backup EndTime: {{.EndTime}}
- Backup Storage: {{.Storage}} - Backup Storage: {{.Storage}}
- Backup Location: {{.BackupLocation}} - Backup Location: {{.BackupLocation}}
- Backup Size: {{.BackupSize}} bytes - Backup Size: {{.BackupSize}}
- Backup Reference: {{.BackupReference}} - Backup Reference: {{.BackupReference}}
You can access the backup at the specified location if needed.

View File

@@ -37,7 +37,7 @@ type MailConfig struct {
} }
type NotificationData struct { type NotificationData struct {
File string File string
BackupSize int64 BackupSize string
Database string Database string
StartTime string StartTime string
EndTime string EndTime string
@@ -50,6 +50,7 @@ type ErrorMessage struct {
EndTime string EndTime string
Error string Error string
BackupReference string BackupReference string
DatabaseName string
} }
// loadMailConfig gets mail environment variables and returns MailConfig // loadMailConfig gets mail environment variables and returns MailConfig
@@ -81,3 +82,5 @@ func backupReference() string {
} }
const templatePath = "/config/templates" const templatePath = "/config/templates"
var DatabaseName = ""

View File

@@ -167,6 +167,7 @@ func NotifyError(error string) {
Error: error, Error: error,
EndTime: time.Now().Format(TimeFormat()), EndTime: time.Now().Format(TimeFormat()),
BackupReference: os.Getenv("BACKUP_REFERENCE"), BackupReference: os.Getenv("BACKUP_REFERENCE"),
DatabaseName: DatabaseName,
}, "email-error.tmpl") }, "email-error.tmpl")
if err != nil { if err != nil {
Error("Could not parse error template: %v", err) Error("Could not parse error template: %v", err)
@@ -183,6 +184,7 @@ func NotifyError(error string) {
Error: error, Error: error,
EndTime: time.Now().Format(TimeFormat()), EndTime: time.Now().Format(TimeFormat()),
BackupReference: os.Getenv("BACKUP_REFERENCE"), BackupReference: os.Getenv("BACKUP_REFERENCE"),
DatabaseName: DatabaseName,
}, "telegram-error.tmpl") }, "telegram-error.tmpl")
if err != nil { if err != nil {
Error("Could not parse error template: %v", err) Error("Could not parse error template: %v", err)

View File

@@ -254,7 +254,19 @@ func CronNextTime(cronExpr string) time.Time {
next := schedule.Next(now) next := schedule.Next(now)
return next return next
} }
func UsageErrorf(cmd *cobra.Command, message string, args ...interface{}) error {
msg := fmt.Sprintf(message, args...) // ConvertBytes converts bytes to a human-readable string with the appropriate unit (bytes, MiB, or GiB).
return fmt.Errorf("%s\nSee '%s -h' for help and examples", msg, cmd.CommandPath()) func ConvertBytes(bytes uint64) string {
const (
MiB = 1024 * 1024
GiB = MiB * 1024
)
switch {
case bytes >= GiB:
return fmt.Sprintf("%.2f GiB", float64(bytes)/float64(GiB))
case bytes >= MiB:
return fmt.Sprintf("%.2f MiB", float64(bytes)/float64(MiB))
default:
return fmt.Sprintf("%d bytes", bytes)
}
} }