Compare commits

...

24 Commits

Author SHA1 Message Date
Jonas Kaninda
041e0a07e9 Merge pull request #89 from jkaninda/develop
Develop
2024-09-28 03:42:39 +02:00
Jonas Kaninda
9daac9c654 fix: scheduled mode script, remove port number 2024-09-28 03:38:26 +02:00
Jonas Kaninda
f6098769cd fix: backup database in scheduled mode 2024-09-28 03:06:09 +02:00
Jonas Kaninda
5cdfaa4d94 chore: update version in Dockerfile 2024-09-28 02:31:07 +02:00
Jonas Kaninda
b205cd61ea Fix: Using a password on the command line interface can be insecure warning message 2024-09-28 02:25:42 +02:00
Jonas Kaninda
e1307250e8 Merge pull request #88 from jkaninda/jkaninda-patch-1
Update FUNDING.yml
2024-09-12 07:59:57 +02:00
Jonas Kaninda
17ac951deb Update FUNDING.yml 2024-09-12 07:59:46 +02:00
Jonas Kaninda
6e2e08224d Merge pull request #87 from jkaninda/jkaninda-patch-1
Create FUNDING.yml
2024-09-12 07:55:10 +02:00
Jonas Kaninda
570b775f48 Create FUNDING.yml 2024-09-12 07:54:51 +02:00
Jonas Kaninda
e38e106983 Merge pull request #86 from jkaninda/docs
chore: change notification title
2024-09-12 07:10:36 +02:00
Jonas Kaninda
3040420a09 chore: change notification title 2024-09-12 07:10:09 +02:00
Jonas Kaninda
eac5f70408 Merge pull request #85 from jkaninda/docs
Docs
2024-09-12 06:34:32 +02:00
Jonas Kaninda
3476c6f529 docs: update readme 2024-09-12 06:33:38 +02:00
Jonas Kaninda
1a9c8483f8 chore: add code comment 2024-09-12 06:23:57 +02:00
Jonas Kaninda
f8722f7ae4 Merge pull request #84 from jkaninda/docs
Update Intro
2024-09-12 06:18:09 +02:00
Jonas Kaninda
421bf12910 Update Intro 2024-09-12 06:17:46 +02:00
Jonas Kaninda
3da4a27baa Merge pull request #83 from jkaninda/docs
fix: add exit after database connection test failed
2024-09-11 08:03:44 +02:00
Jonas Kaninda
0881f075ef fix: add exit after database connection test failed 2024-09-11 08:03:16 +02:00
Jonas Kaninda
066e73f8e4 Merge pull request #82 from jkaninda/docs
clean up project
2024-09-11 04:55:01 +02:00
Jonas Kaninda
645243ff77 clean up project 2024-09-11 04:53:24 +02:00
Jonas Kaninda
9384998127 Merge pull request #81 from jkaninda/docs
refactor: add Telegram env in Dockerfile, move telegram notification …
2024-09-11 04:37:50 +02:00
Jonas Kaninda
390e7dad0c refactor: add Telegram env in Dockerfile, move telegram notification to utils 2024-09-11 04:37:02 +02:00
Jonas Kaninda
67ea22385f Merge pull request #80 from jkaninda/develop
remove operation old cmd
2024-09-10 23:15:38 +02:00
Jonas Kaninda
cde82d8cfc remove operation old cmd 2024-09-10 23:14:09 +02:00
13 changed files with 109 additions and 129 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
ko_fi: jkaninda

View File

