From 4a43a288883296e271b54ffefd27445e72c2870d Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Wed, 9 Oct 2024 22:39:44 +0200 Subject: [PATCH] feat: add email notification for failed and success backup --- Dockerfile | 3 + docs/how-tos/receive-notification.md | 136 ++++++++++++++++++++++ go.mod | 2 + go.sum | 4 + pkg/backup.go | 72 +++++++++++- pkg/var.go | 12 +- templates/email-error.template | 17 +++ templates/email.template | 22 ++++ templates/telegram-error.template | 7 ++ templates/telegram.template | 11 ++ utils/config.go | 42 +++++++ utils/notification.go | 163 +++++++++++++++++++++++++++ utils/utils.go | 74 ------------ 13 files changed, 480 insertions(+), 85 deletions(-) create mode 100644 docs/how-tos/receive-notification.md create mode 100644 templates/email-error.template create mode 100644 templates/email.template create mode 100644 templates/telegram-error.template create mode 100644 templates/telegram.template create mode 100644 utils/config.go create mode 100644 utils/notification.go diff --git a/Dockerfile b/Dockerfile index 7ae797a..a7d3c7b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,6 +47,7 @@ ENV TZ=UTC ARG WORKDIR="/config" ARG BACKUPDIR="/backup" ARG BACKUP_TMP_DIR="/tmp/backup" +ARG TEMPLATES_DIR="/config/templates" ARG appVersion="v1.2.11" ENV VERSION=${appVersion} LABEL author="Jonas Kaninda" @@ -55,12 +56,14 @@ LABEL version=${appVersion} RUN apk --update add --no-cache postgresql-client tzdata RUN mkdir $WORKDIR RUN mkdir $BACKUPDIR +RUN mkdir $TEMPLATES_DIR RUN mkdir -p $BACKUP_TMP_DIR RUN chmod 777 $WORKDIR RUN chmod 777 $BACKUPDIR RUN chmod 777 $BACKUP_TMP_DIR RUN chmod 777 $WORKDIR COPY --from=build /app/pg-bkup /usr/local/bin/pg-bkup +COPY ./templates/* $TEMPLATES_DIR/ RUN chmod +x /usr/local/bin/pg-bkup RUN ln -s /usr/local/bin/pg-bkup /usr/local/bin/bkup diff --git a/docs/how-tos/receive-notification.md b/docs/how-tos/receive-notification.md new file mode 100644 index 0000000..45a0d07 --- /dev/null +++ b/docs/how-tos/receive-notification.md @@ -0,0 +1,136 @@ +--- +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: + pg-bkup: + image: jkaninda/pg-bkup + container_name: pg-bkup + command: backup + volumes: + - ./backup:/backup + environment: + - DB_PORT=5432 + - DB_HOST=postgres + - DB_NAME=database + - DB_USERNAME=username + - DB_PASSWORD=password + - MAIL_HOST= + - MAIL_PORT=587 + - MAIL_USERNAME= + - MAIL_PASSWORD=! + - MAIL_FROM= + - MAIL_TO=me@example.com,team@example.com,manager@example.com + - MAIL_SKIP_TLS=false + networks: + - web +networks: + web: +``` + +### Telegram + +```yaml +services: + pg-bkup: + image: jkaninda/pg-bkup + container_name: pg-bkup + command: backup + volumes: + - ./backup:/backup + environment: + - DB_PORT=5432 + - DB_HOST=postgres + - 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 title and 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 + +### Data + +Here is a list of all data passed to the template: +- `Database` : Database name +- `StartTime`: Backup start time process +- `EndTime`: Backup start time process +- `Storage`: Backup storage +- `BackupLocation`: Backup location +- `BackupSize`: Backup size + +> email.template: + + +```html + + + + + [✅ Database Backup Notification – {{.Database}} + + +

Hi,

+

Backup of the {{.Database}} database has been successfully completed on {{.EndTime}}.

+

Backup Details:

+ +

Best regards,

+ + +``` + +> 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}} +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 876dd6d..6afc2e4 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // 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/go-multierror v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -25,4 +26,5 @@ require ( github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect ) diff --git a/go.sum b/go.sum index 959d82b..b55f810 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/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.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -92,6 +94,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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= diff --git a/pkg/backup.go b/pkg/backup.go index 6851564..21aed44 100644 --- a/pkg/backup.go +++ b/pkg/backup.go @@ -224,17 +224,31 @@ func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool } func localBackup(db *dbConfig, config *BackupConfig) { utils.Info("Backup database to local storage") + startTime = time.Now().Format("2006-01-02 15:04:05") BackupDatabase(db, config.backupFileName, disableCompression) finalFileName := config.backupFileName if config.encryption { encryptBackup(config) finalFileName = fmt.Sprintf("%s.%s", config.backupFileName, gpgExtension) } - + fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName)) + if err != nil { + utils.Error("Error:", err) + } + backupSize = fileInfo.Size() utils.Info("Backup name is %s", finalFileName) moveToBackup(finalFileName, storagePath) + //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 if config.prune { deleteOldBackup(config.backupRetention) @@ -248,6 +262,7 @@ func s3Backup(db *dbConfig, config *BackupConfig) { bucket := utils.GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME") s3Path := utils.GetEnvVariable("AWS_S3_PATH", "S3_PATH") utils.Info("Backup database to s3 storage") + startTime = time.Now().Format("2006-01-02 15:04:05") //Backup database BackupDatabase(db, config.backupFileName, disableCompression) finalFileName := config.backupFileName @@ -263,6 +278,12 @@ func s3Backup(db *dbConfig, config *BackupConfig) { 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 err = utils.DeleteFile(filepath.Join(tmpPath, config.backupFileName)) @@ -279,7 +300,15 @@ func s3Backup(db *dbConfig, config *BackupConfig) { } utils.Done("Uploading backup archive to remote storage S3 ... done ") //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 deleteTemp() utils.Info("Backup completed successfully") @@ -287,6 +316,7 @@ func s3Backup(db *dbConfig, config *BackupConfig) { } func sshBackup(db *dbConfig, config *BackupConfig) { utils.Info("Backup database to Remote server") + startTime = time.Now().Format("2006-01-02 15:04:05") //Backup database BackupDatabase(db, config.backupFileName, disableCompression) finalFileName := config.backupFileName @@ -301,6 +331,12 @@ func sshBackup(db *dbConfig, config *BackupConfig) { 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 err = utils.DeleteFile(filepath.Join(tmpPath, finalFileName)) @@ -316,7 +352,15 @@ func sshBackup(db *dbConfig, config *BackupConfig) { utils.Done("Uploading backup archive to remote storage ... done ") //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 deleteTemp() utils.Info("Backup completed successfully") @@ -324,6 +368,8 @@ func sshBackup(db *dbConfig, config *BackupConfig) { } func ftpBackup(db *dbConfig, config *BackupConfig) { utils.Info("Backup database to the remote FTP server") + startTime = time.Now().Format("2006-01-02 15:04:05") + //Backup database BackupDatabase(db, config.backupFileName, disableCompression) finalFileName := config.backupFileName @@ -338,7 +384,12 @@ func ftpBackup(db *dbConfig, config *BackupConfig) { 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 err = utils.DeleteFile(filepath.Join(tmpPath, finalFileName)) if err != nil { @@ -352,8 +403,17 @@ func ftpBackup(db *dbConfig, config *BackupConfig) { } utils.Done("Uploading backup archive to the remote FTP server ... done ") + //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 deleteTemp() utils.Info("Backup completed successfully") diff --git a/pkg/var.go b/pkg/var.go index 1541f94..5264ab5 100644 --- a/pkg/var.go +++ b/pkg/var.go @@ -15,11 +15,13 @@ var ( storage = "local" file = "" - storagePath = "/backup" - workingDir = "/config" - disableCompression = false - encryption = false - usingKey = false + storagePath = "/backup" + workingDir = "/config" + disableCompression = false + encryption = false + usingKey = false + backupSize int64 = 0 + startTime string ) // dbHVars Required environment variables for database diff --git a/templates/email-error.template b/templates/email-error.template new file mode 100644 index 0000000..02b3997 --- /dev/null +++ b/templates/email-error.template @@ -0,0 +1,17 @@ + + + + + 🔴 Urgent: Database Backup Failure Notification + + +

Hi,

+

An error occurred during database backup.

+

Failure Details:

+ +

©2024 pg-bkup

+ + \ No newline at end of file diff --git a/templates/email.template b/templates/email.template new file mode 100644 index 0000000..c3b5c77 --- /dev/null +++ b/templates/email.template @@ -0,0 +1,22 @@ + + + + + ✅ Database Backup Notification – {{.Database}} + + +

Hi,

+

Backup of the {{.Database}} database has been successfully completed on {{.EndTime}}.

+

Backup Details:

+ +

Best regards,

+

©2024 pg-bkup

+ + \ No newline at end of file diff --git a/templates/telegram-error.template b/templates/telegram-error.template new file mode 100644 index 0000000..d6b465e --- /dev/null +++ b/templates/telegram-error.template @@ -0,0 +1,7 @@ +🔴 Urgent: Database Backup Failure Notification +Hi, +An error occurred during database backup. +Failure Details: +Date: {{.EndTime}} +Error Message: {{.Error}} + diff --git a/templates/telegram.template b/templates/telegram.template new file mode 100644 index 0000000..825b031 --- /dev/null +++ b/templates/telegram.template @@ -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 \ No newline at end of file diff --git a/utils/config.go b/utils/config.go new file mode 100644 index 0000000..6fcbe13 --- /dev/null +++ b/utils/config.go @@ -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" diff --git a/utils/notification.go b/utils/notification.go new file mode 100644 index 0000000..9482fec --- /dev/null +++ b/utils/notification.go @@ -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 +} diff --git a/utils/utils.go b/utils/utils.go index 15392b8..b57e064 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -7,15 +7,10 @@ package utils import ( - "bytes" - "encoding/json" "fmt" - "github.com/robfig/cron/v3" "github.com/spf13/cobra" "io" "io/fs" - "io/ioutil" - "net/http" "os" "strconv" ) @@ -185,72 +180,3 @@ func GetIntEnv(envName string) int { } 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 := "[✅ PostgreSQL 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 := "[🔴 PostgreSQL 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 -}