mirror of
https://github.com/jkaninda/mysql-bkup.git
synced 2025-12-06 21:49:40 +01:00
feat: add email notification for failed and success backup
This commit is contained in:
@@ -47,6 +47,7 @@ ENV TZ=UTC
|
|||||||
ARG WORKDIR="/config"
|
ARG WORKDIR="/config"
|
||||||
ARG BACKUPDIR="/backup"
|
ARG BACKUPDIR="/backup"
|
||||||
ARG BACKUP_TMP_DIR="/tmp/backup"
|
ARG BACKUP_TMP_DIR="/tmp/backup"
|
||||||
|
ARG TEMPLATES_DIR="/config/templates"
|
||||||
ARG appVersion="v1.2.12"
|
ARG appVersion="v1.2.12"
|
||||||
ENV VERSION=${appVersion}
|
ENV VERSION=${appVersion}
|
||||||
LABEL author="Jonas Kaninda"
|
LABEL author="Jonas Kaninda"
|
||||||
@@ -55,6 +56,7 @@ LABEL version=${appVersion}
|
|||||||
RUN apk --update add --no-cache mysql-client mariadb-connector-c tzdata
|
RUN apk --update add --no-cache mysql-client mariadb-connector-c tzdata
|
||||||
RUN mkdir $WORKDIR
|
RUN mkdir $WORKDIR
|
||||||
RUN mkdir $BACKUPDIR
|
RUN mkdir $BACKUPDIR
|
||||||
|
RUN mkdir $TEMPLATES_DIR
|
||||||
RUN mkdir -p $BACKUP_TMP_DIR
|
RUN mkdir -p $BACKUP_TMP_DIR
|
||||||
RUN chmod 777 $WORKDIR
|
RUN chmod 777 $WORKDIR
|
||||||
RUN chmod 777 $BACKUPDIR
|
RUN chmod 777 $BACKUPDIR
|
||||||
@@ -62,6 +64,7 @@ RUN chmod 777 $BACKUP_TMP_DIR
|
|||||||
RUN chmod 777 $WORKDIR
|
RUN chmod 777 $WORKDIR
|
||||||
|
|
||||||
COPY --from=build /app/mysql-bkup /usr/local/bin/mysql-bkup
|
COPY --from=build /app/mysql-bkup /usr/local/bin/mysql-bkup
|
||||||
|
COPY ./templates/* $TEMPLATES_DIR/
|
||||||
RUN chmod +x /usr/local/bin/mysql-bkup
|
RUN chmod +x /usr/local/bin/mysql-bkup
|
||||||
|
|
||||||
RUN ln -s /usr/local/bin/mysql-bkup /usr/local/bin/bkup
|
RUN ln -s /usr/local/bin/mysql-bkup /usr/local/bin/bkup
|
||||||
|
|||||||
126
docs/how-tos/receive-notification.md
Normal file
126
docs/how-tos/receive-notification.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
title: Receive notifications
|
||||||
|
layout: default
|
||||||
|
parent: How Tos
|
||||||
|
nav_order: 12
|
||||||
|
---
|
||||||
|
Send Email or Telegram notifications on success or failed backup.
|
||||||
|
|
||||||
|
### Email
|
||||||
|
To send out email notifications on failed backup runs, provide SMTP credentials, a sender and a recipient:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mysql-bkup:
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
container_name: mysql-bkup
|
||||||
|
command: backup
|
||||||
|
volumes:
|
||||||
|
- ./backup:/backup
|
||||||
|
environment:
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- DB_NAME=database
|
||||||
|
- DB_USERNAME=username
|
||||||
|
- DB_PASSWORD=password
|
||||||
|
- MAIL_HOST=
|
||||||
|
- MAIL_PORT=587
|
||||||
|
- MAIL_USERNAME=
|
||||||
|
- MAIL_PASSWORD=!
|
||||||
|
- MAIL_FROM=sender@example.com
|
||||||
|
- MAIL_TO=me@example.com,team@example.com,manager@example.com
|
||||||
|
- MAIL_SKIP_TLS=false
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
```
|
||||||
|
|
||||||
|
### Telegram
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mysql-bkup:
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
container_name: mysql-bkup
|
||||||
|
command: backup
|
||||||
|
volumes:
|
||||||
|
- ./backup:/backup
|
||||||
|
environment:
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- DB_NAME=database
|
||||||
|
- DB_USERNAME=username
|
||||||
|
- DB_PASSWORD=password
|
||||||
|
- TG_TOKEN=[BOT ID]:[BOT TOKEN]
|
||||||
|
- TG_CHAT_ID=
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize notifications
|
||||||
|
|
||||||
|
The body of the notifications can be tailored to your needs using Go templates.
|
||||||
|
Template sources must be mounted inside the container in /config/templates:
|
||||||
|
|
||||||
|
- email.template: Email notification template
|
||||||
|
- telegram.template: Telegram notification template
|
||||||
|
- error.template: Error notification template
|
||||||
|
|
||||||
|
> email.template:
|
||||||
|
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>[✅ Database Backup Notification – {{.Database}}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Hi,</h2>
|
||||||
|
<p>Backup of the {{.Database}} database has been successfully completed on {{.EndTime}}.</p>
|
||||||
|
<h3>Backup Details:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Database Name: {{.Database}}</li>
|
||||||
|
<li>Backup Start Time: {{.StartTime}}</li>
|
||||||
|
<li>Backup End Time: {{.EndTime}}</li>
|
||||||
|
<li>Backup Storage: {{.Storage}}</li>
|
||||||
|
<li>Backup Location: {{.BackupLocation}}</li>
|
||||||
|
<li>Backup Size: {{.BackupSize}} bytes</li>
|
||||||
|
</ul>
|
||||||
|
<p>Best regards,</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
> telegram.template
|
||||||
|
|
||||||
|
```html
|
||||||
|
[✅ Database Backup Notification – {{.Database}}
|
||||||
|
Hi,
|
||||||
|
Backup of the {{.Database}} database has been successfully completed on {{.EndTime}}.
|
||||||
|
|
||||||
|
Backup Details:
|
||||||
|
- Database Name: {{.Database}}
|
||||||
|
- Backup Start Time: {{.StartTime}}
|
||||||
|
- Backup EndTime: {{.EndTime}}
|
||||||
|
- Backup Storage: {{.Storage}}
|
||||||
|
- Backup Location: {{.BackupLocation}}
|
||||||
|
- Backup Size: {{.BackupSize}} bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
> error.template
|
||||||
|
|
||||||
|
|
||||||
|
```html
|
||||||
|
🔴 Urgent: Database Backup Failure Notification
|
||||||
|
|
||||||
|
An error occurred during database backup.
|
||||||
|
Failure Details:
|
||||||
|
|
||||||
|
Error Message: {{.Error}}
|
||||||
|
Date: {{.EndTime}}
|
||||||
|
```
|
||||||
2
go.mod
2
go.mod
@@ -20,6 +20,7 @@ require (
|
|||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // 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/cloudflare/circl v1.3.3 // indirect
|
github.com/cloudflare/circl v1.3.3 // indirect
|
||||||
|
github.com/go-mail/mail v2.3.1+incompatible // 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
|
||||||
@@ -27,6 +28,7 @@ require (
|
|||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -17,6 +17,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
|
|||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX8AJBAxXExM=
|
||||||
|
github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
@@ -94,6 +96,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
|||||||
@@ -218,17 +218,31 @@ func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool
|
|||||||
}
|
}
|
||||||
func localBackup(db *dbConfig, config *BackupConfig) {
|
func localBackup(db *dbConfig, config *BackupConfig) {
|
||||||
utils.Info("Backup database to local storage")
|
utils.Info("Backup database to local storage")
|
||||||
|
startTime = time.Now().Format("2006-01-02 15:04:05")
|
||||||
BackupDatabase(db, config.backupFileName, disableCompression)
|
BackupDatabase(db, config.backupFileName, disableCompression)
|
||||||
finalFileName := config.backupFileName
|
finalFileName := config.backupFileName
|
||||||
if config.encryption {
|
if config.encryption {
|
||||||
encryptBackup(config)
|
encryptBackup(config)
|
||||||
finalFileName = fmt.Sprintf("%s.%s", config.backupFileName, gpgExtension)
|
finalFileName = fmt.Sprintf("%s.%s", config.backupFileName, gpgExtension)
|
||||||
}
|
}
|
||||||
|
fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName))
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("Error:", err)
|
||||||
|
}
|
||||||
|
//Get backup info
|
||||||
|
backupSize = fileInfo.Size()
|
||||||
utils.Info("Backup name is %s", finalFileName)
|
utils.Info("Backup name is %s", finalFileName)
|
||||||
moveToBackup(finalFileName, storagePath)
|
moveToBackup(finalFileName, storagePath)
|
||||||
//Send notification
|
//Send notification
|
||||||
utils.NotifySuccess(finalFileName)
|
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("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
//Delete old backup
|
//Delete old backup
|
||||||
if config.prune {
|
if config.prune {
|
||||||
deleteOldBackup(config.backupRetention)
|
deleteOldBackup(config.backupRetention)
|
||||||
@@ -242,6 +256,8 @@ func s3Backup(db *dbConfig, config *BackupConfig) {
|
|||||||
bucket := utils.GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
|
bucket := utils.GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
|
||||||
s3Path := utils.GetEnvVariable("AWS_S3_PATH", "S3_PATH")
|
s3Path := utils.GetEnvVariable("AWS_S3_PATH", "S3_PATH")
|
||||||
utils.Info("Backup database to s3 storage")
|
utils.Info("Backup database to s3 storage")
|
||||||
|
startTime = time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
//Backup database
|
//Backup database
|
||||||
BackupDatabase(db, config.backupFileName, disableCompression)
|
BackupDatabase(db, config.backupFileName, disableCompression)
|
||||||
finalFileName := config.backupFileName
|
finalFileName := config.backupFileName
|
||||||
@@ -257,7 +273,12 @@ func s3Backup(db *dbConfig, config *BackupConfig) {
|
|||||||
utils.Fatal("Error uploading backup archive to S3: %s ", err)
|
utils.Fatal("Error uploading backup archive to S3: %s ", err)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
//Get backup info
|
||||||
|
fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName))
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("Error:", err)
|
||||||
|
}
|
||||||
|
backupSize = fileInfo.Size()
|
||||||
//Delete backup file from tmp folder
|
//Delete backup file from tmp folder
|
||||||
err = utils.DeleteFile(filepath.Join(tmpPath, config.backupFileName))
|
err = utils.DeleteFile(filepath.Join(tmpPath, config.backupFileName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -273,7 +294,15 @@ func s3Backup(db *dbConfig, config *BackupConfig) {
|
|||||||
}
|
}
|
||||||
utils.Done("Uploading backup archive to remote storage S3 ... done ")
|
utils.Done("Uploading backup archive to remote storage S3 ... done ")
|
||||||
//Send notification
|
//Send notification
|
||||||
utils.NotifySuccess(finalFileName)
|
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("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
//Delete temp
|
//Delete temp
|
||||||
deleteTemp()
|
deleteTemp()
|
||||||
utils.Info("Backup completed successfully")
|
utils.Info("Backup completed successfully")
|
||||||
@@ -281,6 +310,8 @@ func s3Backup(db *dbConfig, config *BackupConfig) {
|
|||||||
}
|
}
|
||||||
func sshBackup(db *dbConfig, config *BackupConfig) {
|
func sshBackup(db *dbConfig, config *BackupConfig) {
|
||||||
utils.Info("Backup database to Remote server")
|
utils.Info("Backup database to Remote server")
|
||||||
|
startTime = time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
//Backup database
|
//Backup database
|
||||||
BackupDatabase(db, config.backupFileName, disableCompression)
|
BackupDatabase(db, config.backupFileName, disableCompression)
|
||||||
finalFileName := config.backupFileName
|
finalFileName := config.backupFileName
|
||||||
@@ -295,7 +326,12 @@ func sshBackup(db *dbConfig, config *BackupConfig) {
|
|||||||
utils.Fatal("Error uploading file to the remote server: %s ", err)
|
utils.Fatal("Error uploading file to the remote server: %s ", err)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
//Get backup info
|
||||||
|
fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName))
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("Error:", err)
|
||||||
|
}
|
||||||
|
backupSize = fileInfo.Size()
|
||||||
//Delete backup file from tmp folder
|
//Delete backup file from tmp folder
|
||||||
err = utils.DeleteFile(filepath.Join(tmpPath, finalFileName))
|
err = utils.DeleteFile(filepath.Join(tmpPath, finalFileName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -310,7 +346,15 @@ func sshBackup(db *dbConfig, config *BackupConfig) {
|
|||||||
|
|
||||||
utils.Done("Uploading backup archive to remote storage ... done ")
|
utils.Done("Uploading backup archive to remote storage ... done ")
|
||||||
//Send notification
|
//Send notification
|
||||||
utils.NotifySuccess(finalFileName)
|
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("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
//Delete temp
|
//Delete temp
|
||||||
deleteTemp()
|
deleteTemp()
|
||||||
utils.Info("Backup completed successfully")
|
utils.Info("Backup completed successfully")
|
||||||
@@ -318,6 +362,8 @@ func sshBackup(db *dbConfig, config *BackupConfig) {
|
|||||||
}
|
}
|
||||||
func ftpBackup(db *dbConfig, config *BackupConfig) {
|
func ftpBackup(db *dbConfig, config *BackupConfig) {
|
||||||
utils.Info("Backup database to the remote FTP server")
|
utils.Info("Backup database to the remote FTP server")
|
||||||
|
startTime = time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
//Backup database
|
//Backup database
|
||||||
BackupDatabase(db, config.backupFileName, disableCompression)
|
BackupDatabase(db, config.backupFileName, disableCompression)
|
||||||
finalFileName := config.backupFileName
|
finalFileName := config.backupFileName
|
||||||
@@ -332,7 +378,12 @@ func ftpBackup(db *dbConfig, config *BackupConfig) {
|
|||||||
utils.Fatal("Error uploading file to the remote FTP server: %s ", err)
|
utils.Fatal("Error uploading file to the remote FTP server: %s ", err)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
//Get backup info
|
||||||
|
fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName))
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("Error:", err)
|
||||||
|
}
|
||||||
|
backupSize = fileInfo.Size()
|
||||||
//Delete backup file from tmp folder
|
//Delete backup file from tmp folder
|
||||||
err = utils.DeleteFile(filepath.Join(tmpPath, finalFileName))
|
err = utils.DeleteFile(filepath.Join(tmpPath, finalFileName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -347,7 +398,15 @@ func ftpBackup(db *dbConfig, config *BackupConfig) {
|
|||||||
|
|
||||||
utils.Done("Uploading backup archive to the remote FTP server ... done ")
|
utils.Done("Uploading backup archive to the remote FTP server ... done ")
|
||||||
//Send notification
|
//Send notification
|
||||||
utils.NotifySuccess(finalFileName)
|
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("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
//Delete temp
|
//Delete temp
|
||||||
deleteTemp()
|
deleteTemp()
|
||||||
utils.Info("Backup completed successfully")
|
utils.Info("Backup completed successfully")
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ var (
|
|||||||
disableCompression = false
|
disableCompression = false
|
||||||
encryption = false
|
encryption = false
|
||||||
usingKey = false
|
usingKey = false
|
||||||
|
backupSize int64 = 0
|
||||||
|
startTime string
|
||||||
)
|
)
|
||||||
|
|
||||||
// dbHVars Required environment variables for database
|
// dbHVars Required environment variables for database
|
||||||
|
|||||||
17
templates/email-error.template
Normal file
17
templates/email-error.template
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>🔴 Urgent: Database Backup Failure Notification</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Hi,</h2>
|
||||||
|
<p>An error occurred during database backup.</p>
|
||||||
|
<h3>Failure Details:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Error Message: {{.Error}}</li>
|
||||||
|
<li>Date: {{.EndTime}}</li>
|
||||||
|
</ul>
|
||||||
|
<p>©2024 <a href="github.com/jkaninda/mysql-bkup">mysql-bkup</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
23
templates/email.template
Normal file
23
templates/email.template
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>✅ Database Backup Notification – {{.Database}}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Hi,</h2>
|
||||||
|
<p>Backup of the {{.Database}} database has been successfully completed on {{.EndTime}}.</p>
|
||||||
|
<h3>Backup Details:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Database Name: {{.Database}}</li>
|
||||||
|
<li>Backup Start Time: {{.StartTime}}</li>
|
||||||
|
<li>Backup End Time: {{.EndTime}}</li>
|
||||||
|
<li>Backup Storage: {{.Storage}}</li>
|
||||||
|
<li>Backup Location: {{.BackupLocation}}</li>
|
||||||
|
<li>Backup Size: {{.BackupSize}} bytes</li>
|
||||||
|
</ul>
|
||||||
|
<p>Best regards,</p>
|
||||||
|
<p>©2024 <a href="github.com/jkaninda/mysql-bkup">mysql-bkup</a></p>
|
||||||
|
<href>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7
templates/telegram-error.template
Normal file
7
templates/telegram-error.template
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
🔴 Urgent: Database Backup Failure Notification
|
||||||
|
Hi,
|
||||||
|
An error occurred during database backup.
|
||||||
|
Failure Details:
|
||||||
|
Date: {{.EndTime}}
|
||||||
|
Error Message: {{.Error}}
|
||||||
|
|
||||||
11
templates/telegram.template
Normal file
11
templates/telegram.template
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
✅ Database Backup Notification – {{.Database}}
|
||||||
|
Hi,
|
||||||
|
Backup of the {{.Database}} database has been successfully completed on {{.EndTime}}.
|
||||||
|
|
||||||
|
Backup Details:
|
||||||
|
- Database Name: {{.Database}}
|
||||||
|
- Backup Start Time: {{.StartTime}}
|
||||||
|
- Backup EndTime: {{.EndTime}}
|
||||||
|
- Backup Storage: {{.Storage}}
|
||||||
|
- Backup Location: {{.BackupLocation}}
|
||||||
|
- Backup Size: {{.BackupSize}} bytes
|
||||||
42
utils/config.go
Normal file
42
utils/config.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
type MailConfig struct {
|
||||||
|
MailHost string
|
||||||
|
MailPort int
|
||||||
|
MailUserName string
|
||||||
|
MailPassword string
|
||||||
|
MailTo string
|
||||||
|
MailFrom string
|
||||||
|
SkipTls bool
|
||||||
|
}
|
||||||
|
type NotificationData struct {
|
||||||
|
File string
|
||||||
|
BackupSize int64
|
||||||
|
Database string
|
||||||
|
StartTime string
|
||||||
|
EndTime string
|
||||||
|
Storage string
|
||||||
|
BackupLocation string
|
||||||
|
}
|
||||||
|
type ErrorMessage struct {
|
||||||
|
Database string
|
||||||
|
EndTime string
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMailConfig() *MailConfig {
|
||||||
|
return &MailConfig{
|
||||||
|
MailHost: os.Getenv("MAIL_HOST"),
|
||||||
|
MailPort: GetIntEnv("MAIL_PORT"),
|
||||||
|
MailUserName: os.Getenv("MAIL_USERNAME"),
|
||||||
|
MailPassword: os.Getenv("MAIL_PASSWORD"),
|
||||||
|
MailTo: os.Getenv("MAIL_TO"),
|
||||||
|
MailFrom: os.Getenv("MAIL_FROM"),
|
||||||
|
SkipTls: os.Getenv("MAIL_SKIP_TLS") == "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const templatePath = "/config/templates"
|
||||||
163
utils/notification.go
Normal file
163
utils/notification.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-mail/mail"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
"html/template"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseTemplate[T any](data T, fileName string) (string, error) {
|
||||||
|
// Open the file
|
||||||
|
tmpl, err := template.ParseFiles(filepath.Join(templatePath, fileName))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err = tmpl.Execute(&buf, data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendEmail(subject, body string) {
|
||||||
|
Info("Start sending email....")
|
||||||
|
config := loadMailConfig()
|
||||||
|
emails := strings.Split(config.MailTo, ",")
|
||||||
|
m := mail.NewMessage()
|
||||||
|
m.SetHeader("From", config.MailFrom)
|
||||||
|
m.SetHeader("To", emails...)
|
||||||
|
m.SetHeader("Subject", subject)
|
||||||
|
m.SetBody("text/html", body)
|
||||||
|
d := mail.NewDialer(config.MailHost, config.MailPort, config.MailUserName, config.MailPassword)
|
||||||
|
d.TLSConfig = &tls.Config{InsecureSkipVerify: config.SkipTls}
|
||||||
|
|
||||||
|
if err := d.DialAndSend(m); err != nil {
|
||||||
|
Fatal("Error could not send email : %v", err)
|
||||||
|
}
|
||||||
|
Info("Email has been sent")
|
||||||
|
|
||||||
|
}
|
||||||
|
func sendMessage(msg string) {
|
||||||
|
|
||||||
|
Info("Sending notification... ")
|
||||||
|
chatId := os.Getenv("TG_CHAT_ID")
|
||||||
|
body, _ := json.Marshal(map[string]string{
|
||||||
|
"chat_id": chatId,
|
||||||
|
"text": msg,
|
||||||
|
})
|
||||||
|
url := fmt.Sprintf("%s/sendMessage", getTgUrl())
|
||||||
|
// Create an HTTP post request
|
||||||
|
request, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
request.Header.Add("Content-Type", "application/json")
|
||||||
|
client := &http.Client{}
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
code := response.StatusCode
|
||||||
|
if code == 200 {
|
||||||
|
Info("Notification has been sent")
|
||||||
|
} else {
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
Fatal("Error could not send message, error: %s", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
func NotifySuccess(notificationData *NotificationData) {
|
||||||
|
var vars = []string{
|
||||||
|
"TG_TOKEN",
|
||||||
|
"TG_CHAT_ID",
|
||||||
|
}
|
||||||
|
var mailVars = []string{
|
||||||
|
"MAIL_HOST",
|
||||||
|
"MAIL_PORT",
|
||||||
|
"MAIL_USERNAME",
|
||||||
|
"MAIL_PASSWORD",
|
||||||
|
"MAIL_FROM",
|
||||||
|
"MAIL_TO",
|
||||||
|
}
|
||||||
|
|
||||||
|
//Email notification
|
||||||
|
err := CheckEnvVars(mailVars)
|
||||||
|
if err == nil {
|
||||||
|
body, err := parseTemplate(*notificationData, "email.template")
|
||||||
|
if err != nil {
|
||||||
|
Error("Could not parse email template: %v", err)
|
||||||
|
}
|
||||||
|
SendEmail(fmt.Sprintf("✅ Database Backup Notification – %s", notificationData.Database), body)
|
||||||
|
}
|
||||||
|
//Telegram notification
|
||||||
|
err = CheckEnvVars(vars)
|
||||||
|
if err == nil {
|
||||||
|
message, err := parseTemplate(*notificationData, "telegram.template")
|
||||||
|
if err != nil {
|
||||||
|
Error("Could not parse email template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func NotifyError(error string) {
|
||||||
|
var vars = []string{
|
||||||
|
"TG_TOKEN",
|
||||||
|
"TG_CHAT_ID",
|
||||||
|
}
|
||||||
|
var mailVars = []string{
|
||||||
|
"MAIL_HOST",
|
||||||
|
"MAIL_PORT",
|
||||||
|
"MAIL_USERNAME",
|
||||||
|
"MAIL_PASSWORD",
|
||||||
|
"MAIL_FROM",
|
||||||
|
"MAIL_TO",
|
||||||
|
}
|
||||||
|
|
||||||
|
//Email notification
|
||||||
|
err := CheckEnvVars(mailVars)
|
||||||
|
if err == nil {
|
||||||
|
body, err := parseTemplate(ErrorMessage{
|
||||||
|
Error: error,
|
||||||
|
EndTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
}, "email-error.template")
|
||||||
|
if err != nil {
|
||||||
|
Error("Could not parse email template: %v", err)
|
||||||
|
}
|
||||||
|
SendEmail(fmt.Sprintf("🔴 Urgent: Database Backup Failure Notification"), body)
|
||||||
|
}
|
||||||
|
//Telegram notification
|
||||||
|
err = CheckEnvVars(vars)
|
||||||
|
if err == nil {
|
||||||
|
message, err := parseTemplate(ErrorMessage{
|
||||||
|
Error: error,
|
||||||
|
EndTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
}, "telegram-error.template")
|
||||||
|
if err != nil {
|
||||||
|
Error("Could not parse email template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTgUrl() string {
|
||||||
|
return fmt.Sprintf("https://api.telegram.org/bot%s", os.Getenv("TG_TOKEN"))
|
||||||
|
|
||||||
|
}
|
||||||
|
func IsValidCronExpression(cronExpr string) bool {
|
||||||
|
_, err := cron.ParseStandard(cronExpr)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
@@ -7,19 +7,15 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/robfig/cron/v3"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FileExists checks if the file does exist
|
||||||
func FileExists(filename string) bool {
|
func FileExists(filename string) bool {
|
||||||
info, err := os.Stat(filename)
|
info, err := os.Stat(filename)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -134,13 +130,10 @@ func GetEnvVariable(envName, oldEnvName string) string {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
Warn("%s is deprecated, please use %s instead! ", oldEnvName, envName)
|
Warn("%s is deprecated, please use %s instead! ", oldEnvName, envName)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
func ShowHistory() {
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckEnvVars checks if all the specified environment variables are set
|
// CheckEnvVars checks if all the specified environment variables are set
|
||||||
func CheckEnvVars(vars []string) error {
|
func CheckEnvVars(vars []string) error {
|
||||||
@@ -187,71 +180,3 @@ func GetIntEnv(envName string) int {
|
|||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
func sendMessage(msg string) {
|
|
||||||
|
|
||||||
Info("Sending notification... ")
|
|
||||||
chatId := os.Getenv("TG_CHAT_ID")
|
|
||||||
body, _ := json.Marshal(map[string]string{
|
|
||||||
"chat_id": chatId,
|
|
||||||
"text": msg,
|
|
||||||
})
|
|
||||||
url := fmt.Sprintf("%s/sendMessage", getTgUrl())
|
|
||||||
// Create an HTTP post request
|
|
||||||
request, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
request.Header.Add("Content-Type", "application/json")
|
|
||||||
client := &http.Client{}
|
|
||||||
response, err := client.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
code := response.StatusCode
|
|
||||||
if code == 200 {
|
|
||||||
Info("Notification has been sent")
|
|
||||||
} else {
|
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
|
||||||
Error("Message not sent, error: %s", string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
func NotifySuccess(fileName string) {
|
|
||||||
var vars = []string{
|
|
||||||
"TG_TOKEN",
|
|
||||||
"TG_CHAT_ID",
|
|
||||||
}
|
|
||||||
|
|
||||||
//Telegram notification
|
|
||||||
err := CheckEnvVars(vars)
|
|
||||||
if err == nil {
|
|
||||||
message := "[✅ MySQL Backup ]\n" +
|
|
||||||
"Database has been backed up \n" +
|
|
||||||
"Backup name is " + fileName
|
|
||||||
sendMessage(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func NotifyError(error string) {
|
|
||||||
var vars = []string{
|
|
||||||
"TG_TOKEN",
|
|
||||||
"TG_CHAT_ID",
|
|
||||||
}
|
|
||||||
|
|
||||||
//Telegram notification
|
|
||||||
err := CheckEnvVars(vars)
|
|
||||||
if err == nil {
|
|
||||||
message := "[🔴 MySQL Backup ]\n" +
|
|
||||||
"An error occurred during database backup \n" +
|
|
||||||
"Error: " + error
|
|
||||||
sendMessage(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTgUrl() string {
|
|
||||||
return fmt.Sprintf("https://api.telegram.org/bot%s", os.Getenv("TG_TOKEN"))
|
|
||||||
|
|
||||||
}
|
|
||||||
func IsValidCronExpression(cronExpr string) bool {
|
|
||||||
_, err := cron.ParseStandard(cronExpr)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user