@@ -1,5 +1,5 @@
# MySQL Backup
MySQL Backup is a Docker container image that can be used to backup and restore MySQL database. It supports local storage, AWS S3 or any S3 Alternatives for Object Storage, and SSH compatible storage.
MySQL Backup is a Docker container image that can be used to backup, restore and migrate MySQL database. It supports local storage, AWS S3 or any S3 Alternatives for Object Storage, and SSH compatible storage.
It also supports __encrypting__ your backups using GPG.
The [jkaninda/mysql-bkup](https://hub.docker.com/r/jkaninda/mysql-bkup) Docker image can be deployed on Docker, Docker Swarm and Kubernetes.

View File

@@ -33,7 +33,6 @@ func Execute() {
func init() {
rootCmd.PersistentFlags().StringP("dbname", "d", "", "Database name")
rootCmd.PersistentFlags().StringVarP(&operation, "operation", "o", "", "Set operation, for old version only")
rootCmd.AddCommand(VersionCmd)
rootCmd.AddCommand(BackupCmd)
rootCmd.AddCommand(RestoreCmd)

View File

@@ -36,8 +36,10 @@ ENV TARGET_DB_NAME="localhost"
ENV TARGET_DB_USERNAME=""
ENV TARGET_DB_PASSWORD=""
ARG DEBIAN_FRONTEND=noninteractive
ENV VERSION="v1.2.5"
ENV VERSION="v1.2.7"
ENV BACKUP_CRON_EXPRESSION=""
ENV TG_TOKEN=""
ENV TG_CHAT_ID=""
ARG WORKDIR="/config"
ARG BACKUPDIR="/backup"
ARG BACKUP_TMP_DIR="/tmp/backup"

View File

@@ -1,12 +0,0 @@
FROM ruby:3.3.4
ENV LC_ALL C.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US.UTF-8
WORKDIR /usr/src/app
COPY . ./
RUN gem install bundler && bundle install
EXPOSE 4000

View File

@@ -1,13 +0,0 @@
services:
jekyll:
build:
context: ./
ports:
- 4000:4000
environment:
- JEKYLL_ENV=development
volumes:
- .:/usr/src/app
stdin_open: true
tty: true
command: bundle exec jekyll serve -H 0.0.0.0 -t

View File

@@ -19,7 +19,7 @@ import (
)
func StartBackup(cmd *cobra.Command) {
utils.Welcome()
intro()
//Set env
utils.SetEnv("STORAGE_PATH", storagePath)
utils.GetEnv(cmd, "period", "BACKUP_CRON_EXPRESSION")
@@ -116,6 +116,10 @@ func scheduledMode(db *dbConfig, storage string) {
fmt.Println(line.Text)
}
}
func intro() {
utils.Info("Starting MySQL Backup...")
utils.Info("Copyright © 2024 Jonas Kaninda ")
}
// BackupDatabase backup database
func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool) {
@@ -128,6 +132,10 @@ func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool
}
utils.Info("Starting database backup...")
err = os.Setenv("MYSQL_PWD", db.dbPassword)
if err != nil {
return
}
testDatabaseConnection(db)
// Backup Database database
@@ -139,7 +147,6 @@ func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool
"-h", db.dbHost,
"-P", db.dbPort,
"-u", db.dbUserName,
"--password="+db.dbPassword,
db.dbName,
)
output, err := cmd.Output()
@@ -162,7 +169,7 @@ func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool
} else {
// Execute mysqldump
cmd := exec.Command("mysqldump", "-h", db.dbHost, "-P", db.dbPort, "-u", db.dbUserName, "--password="+db.dbPassword, db.dbName)
cmd := exec.Command("mysqldump", "-h", db.dbHost, "-P", db.dbPort, "-u", db.dbUserName, db.dbName)
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
@@ -243,6 +250,8 @@ func s3Backup(db *dbConfig, backupFileName string, disableCompression bool, prun
//Delete temp
deleteTemp()
}
// sshBackup backup database to SSH remote server
func sshBackup(db *dbConfig, backupFileName, remotePath string, disableCompression bool, prune bool, backupRetention int, encrypt bool) {
utils.Info("Backup database to Remote server")
//Backup database
@@ -278,6 +287,8 @@ func sshBackup(db *dbConfig, backupFileName, remotePath string, disableCompressi
//Delete temp
deleteTemp()
}
// encryptBackup encrypt backup
func encryptBackup(backupFileName string) {
gpgPassphrase := os.Getenv("GPG_PASSPHRASE")
err := Encrypt(filepath.Join(tmpPath, backupFileName), gpgPassphrase)

View File

@@ -107,19 +107,19 @@ func deleteTemp() {
// TestDatabaseConnection tests the database connection
func testDatabaseConnection(db *dbConfig) {
err := os.Setenv("MYSQL_PWD", db.dbPassword)
if err != nil {
return
}
utils.Info("Connecting to %s database ...", db.dbName)
cmd := exec.Command("mysql", "-h", db.dbHost, "-P", db.dbPort, "-u", db.dbUserName, "--password="+db.dbPassword, db.dbName, "-e", "quit")
cmd := exec.Command("mysql", "-h", db.dbHost, "-P", db.dbPort, "-u", db.dbUserName, db.dbName, "-e", "quit")
// Capture the output
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
err := cmd.Run()
err = cmd.Run()
if err != nil {
utils.Error("Error testing database connection: %v\nOutput: %s", err, out.String())
os.Exit(1)
utils.Fatal("Error testing database connection: %v\nOutput: %s", err, out.String())
}
utils.Info("Successfully connected to %s database", db.dbName)

View File

@@ -14,7 +14,7 @@ import (
)
func StartMigration(cmd *cobra.Command) {
utils.Welcome()
intro()
utils.Info("Starting database migration...")
//Get DB config
dbConf = getDbConfig(cmd)

View File

@@ -16,7 +16,7 @@ import (
)
func StartRestore(cmd *cobra.Command) {
utils.Welcome()
intro()
//Set env
utils.SetEnv("STORAGE_PATH", storagePath)
@@ -95,13 +95,17 @@ func RestoreDatabase(db *dbConfig, file string) {
}
if utils.FileExists(fmt.Sprintf("%s/%s", tmpPath, file)) {
err = os.Setenv("MYSQL_PWD", db.dbPassword)
if err != nil {
return
}
testDatabaseConnection(db)
utils.Info("Restoring database...")
extension := filepath.Ext(fmt.Sprintf("%s/%s", tmpPath, file))
// Restore from compressed file / .sql.gz
if extension == ".gz" {
str := "zcat " + fmt.Sprintf("%s/%s", tmpPath, file) + " | mysql -h " + db.dbHost + " -P " + db.dbPort + " -u " + db.dbUserName + " --password=" + db.dbPassword + " " + db.dbName
str := "zcat " + filepath.Join(tmpPath, file) + " | mysql -h " + db.dbHost + " -P " + db.dbPort + " -u " + db.dbUserName + " " + db.dbName
_, err := exec.Command("bash", "-c", str).Output()
if err != nil {
utils.Fatal("Error, in restoring the database %v", err)
@@ -113,20 +117,20 @@ func RestoreDatabase(db *dbConfig, file string) {
} else if extension == ".sql" {
//Restore from sql file
str := "cat " + fmt.Sprintf("%s/%s", tmpPath, file) + " | mysql -h " + db.dbHost + " -P " + db.dbPort + " -u " + db.dbUserName + " --password=" + db.dbPassword + " " + db.dbName
str := "cat " + filepath.Join(tmpPath, file) + " | mysql -h " + db.dbHost + " -P " + db.dbPort + " -u " + db.dbUserName + " " + db.dbName
_, err := exec.Command("bash", "-c", str).Output()
if err != nil {
utils.Fatal(fmt.Sprintf("Error in restoring the database %s", err))
utils.Fatal("Error in restoring the database %v", err)
}
utils.Info("Restoring database... done")
utils.Done("Database has been restored")
//Delete temp
deleteTemp()
} else {
utils.Fatal(fmt.Sprintf("Unknown file extension %s", extension))
utils.Fatal("Unknown file extension %s", extension)
}
} else {
utils.Fatal(fmt.Sprintf("File not found in %s", fmt.Sprintf("%s/%s", tmpPath, file)))
utils.Fatal("File not found in %s", filepath.Join(tmpPath, file))
}
}

View File

@@ -26,8 +26,8 @@ func CreateCrontabScript(disableCompression bool, storage string) {
scriptContent := fmt.Sprintf(`#!/usr/bin/env bash
set -e
/usr/local/bin/mysql-bkup backup --dbname %s --port %s --storage %s %v
`, os.Getenv("DB_NAME"), os.Getenv("DB_PORT"), storage, disableC)
/usr/local/bin/mysql-bkup backup --dbname %s --storage %s %v
`, os.Getenv("DB_NAME"), storage, disableC)
if err := utils.WriteToFile(backupCronFile, scriptContent); err != nil {
utils.Fatal("Error writing to %s: %v\n", backupCronFile, err)

View File

@@ -1,75 +0,0 @@
package utils
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
)
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"))
}

View File

@@ -7,10 +7,14 @@
package utils
import (
"bytes"
"encoding/json"
"fmt"
"github.com/spf13/cobra"
"io"
"io/fs"
"io/ioutil"
"net/http"
"os"
"strconv"
)
@@ -153,13 +157,6 @@ func CheckEnvVars(vars []string) error {
return nil
}
func Welcome() {
fmt.Println()
fmt.Println("**********************************")
fmt.Println(" MySQL Backup ")
fmt.Println(" @Copyright © 2024 jkaninda ")
fmt.Println("***********************************")
}
// MakeDir create directory
func MakeDir(dirPath string) error {
@@ -189,3 +186,67 @@ 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 := "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"))
}