mirror of
https://github.com/jkaninda/mysql-bkup.git
synced 2025-12-06 13:39:41 +01:00
Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd444293b4 | |||
|
|
1940ceba9a | ||
|
|
07d580a8a9 | ||
| 9a261b22ec | |||
|
|
e7a58f0569 | ||
| 1b529725d7 | |||
|
|
d8c73560b8 | ||
|
|
d5a0adc981 | ||
| 6df3bae9e2 | |||
|
|
f7d624fd15 | ||
| 1e9e1ed951 | |||
|
|
917ba8947f | ||
| 94a1dcdff7 | |||
|
|
f70e549b16 | ||
|
|
607478fcc6 | ||
| 2862e504f5 | |||
|
|
29420ee13e | ||
| f53272ccf0 | |||
|
|
c360441445 | ||
|
|
f6916231f7 | ||
|
|
afd4afc83b | ||
|
|
9016a9ec7a | ||
| 4ecd96e75c | |||
|
|
8a88e4a727 | ||
| 62f86adea9 | |||
| eb414d818c | |||
| 6721cc430d | |||
|
|
8e20e9595f | ||
| 02e3267237 | |||
|
|
448ef4d988 | ||
| 70ac78c2cd | |||
|
|
72f5ef4839 | ||
| 6a51f591a5 | |||
| d55ade3c21 | |||
|
|
cdbd6dcd6a | ||
|
|
307e18d9ff | ||
| 8d366f0302 | |||
| 05e32c3cc1 | |||
|
|
edd13907d0 | ||
|
|
7cb1c50927 | ||
| f545704b02 | |||
| 90f5391b24 | |||
| ca241b4fef | |||
|
|
3911296921 | ||
| 8d04d276ba | |||
|
|
221079e0ea | ||
| 590b2d8bc6 | |||
|
|
d2aeb55ebc | ||
|
|
431be36210 | ||
| ef2c5c80cd | |||
|
|
3a0137d6ea | ||
|
|
8afb5ace40 | ||
|
|
5569258a71 | ||
|
|
f3ec395e37 | ||
| ba432997c8 | |||
|
|
dc20ea9635 | ||
| 40557af437 | |||
|
|
1dcb9586a6 | ||
|
|
2c6336e84a | ||
| c16ee3a492 | |||
|
|
3f7d28ea49 | ||
| cea1ef9c3b | |||
|
|
56c271bc29 | ||
| 45c30dca5f | |||
|
|
b0ae212578 | ||
|
|
6e2d3a9f21 | ||
|
|
dd314aa4cb | ||
|
|
24ccdaa671 | ||
| 45e3452376 | |||
|
|
3527b4cdcd | ||
| dc6fe2f4b9 | |||
|
|
f0afc0f4e0 | ||
| 7d7c813bb0 | |||
|
|
6b8491cdc0 | ||
| a1dd6e3f58 | |||
|
|
86ba3530c9 | ||
| e1f3b15003 | |||
|
|
1577e92a66 | ||
| 7b67f88769 | |||
|
|
043233dabe | ||
|
|
d6652cfb75 | ||
| 140ed608ab | |||
|
|
98211a27b8 | ||
| 4e4d45e555 | |||
|
|
01e41acb5c | ||
| 3dce2017f8 | |||
|
|
ed2f1b8d9c | ||
| b64875df21 | |||
|
|
fc90507b3f | ||
| df0efd24d3 | |||
|
|
e5dd7e76ce | ||
| 12fbb67a09 | |||
|
|
df490af7b6 | ||
| d930c3e2f6 | |||
|
|
e4258cb12e | ||
| 4c44166921 | |||
| 554df819ab | |||
|
|
ca5633882e | ||
| c5cca82841 | |||
|
|
bbd5422089 | ||
|
|
d72156f890 | ||
|
|
909a50dbe7 | ||
|
|
94ceb71da2 | ||
|
|
fe05fe5110 | ||
| dabba2050a | |||
|
|
47e1ac407b | ||
| 28f6ed3a82 | |||
|
|
504926c7cd | ||
| 737f473f92 | |||
|
|
300d2a8205 | ||
|
|
a4ad0502cf | ||
| f344867edf | |||
|
|
d774584f64 | ||
| 96927cd57e | |||
|
|
ceacfa1d9d | ||
|
|
9380a18b45 | ||
|
|
d186071df9 | ||
|
|
71429b0e1a | ||
|
|
0bed86ded4 | ||
|
|
e891801125 | ||
|
|
01cf8a3392 | ||
|
|
efea81833a | ||
|
|
1cbf65d686 | ||
|
|
73d19913f8 | ||
|
|
b0224e43ef | ||
|
|
fa0485bb5a | ||
|
|
65ef6d3e8f | ||
|
|
a7b6abb101 | ||
|
|
3b21c109bc | ||
|
|
a50a1ef6f9 | ||
|
|
76bbfa35c4 | ||
|
|
599d93bef4 | ||
|
|
247e90f73e | ||
|
|
7d544aca68 | ||
|
|
1722ee0eeb | ||
|
|
726fd14831 | ||
|
|
fdc88e6064 | ||
|
|
2ba1b516e9 | ||
|
|
301594676b | ||
|
|
d06f2f2d7e | ||
|
|
2f06bd1c3a | ||
|
|
f383f5559d | ||
|
|
3725809d28 | ||
|
|
b1598ef7d0 | ||
|
|
e4a83b9851 | ||
|
|
4b2527f416 | ||
|
|
e97fc7512a | ||
|
|
7912ce46ed | ||
|
|
050f5e81bc | ||
|
|
b39e97b77d | ||
|
|
cbb73ae89b | ||
|
|
29a58aa26d |
69
.env.example
Normal file
69
.env.example
Normal file
@@ -0,0 +1,69 @@
|
||||
### Database
|
||||
DB_HOST=
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=
|
||||
DB_PASSWORD=
|
||||
DB_NAME=
|
||||
TZ=Europe/Paris
|
||||
|
||||
### Database Migration
|
||||
#TARGET_DB_HOST=
|
||||
#TARGET_DB_PORT=3306
|
||||
#TARGET_DB_NAME=
|
||||
#TARGET_DB_USERNAME=
|
||||
#TARGET_DB_PASSWORD=
|
||||
|
||||
### Backup restoration
|
||||
#FILE_NAME=
|
||||
### AWS S3 Storage
|
||||
#ACCESS_KEY=
|
||||
#SECRET_KEY=
|
||||
#AWS_S3_BUCKET_NAME=
|
||||
#AWS_S3_ENDPOINT=
|
||||
#AWS_REGION=
|
||||
#AWS_S3_PATH=
|
||||
#AWS_DISABLE_SSL=false
|
||||
#AWS_FORCE_PATH_STYLE=true
|
||||
|
||||
### Backup Cron Expression
|
||||
#BACKUP_CRON_EXPRESSION=@midnight
|
||||
##Delete old backup created more than specified days ago
|
||||
#BACKUP_RETENTION_DAYS=7
|
||||
|
||||
####SSH Storage
|
||||
#SSH_HOST_NAME=
|
||||
#SSH_PORT=22
|
||||
#SSH_USER=
|
||||
#SSH_PASSWORD=
|
||||
#SSH_IDENTIFY_FILE=/tmp/id_ed25519
|
||||
|
||||
####FTP Storage
|
||||
#FTP_PASSWORD=
|
||||
#FTP_HOST_NAME=
|
||||
#FTP_USER=
|
||||
#FTP_PORT=21
|
||||
#REMOTE_PATH=
|
||||
#### Backup encryption
|
||||
#GPG_PUBLIC_KEY=/config/public_key.asc
|
||||
#GPG_PRIVATE_KEY=/config/private_key.asc
|
||||
#GPG_PASSPHRASE=Your strong passphrase
|
||||
## For multiple database backup on Docker or Docker in Swarm mode
|
||||
#BACKUP_CONFIG_FILE=/config/config.yaml
|
||||
### Database restoration
|
||||
#FILE_NAME=
|
||||
### Notification
|
||||
#BACKUP_REFERENCE=K8s/Paris cluster
|
||||
## Telegram
|
||||
#TG_TOKEN=
|
||||
#TG_CHAT_ID=
|
||||
### Email
|
||||
#MAIL_HOST=
|
||||
#MAIL_PORT=
|
||||
#MAIL_USERNAME=
|
||||
#MAIL_PASSWORD=
|
||||
#MAIL_FROM=Backup Jobs <backup-jobs@example.com>
|
||||
#MAIL_TO=backup@example.com,me@example.com,team@example.com
|
||||
#MAIL_SKIP_TLS=false
|
||||
|
||||
|
||||
|
||||
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: docker
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -25,8 +25,10 @@ jobs:
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
file: "./docker/Dockerfile"
|
||||
file: "./Dockerfile"
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
build-args: |
|
||||
appVersion=develop-${{ github.sha }}
|
||||
tags: |
|
||||
"${{env.BUILDKIT_IMAGE}}:develop-${{ github.sha }}"
|
||||
"${{vars.BUILDKIT_IMAGE}}:develop-${{ github.sha }}"
|
||||
|
||||
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -39,11 +39,13 @@ jobs:
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
file: "./docker/Dockerfile"
|
||||
file: "./Dockerfile"
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
build-args: |
|
||||
appVersion=${{ env.TAG_NAME }}
|
||||
tags: |
|
||||
"${{env.BUILDKIT_IMAGE}}:${{ env.TAG_NAME }}"
|
||||
"${{env.BUILDKIT_IMAGE}}:latest"
|
||||
"ghcr.io/${{env.BUILDKIT_IMAGE}}:${{ env.TAG_NAME }}"
|
||||
"ghcr.io/${{env.BUILDKIT_IMAGE}}:latest"
|
||||
"${{vars.BUILDKIT_IMAGE}}:${{ env.TAG_NAME }}"
|
||||
"${{vars.BUILDKIT_IMAGE}}:latest"
|
||||
"ghcr.io/${{vars.BUILDKIT_IMAGE}}:${{ env.TAG_NAME }}"
|
||||
"ghcr.io/${{vars.BUILDKIT_IMAGE}}:latest"
|
||||
|
||||
|
||||
43
.golangci.yml
Normal file
43
.golangci.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
allow-parallel-runners: true
|
||||
|
||||
issues:
|
||||
# don't skip warning about doc comments
|
||||
# don't exclude the default set of lint
|
||||
exclude-use-default: false
|
||||
# restore some of the defaults
|
||||
# (fill in the rest as needed)
|
||||
exclude-rules:
|
||||
- path: "internal/*"
|
||||
linters:
|
||||
- dupl
|
||||
- lll
|
||||
- goimports
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- dupl
|
||||
- errcheck
|
||||
- copyloopvar
|
||||
- ginkgolinter
|
||||
- goconst
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- nakedret
|
||||
- prealloc
|
||||
- revive
|
||||
- staticcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
|
||||
linters-settings:
|
||||
revive:
|
||||
rules:
|
||||
- name: comment-spacings
|
||||
44
Dockerfile
Normal file
44
Dockerfile
Normal file
@@ -0,0 +1,44 @@
|
||||
FROM golang:1.23.3 AS build
|
||||
WORKDIR /app
|
||||
ARG appVersion=""
|
||||
|
||||
# Copy the source code.
|
||||
COPY . .
|
||||
# Installs Go dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Build
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-X 'github.com/jkaninda/mysql-bkup/utils.Version=${appVersion}'" -o /app/mysql-bkup
|
||||
|
||||
FROM alpine:3.21.0
|
||||
ENV TZ=UTC
|
||||
ARG WORKDIR="/config"
|
||||
ARG BACKUPDIR="/backup"
|
||||
ARG BACKUP_TMP_DIR="/tmp/backup"
|
||||
ARG TEMPLATES_DIR="/config/templates"
|
||||
ARG appVersion=""
|
||||
ENV VERSION=${appVersion}
|
||||
LABEL author="Jonas Kaninda"
|
||||
LABEL version=${appVersion}
|
||||
LABEL github="github.com/jkaninda/mysql-bkup"
|
||||
|
||||
RUN apk --update add --no-cache mysql-client mariadb-connector-c tzdata ca-certificates
|
||||
RUN mkdir -p $WORKDIR $BACKUPDIR $TEMPLATES_DIR $BACKUP_TMP_DIR && \
|
||||
chmod a+rw $WORKDIR $BACKUPDIR $BACKUP_TMP_DIR
|
||||
COPY --from=build /app/mysql-bkup /usr/local/bin/mysql-bkup
|
||||
COPY ./templates/* $TEMPLATES_DIR/
|
||||
RUN chmod +x /usr/local/bin/mysql-bkup && \
|
||||
ln -s /usr/local/bin/mysql-bkup /usr/local/bin/bkup
|
||||
|
||||
# Create backup script and make it executable
|
||||
RUN printf '#!/bin/sh\n/usr/local/bin/mysql-bkup backup "$@"' > /usr/local/bin/backup && \
|
||||
chmod +x /usr/local/bin/backup
|
||||
# Create restore script and make it executable
|
||||
RUN printf '#!/bin/sh\n/usr/local/bin/mysql-bkup restore "$@"' > /usr/local/bin/restore && \
|
||||
chmod +x /usr/local/bin/restore
|
||||
# Create migrate script and make it executable
|
||||
RUN printf '#!/bin/sh\n/usr/local/bin/mysql-bkup migrate "$@"' > /usr/local/bin/migrate && \
|
||||
chmod +x /usr/local/bin/migrate
|
||||
|
||||
WORKDIR $WORKDIR
|
||||
ENTRYPOINT ["/usr/local/bin/mysql-bkup"]
|
||||
68
README.md
68
README.md
@@ -1,11 +1,7 @@
|
||||
# MySQL Backup
|
||||
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.
|
||||
# MYSQL-BKUP
|
||||
|
||||
The [jkaninda/mysql-bkup](https://hub.docker.com/r/jkaninda/mysql-bkup) Docker image can be deployed on Docker, Docker Swarm and Kubernetes.
|
||||
It handles __recurring__ backups of postgres database on Docker and can be deployed as __CronJob on Kubernetes__ using local, AWS S3 or SSH compatible storage.
|
||||
|
||||
It also supports database __encryption__ using GPG.
|
||||
**MYSQL-BKUP** is a Docker container image designed to **backup, restore, and migrate MySQL databases**.
|
||||
It supports a variety of storage options and ensures data security through GPG encryption.
|
||||
|
||||
[](https://github.com/jkaninda/mysql-bkup/actions/workflows/release.yml)
|
||||
[](https://goreportcard.com/report/github.com/jkaninda/mysql-bkup)
|
||||
@@ -13,6 +9,37 @@ It also supports database __encryption__ using GPG.
|
||||

|
||||
<a href="https://ko-fi.com/jkaninda"><img src="https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/5cbed8a4ae2b88347c06c923_BuyMeACoffee_blue.png" height="20" alt="buy ma a coffee"></a>
|
||||
|
||||
## Features
|
||||
|
||||
- **Storage Options:**
|
||||
- Local storage
|
||||
- AWS S3 or any S3-compatible object storage
|
||||
- FTP
|
||||
- SSH-compatible storage
|
||||
- Azure Blob storage
|
||||
|
||||
- **Data Security:**
|
||||
- Backups can be encrypted using **GPG** to ensure confidentiality.
|
||||
|
||||
- **Deployment Flexibility:**
|
||||
- Available as the [jkaninda/mysql-bkup](https://hub.docker.com/r/jkaninda/mysql-bkup) Docker image.
|
||||
- Deployable on **Docker**, **Docker Swarm**, and **Kubernetes**.
|
||||
- Supports recurring backups of MySQL databases when deployed:
|
||||
- On Docker for automated backup schedules.
|
||||
- As a **Job** or **CronJob** on Kubernetes.
|
||||
|
||||
- **Notifications:**
|
||||
- Get real-time updates on backup success or failure via:
|
||||
- **Telegram**
|
||||
- **Email**
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Automated Recurring Backups:** Schedule regular backups for MySQL databases.
|
||||
- **Cross-Environment Migration:** Easily migrate your MySQL databases across different environments using supported storage options.
|
||||
- **Secure Backup Management:** Protect your data with GPG encryption.
|
||||
|
||||
|
||||
Successfully tested on:
|
||||
- Docker
|
||||
- Docker in Swarm mode
|
||||
@@ -34,8 +61,9 @@ Successfully tested on:
|
||||
## Storage:
|
||||
- Local
|
||||
- AWS S3 or any S3 Alternatives for Object Storage
|
||||
- SSH remote server
|
||||
|
||||
- SSH remote storage server
|
||||
- FTP remote storage server
|
||||
- Azure Blob storage
|
||||
## Quickstart
|
||||
|
||||
### Simple backup using Docker CLI
|
||||
@@ -80,12 +108,25 @@ services:
|
||||
- DB_NAME=foo
|
||||
- DB_USERNAME=bar
|
||||
- DB_PASSWORD=password
|
||||
- TZ=Europe/Paris
|
||||
# mysql-bkup container must be connected to the same network with your database
|
||||
networks:
|
||||
- web
|
||||
networks:
|
||||
web:
|
||||
```
|
||||
### Docker recurring backup
|
||||
|
||||
```shell
|
||||
docker run --rm --network network_name \
|
||||
-v $PWD/backup:/backup/ \
|
||||
-e "DB_HOST=hostname" \
|
||||
-e "DB_USERNAME=user" \
|
||||
-e "DB_PASSWORD=password" \
|
||||
jkaninda/mysql-bkup backup -d dbName --cron-expression "@every 15m" #@midnight
|
||||
```
|
||||
See: https://jkaninda.github.io/mysql-bkup/reference/#predefined-schedules
|
||||
|
||||
## Deploy on Kubernetes
|
||||
|
||||
For Kubernetes, you don't need to run it in scheduled mode. You can deploy it as Job or CronJob.
|
||||
@@ -102,7 +143,7 @@ spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: pg-bkup
|
||||
- name: mysql-bkup
|
||||
# In production, it is advised to lock your image tag to a proper
|
||||
# release version instead of using `latest`.
|
||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
||||
@@ -145,16 +186,11 @@ docker pull ghcr.io/jkaninda/mysql-bkup
|
||||
|
||||
Documentation references Docker Hub, but all examples will work using ghcr.io just as well.
|
||||
|
||||
## Supported Engines
|
||||
|
||||
This image is developed and tested against the Docker CE engine and Kubernetes exclusively.
|
||||
While it may work against different implementations, there are no guarantees about support for non-Docker engines.
|
||||
|
||||
## References
|
||||
|
||||
We decided to publish this image as a simpler and more lightweight alternative because of the following requirements:
|
||||
|
||||
- The original image is based on `ubuntu` and requires additional tools, making it heavy.
|
||||
- The original image is based on `Alpine` and requires additional tools, making it heavy.
|
||||
- This image is written in Go.
|
||||
- `arm64` and `arm/v7` architectures are supported.
|
||||
- Docker in Swarm mode is supported.
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
// Package cmd /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
@@ -20,19 +38,16 @@ var BackupCmd = &cobra.Command{
|
||||
if len(args) == 0 {
|
||||
pkg.StartBackup(cmd)
|
||||
} else {
|
||||
utils.Fatal("Error, no argument required")
|
||||
utils.Fatal(`"backup" accepts no argument %q`, args)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
//Backup
|
||||
BackupCmd.PersistentFlags().StringP("storage", "s", "local", "Storage. local or s3")
|
||||
BackupCmd.PersistentFlags().StringP("storage", "s", "local", "Define storage: local, s3, ssh, ftp")
|
||||
BackupCmd.PersistentFlags().StringP("path", "P", "", "AWS S3 path without file name. eg: /custom_path or ssh remote path `/home/foo/backup`")
|
||||
BackupCmd.PersistentFlags().StringP("mode", "m", "default", "Execution mode. default or scheduled")
|
||||
BackupCmd.PersistentFlags().StringP("period", "", "0 1 * * *", "Schedule period time")
|
||||
BackupCmd.PersistentFlags().BoolP("prune", "", false, "Delete old backup, default disabled")
|
||||
BackupCmd.PersistentFlags().IntP("keep-last", "", 7, "Delete files created more than specified days ago, default 7 days")
|
||||
BackupCmd.PersistentFlags().StringP("cron-expression", "", "", "Backup cron expression")
|
||||
BackupCmd.PersistentFlags().BoolP("disable-compression", "", false, "Disable backup compression")
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
// Package cmd /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
@@ -19,7 +37,7 @@ var MigrateCmd = &cobra.Command{
|
||||
if len(args) == 0 {
|
||||
pkg.StartMigration(cmd)
|
||||
} else {
|
||||
utils.Fatal("Error, no argument required")
|
||||
utils.Fatal(`"migrate" accepts no argument %q`, args)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
@@ -14,7 +38,7 @@ var RestoreCmd = &cobra.Command{
|
||||
if len(args) == 0 {
|
||||
pkg.StartRestore(cmd)
|
||||
} else {
|
||||
utils.Fatal("Error, no argument required")
|
||||
utils.Fatal(`"restore" accepts no argument %q`, args)
|
||||
|
||||
}
|
||||
|
||||
@@ -24,7 +48,7 @@ var RestoreCmd = &cobra.Command{
|
||||
func init() {
|
||||
//Restore
|
||||
RestoreCmd.PersistentFlags().StringP("file", "f", "", "File name of database")
|
||||
RestoreCmd.PersistentFlags().StringP("storage", "s", "local", "Storage. local or s3")
|
||||
RestoreCmd.PersistentFlags().StringP("storage", "s", "local", "Define storage: local, s3, ssh, ftp")
|
||||
RestoreCmd.PersistentFlags().StringP("path", "P", "", "AWS S3 path without file name. eg: /custom_path or ssh remote path `/home/foo/backup`")
|
||||
|
||||
}
|
||||
|
||||
30
cmd/root.go
30
cmd/root.go
@@ -1,9 +1,27 @@
|
||||
// Package cmd /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
// Package cmd /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jkaninda/mysql-bkup/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
)
|
||||
@@ -23,6 +42,6 @@ var VersionCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func Version() {
|
||||
fmt.Printf("Version: %s \n", appVersion)
|
||||
fmt.Printf("Version: %s \n", utils.Version)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
FROM golang:1.22.5 AS build
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the source code.
|
||||
COPY . .
|
||||
# Installs Go dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Build
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/mysql-bkup
|
||||
|
||||
FROM ubuntu:24.04
|
||||
ENV DB_HOST="localhost"
|
||||
ENV DB_NAME=""
|
||||
ENV DB_USERNAME=""
|
||||
ENV DB_PASSWORD=""
|
||||
ENV DB_PORT=3306
|
||||
ENV STORAGE=local
|
||||
ENV AWS_S3_ENDPOINT=""
|
||||
ENV AWS_S3_BUCKET_NAME=""
|
||||
ENV AWS_ACCESS_KEY=""
|
||||
ENV AWS_SECRET_KEY=""
|
||||
ENV AWS_REGION="us-west-2"
|
||||
ENV AWS_S3_PATH=""
|
||||
ENV AWS_DISABLE_SSL="false"
|
||||
ENV GPG_PASSPHRASE=""
|
||||
ENV SSH_USER=""
|
||||
ENV SSH_REMOTE_PATH=""
|
||||
ENV SSH_PASSWORD=""
|
||||
ENV SSH_HOST_NAME=""
|
||||
ENV SSH_IDENTIFY_FILE=""
|
||||
ENV SSH_PORT="22"
|
||||
ENV TARGET_DB_HOST=""
|
||||
ENV TARGET_DB_PORT=3306
|
||||
ENV TARGET_DB_NAME="localhost"
|
||||
ENV TARGET_DB_USERNAME=""
|
||||
ENV TARGET_DB_PASSWORD=""
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
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"
|
||||
ARG BACKUP_CRON="/etc/cron.d/backup_cron"
|
||||
ARG BACKUP_CRON_SCRIPT="/usr/local/bin/backup_cron.sh"
|
||||
LABEL author="Jonas Kaninda"
|
||||
|
||||
RUN apt-get update -qq
|
||||
RUN apt install mysql-client supervisor cron gnupg -y
|
||||
|
||||
# Clear cache
|
||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir $WORKDIR
|
||||
RUN mkdir $BACKUPDIR
|
||||
RUN mkdir -p $BACKUP_TMP_DIR
|
||||
RUN chmod 777 $WORKDIR
|
||||
RUN chmod 777 $BACKUPDIR
|
||||
RUN chmod 777 $BACKUP_TMP_DIR
|
||||
RUN touch $BACKUP_CRON && \
|
||||
touch $BACKUP_CRON_SCRIPT && \
|
||||
chmod 777 $BACKUP_CRON && \
|
||||
chmod 777 $BACKUP_CRON_SCRIPT
|
||||
|
||||
COPY --from=build /app/mysql-bkup /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
|
||||
|
||||
ADD docker/supervisord.conf /etc/supervisor/supervisord.conf
|
||||
|
||||
# Create backup script and make it executable
|
||||
RUN echo '#!/bin/sh\n/usr/local/bin/mysql-bkup backup "$@"' > /usr/local/bin/backup && \
|
||||
chmod +x /usr/local/bin/backup
|
||||
# Create restore script and make it executable
|
||||
RUN echo '#!/bin/sh\n/usr/local/bin/mysql-bkup restore "$@"' > /usr/local/bin/restore && \
|
||||
chmod +x /usr/local/bin/restore
|
||||
# Create migrate script and make it executable
|
||||
RUN echo '#!/bin/sh\n/usr/local/bin/mysql-bkup migrate "$@"' > /usr/local/bin/migrate && \
|
||||
chmod +x /usr/local/bin/migrate
|
||||
|
||||
WORKDIR $WORKDIR
|
||||
ENTRYPOINT ["/usr/local/bin/mysql-bkup"]
|
||||
@@ -1,13 +0,0 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=root
|
||||
logfile=/var/log/supervisor/supervisord.log
|
||||
pidfile=/var/run/supervisord.pid
|
||||
|
||||
[program:cron]
|
||||
command = /bin/bash -c "declare -p | grep -Ev '^declare -[[:alpha:]]*r' > /run/supervisord.env && /usr/sbin/cron -f -L 15"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
user = root
|
||||
stderr_logfile=/var/log/cron.err.log
|
||||
stdout_logfile=/var/log/cron.out.log
|
||||
BIN
docs/favicon.ico
Normal file
BIN
docs/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
44
docs/how-tos/azure-blob.md
Normal file
44
docs/how-tos/azure-blob.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Azure Blob storage
|
||||
layout: default
|
||||
parent: How Tos
|
||||
nav_order: 5
|
||||
---
|
||||
# Azure Blob storage
|
||||
|
||||
{: .note }
|
||||
As described on local backup section, to change the storage of you backup and use Azure Blob as storage. You need to add `--storage azure` (-s azure).
|
||||
You can also specify a folder where you want to save you data by adding `--path my-custom-path` flag.
|
||||
|
||||
|
||||
## Backup to Azure Blob storage
|
||||
|
||||
```yml
|
||||
services:
|
||||
mysql-bkup:
|
||||
# In production, it is advised to lock your image tag to a proper
|
||||
# release version instead of using `latest`.
|
||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
||||
# for a list of available releases.
|
||||
image: jkaninda/mysql-bkup
|
||||
container_name: mysql-bkup
|
||||
command: backup --storage azure -d database --path my-custom-path
|
||||
environment:
|
||||
- DB_PORT=3306
|
||||
- DB_HOST=mysql
|
||||
- DB_NAME=database
|
||||
- DB_USERNAME=username
|
||||
- DB_PASSWORD=password
|
||||
## Azure Blob configurations
|
||||
- AZURE_STORAGE_CONTAINER_NAME=backup-container
|
||||
- AZURE_STORAGE_ACCOUNT_NAME=account-name
|
||||
- AZURE_STORAGE_ACCOUNT_KEY=Ppby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
|
||||
# mysql-bkup container must be connected to the same network with your database
|
||||
networks:
|
||||
- web
|
||||
networks:
|
||||
web:
|
||||
```
|
||||
|
||||
|
||||
|
||||
44
docs/how-tos/backup-to-ftp.md
Normal file
44
docs/how-tos/backup-to-ftp.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Backup to FTP remote server
|
||||
layout: default
|
||||
parent: How Tos
|
||||
nav_order: 4
|
||||
---
|
||||
# Backup to FTP remote server
|
||||
|
||||
|
||||
As described for SSH backup section, to change the storage of your backup and use FTP Remote server as storage. You need to add `--storage ftp`.
|
||||
You need to add the full remote path by adding `--path /home/jkaninda/backups` flag or using `REMOTE_PATH` environment variable.
|
||||
|
||||
{: .note }
|
||||
These environment variables are required for SSH backup `FTP_HOST`, `FTP_USER`, `REMOTE_PATH`, `FTP_PORT` or `FTP_PASSWORD`.
|
||||
|
||||
```yml
|
||||
services:
|
||||
mysql-bkup:
|
||||
# In production, it is advised to lock your image tag to a proper
|
||||
# release version instead of using `latest`.
|
||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
||||
# for a list of available releases.
|
||||
image: jkaninda/mysql-bkup
|
||||
container_name: mysql-bkup
|
||||
command: backup --storage ftp -d database
|
||||
environment:
|
||||
- DB_PORT=3306
|
||||
- DB_HOST=postgres
|
||||
- DB_NAME=database
|
||||
- DB_USERNAME=username
|
||||
- DB_PASSWORD=password
|
||||
## FTP config
|
||||
- FTP_HOST="hostname"
|
||||
- FTP_PORT=21
|
||||
- FTP_USER=user
|
||||
- FTP_PASSWORD=password
|
||||
- REMOTE_PATH=/home/jkaninda/backups
|
||||
|
||||
# pg-bkup container must be connected to the same network with your database
|
||||
networks:
|
||||
- web
|
||||
networks:
|
||||
web:
|
||||
```
|
||||
@@ -37,6 +37,7 @@ services:
|
||||
- AWS_SECRET_KEY=xxxxx
|
||||
## In case you are using S3 alternative such as Minio and your Minio instance is not secured, you change it to true
|
||||
- AWS_DISABLE_SSL="false"
|
||||
- AWS_FORCE_PATH_STYLE=true # true for S3 alternative such as Minio
|
||||
|
||||
# mysql-bkup container must be connected to the same network with your database
|
||||
networks:
|
||||
@@ -48,7 +49,7 @@ networks:
|
||||
### Recurring backups to S3
|
||||
|
||||
As explained above, you need just to add AWS environment variables and specify the storage type `--storage s3`.
|
||||
In case you need to use recurring backups, you can use `--mode scheduled` and specify the periodical backup time by adding `--period "0 1 * * *"` flag as described below.
|
||||
In case you need to use recurring backups, you can use `--cron-expression "0 1 * * *"` flag or `BACKUP_CRON_EXPRESSION=0 1 * * *` as described below.
|
||||
|
||||
```yml
|
||||
services:
|
||||
@@ -59,7 +60,7 @@ services:
|
||||
# for a list of available releases.
|
||||
image: jkaninda/mysql-bkup
|
||||
container_name: mysql-bkup
|
||||
command: backup --storage s3 -d my-database --mode scheduled --period "0 1 * * *"
|
||||
command: backup --storage s3 -d my-database --cron-expression "0 1 * * *"
|
||||
environment:
|
||||
- DB_PORT=3306
|
||||
- DB_HOST=mysql
|
||||
@@ -72,8 +73,12 @@ services:
|
||||
- AWS_REGION="us-west-2"
|
||||
- AWS_ACCESS_KEY=xxxx
|
||||
- AWS_SECRET_KEY=xxxxx
|
||||
# - BACKUP_CRON_EXPRESSION=0 1 * * * # Optional
|
||||
#Delete old backup created more than specified days ago
|
||||
#- BACKUP_RETENTION_DAYS=7
|
||||
## In case you are using S3 alternative such as Minio and your Minio instance is not secured, you change it to true
|
||||
- AWS_DISABLE_SSL="false"
|
||||
- AWS_FORCE_PATH_STYLE=true # true for S3 alternative such as Minio
|
||||
# mysql-bkup container must be connected to the same network with your database
|
||||
networks:
|
||||
- web
|
||||
@@ -81,53 +86,3 @@ networks:
|
||||
web:
|
||||
```
|
||||
|
||||
## Deploy on Kubernetes
|
||||
|
||||
For Kubernetes, you don't need to run it in scheduled mode. You can deploy it as CronJob.
|
||||
|
||||
### Simple Kubernetes CronJob usage:
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: bkup-job
|
||||
spec:
|
||||
schedule: "0 1 * * *"
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: mysql-bkup
|
||||
image: jkaninda/mysql-bkup
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- backup -s s3 --path /custom_path
|
||||
env:
|
||||
- name: DB_PORT
|
||||
value: "3306"
|
||||
- name: DB_HOST
|
||||
value: ""
|
||||
- name: DB_NAME
|
||||
value: ""
|
||||
- name: DB_USERNAME
|
||||
value: ""
|
||||
# Please use secret!
|
||||
- name: DB_PASSWORD
|
||||
value: ""
|
||||
- name: AWS_S3_ENDPOINT
|
||||
value: "https://s3.amazonaws.com"
|
||||
- name: AWS_S3_BUCKET_NAME
|
||||
value: "xxx"
|
||||
- name: AWS_REGION
|
||||
value: "us-west-2"
|
||||
- name: AWS_ACCESS_KEY
|
||||
value: "xxxx"
|
||||
- name: AWS_SECRET_KEY
|
||||
value: "xxxx"
|
||||
- name: AWS_DISABLE_SSL
|
||||
value: "false"
|
||||
restartPolicy: OnFailure
|
||||
```
|
||||
@@ -8,10 +8,10 @@ nav_order: 3
|
||||
|
||||
|
||||
As described for s3 backup section, to change the storage of your backup and use SSH Remote server as storage. You need to add `--storage ssh` or `--storage remote`.
|
||||
You need to add the full remote path by adding `--path /home/jkaninda/backups` flag or using `SSH_REMOTE_PATH` environment variable.
|
||||
You need to add the full remote path by adding `--path /home/jkaninda/backups` flag or using `REMOTE_PATH` environment variable.
|
||||
|
||||
{: .note }
|
||||
These environment variables are required for SSH backup `SSH_HOST_NAME`, `SSH_USER`, `SSH_REMOTE_PATH`, `SSH_IDENTIFY_FILE`, `SSH_PORT` or `SSH_PASSWORD` if you dont use a private key to access to your server.
|
||||
These environment variables are required for SSH backup `SSH_HOST`, `SSH_USER`, `SSH_REMOTE_PATH`, `SSH_IDENTIFY_FILE`, `SSH_PORT` or `SSH_PASSWORD` if you dont use a private key to access to your server.
|
||||
Accessing the remote server using password is not recommended, use private key instead.
|
||||
|
||||
```yml
|
||||
@@ -33,10 +33,10 @@ services:
|
||||
- DB_USERNAME=username
|
||||
- DB_PASSWORD=password
|
||||
## SSH config
|
||||
- SSH_HOST_NAME="hostname"
|
||||
- SSH_HOST="hostname"
|
||||
- SSH_PORT=22
|
||||
- SSH_USER=user
|
||||
- SSH_REMOTE_PATH=/home/jkaninda/backups
|
||||
- REMOTE_PATH=/home/jkaninda/backups
|
||||
- SSH_IDENTIFY_FILE=/tmp/id_ed25519
|
||||
## We advise you to use a private jey instead of password
|
||||
#- SSH_PASSWORD=password
|
||||
@@ -52,7 +52,7 @@ networks:
|
||||
### Recurring backups to SSH remote server
|
||||
|
||||
As explained above, you need just to add required environment variables and specify the storage type `--storage ssh`.
|
||||
You can use `--mode scheduled` and specify the periodical backup time by adding `--period "0 1 * * *"` flag as described below.
|
||||
You can use `--cron-expression "* * * * *"` or `BACKUP_CRON_EXPRESSION=0 1 * * *` as described below.
|
||||
|
||||
```yml
|
||||
services:
|
||||
@@ -63,7 +63,7 @@ services:
|
||||
# for a list of available releases.
|
||||
image: jkaninda/mysql-bkup
|
||||
container_name: mysql-bkup
|
||||
command: backup -d database --storage ssh --mode scheduled --period "0 1 * * *"
|
||||
command: backup -d database --storage ssh --cron-expression "0 1 * * *"
|
||||
volumes:
|
||||
- ./id_ed25519:/tmp/id_ed25519"
|
||||
environment:
|
||||
@@ -73,11 +73,14 @@ services:
|
||||
- DB_USERNAME=username
|
||||
- DB_PASSWORD=password
|
||||
## SSH config
|
||||
- SSH_HOST_NAME="hostname"
|
||||
- SSH_HOST="hostname"
|
||||
- SSH_PORT=22
|
||||
- SSH_USER=user
|
||||
- SSH_REMOTE_PATH=/home/jkaninda/backups
|
||||
- REMOTE_PATH=/home/jkaninda/backups
|
||||
- SSH_IDENTIFY_FILE=/tmp/id_ed25519
|
||||
# - BACKUP_CRON_EXPRESSION=0 1 * * * # Optional
|
||||
#Delete old backup created more than specified days ago
|
||||
#- BACKUP_RETENTION_DAYS=7
|
||||
## We advise you to use a private jey instead of password
|
||||
#- SSH_PASSWORD=password
|
||||
# mysql-bkup container must be connected to the same network with your database
|
||||
@@ -86,55 +89,3 @@ services:
|
||||
networks:
|
||||
web:
|
||||
```
|
||||
|
||||
## Deploy on Kubernetes
|
||||
|
||||
For Kubernetes, you don't need to run it in scheduled mode.
|
||||
You can deploy it as CronJob.
|
||||
|
||||
Simple Kubernetes CronJob usage:
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: bkup-job
|
||||
spec:
|
||||
schedule: "0 1 * * *"
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: mysql-bkup
|
||||
image: jkaninda/mysql-bkup
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- backup -s ssh
|
||||
env:
|
||||
- name: DB_PORT
|
||||
value: "3306"
|
||||
- name: DB_HOST
|
||||
value: ""
|
||||
- name: DB_NAME
|
||||
value: ""
|
||||
- name: DB_USERNAME
|
||||
value: ""
|
||||
# Please use secret!
|
||||
- name: DB_PASSWORD
|
||||
value: ""
|
||||
- name: SSH_HOST_NAME
|
||||
value: ""
|
||||
- name: SSH_PORT
|
||||
value: "22"
|
||||
- name: SSH_USER
|
||||
value: "xxx"
|
||||
- name: SSH_REMOTE_PATH
|
||||
value: "/home/jkaninda/backups"
|
||||
- name: AWS_ACCESS_KEY
|
||||
value: "xxxx"
|
||||
- name: SSH_IDENTIFY_FILE
|
||||
value: "/tmp/id_ed25519"
|
||||
restartPolicy: Never
|
||||
```
|
||||
@@ -54,7 +54,7 @@ networks:
|
||||
jkaninda/mysql-bkup backup -d database_name
|
||||
```
|
||||
|
||||
In case you need to use recurring backups, you can use `--mode scheduled` and specify the periodical backup time by adding `--period "0 1 * * *"` flag as described below.
|
||||
In case you need to use recurring backups, you can use `--cron-expression "0 1 * * *"` flag or `BACKUP_CRON_EXPRESSION=0 1 * * *` as described below.
|
||||
|
||||
```yml
|
||||
services:
|
||||
@@ -65,7 +65,7 @@ services:
|
||||
# for a list of available releases.
|
||||
image: jkaninda/mysql-bkup
|
||||
container_name: mysql-bkup
|
||||
command: backup -d database --mode scheduled --period "0 1 * * *"
|
||||
command: backup -d database --cron-expression "0 1 * * *"
|
||||
volumes:
|
||||
- ./backup:/backup
|
||||
environment:
|
||||
@@ -74,6 +74,9 @@ services:
|
||||
- DB_NAME=database
|
||||
- DB_USERNAME=username
|
||||
- DB_PASSWORD=password
|
||||
- BACKUP_CRON_EXPRESSION=0 1 * * *
|
||||
#Delete old backup created more than specified days ago
|
||||
#- BACKUP_RETENTION_DAYS=7
|
||||
# mysql-bkup container must be connected to the same network with your database
|
||||
networks:
|
||||
- web
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Deploy on Kubernetes
|
||||
layout: default
|
||||
parent: How Tos
|
||||
nav_order: 8
|
||||
nav_order: 9
|
||||
---
|
||||
|
||||
## Deploy on Kubernetes
|
||||
@@ -59,6 +59,8 @@ spec:
|
||||
value: "xxxx"
|
||||
- name: AWS_DISABLE_SSL
|
||||
value: "false"
|
||||
- name: AWS_FORCE_PATH_STYLE
|
||||
value: "false"
|
||||
restartPolicy: Never
|
||||
```
|
||||
|
||||
@@ -83,11 +85,7 @@ spec:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- bkup
|
||||
- backup
|
||||
- --storage
|
||||
- ssh
|
||||
- --disable-compression
|
||||
- backup --storage ssh
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
@@ -116,7 +114,7 @@ spec:
|
||||
value: "/home/toto/backup"
|
||||
# Optional, required if you want to encrypt your backup
|
||||
- name: GPG_PASSPHRASE
|
||||
value: "xxxx"
|
||||
value: "secure-passphrase"
|
||||
restartPolicy: Never
|
||||
```
|
||||
|
||||
@@ -141,11 +139,7 @@ spec:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- bkup
|
||||
- restore
|
||||
- --storage
|
||||
- ssh
|
||||
- --file store_20231219_022941.sql.gz
|
||||
- backup --storage ssh --file store_20231219_022941.sql.gz
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
@@ -238,7 +232,6 @@ spec:
|
||||
|
||||
This image also supports Kubernetes security context, you can run it in Rootless environment.
|
||||
It has been tested on Openshift, it works well.
|
||||
Deployment on OpenShift is supported, you need to remove `securityContext` section on your yaml file.
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
@@ -301,3 +294,55 @@ spec:
|
||||
# value: "xxx"
|
||||
restartPolicy: OnFailure
|
||||
```
|
||||
|
||||
## Migrate database
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: migrate-db
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 100
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: mysql-bkup
|
||||
# In production, it is advised to lock your image tag to a proper
|
||||
# release version instead of using `latest`.
|
||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
||||
# for a list of available releases.
|
||||
image: jkaninda/mysql-bkup
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- migrate
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
env:
|
||||
## Source Database
|
||||
- name: DB_HOST
|
||||
value: "mysql"
|
||||
- name: DB_PORT
|
||||
value: "3306"
|
||||
- name: DB_NAME
|
||||
value: "dbname"
|
||||
- name: DB_USERNAME
|
||||
value: "username"
|
||||
- name: DB_PASSWORD
|
||||
value: "password"
|
||||
## Target Database
|
||||
- name: TARGET_DB_HOST
|
||||
value: "target-mysql"
|
||||
- name: TARGET_DB_PORT
|
||||
value: "3306"
|
||||
- name: TARGET_DB_NAME
|
||||
value: "dbname"
|
||||
- name: TARGET_DB_USERNAME
|
||||
value: "username"
|
||||
- name: TARGET_DB_PASSWORD
|
||||
value: "password"
|
||||
restartPolicy: Never
|
||||
```
|
||||
6
docs/how-tos/deprecated-configs.md
Normal file
6
docs/how-tos/deprecated-configs.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Update deprecated configurations
|
||||
layout: default
|
||||
parent: How Tos
|
||||
nav_order: 11
|
||||
---
|
||||
@@ -1,27 +1,39 @@
|
||||
---
|
||||
title: Encrypt backups using GPG
|
||||
title: Encrypt backups
|
||||
layout: default
|
||||
parent: How Tos
|
||||
nav_order: 7
|
||||
nav_order: 8
|
||||
---
|
||||
# Encrypt backup
|
||||
|
||||
The image supports encrypting backups using GPG out of the box. In case a `GPG_PASSPHRASE` environment variable is set, the backup archive will be encrypted using the given key and saved as a sql.gpg file instead or sql.gz.gpg.
|
||||
The image supports encrypting backups using one of two available methods: GPG with passphrase or GPG with a public key.
|
||||
|
||||
|
||||
The image supports encrypting backups using GPG out of the box. In case a `GPG_PASSPHRASE` or `GPG_PUBLIC_KEY` environment variable is set, the backup archive will be encrypted using the given key and saved as a sql.gpg file instead or sql.gz.gpg.
|
||||
|
||||
{: .warning }
|
||||
To restore an encrypted backup, you need to provide the same GPG passphrase used during backup process.
|
||||
|
||||
To decrypt manually, you need to install `gnupg`
|
||||
- GPG home directory `/config/gnupg`
|
||||
- Cipher algorithm `aes256`
|
||||
|
||||
### Decrypt backup
|
||||
{: .note }
|
||||
The backup encrypted using `GPG passphrase` method can be restored automatically, no need to decrypt it before restoration.
|
||||
Suppose you used a GPG public key during the backup process. In that case, you need to decrypt your backup before restoration because decryption using a `GPG private` key is not fully supported.
|
||||
|
||||
To decrypt manually, you need to install `gnupg`
|
||||
|
||||
```shell
|
||||
gpg --batch --passphrase "my-passphrase" \
|
||||
--output database_20240730_044201.sql.gz \
|
||||
--decrypt database_20240730_044201.sql.gz.gpg
|
||||
```
|
||||
Using your private key
|
||||
|
||||
### Backup
|
||||
```shell
|
||||
gpg --output database_20240730_044201.sql.gz --decrypt database_20240730_044201.sql.gz.gpg
|
||||
```
|
||||
## Using GPG passphrase
|
||||
|
||||
```yml
|
||||
services:
|
||||
@@ -49,3 +61,31 @@ services:
|
||||
networks:
|
||||
web:
|
||||
```
|
||||
## Using GPG Public Key
|
||||
|
||||
```yml
|
||||
services:
|
||||
mysql-bkup:
|
||||
# In production, it is advised to lock your image tag to a proper
|
||||
# release version instead of using `latest`.
|
||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
||||
# for a list of available releases.
|
||||
image: jkaninda/mysql-bkup
|
||||
container_name: mysql-bkup
|
||||
command: backup -d database
|
||||
volumes:
|
||||
- ./backup:/backup
|
||||
environment:
|
||||
- DB_PORT=3306
|
||||
- DB_HOST=mysql
|
||||
- DB_NAME=database
|
||||
- DB_USERNAME=username
|
||||
- DB_PASSWORD=password
|
||||
## Required to encrypt backup
|
||||
- GPG_PUBLIC_KEY=/config/public_key.asc
|
||||
# mysql-bkup container must be connected to the same network with your database
|
||||
networks:
|
||||
- web
|
||||
networks:
|
||||
web:
|
||||
```
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Migrate database
|
||||
layout: default
|
||||
parent: How Tos
|
||||
nav_order: 9
|
||||
nav_order: 10
|
||||
---
|
||||
|
||||
# Migrate database
|
||||
@@ -78,54 +78,3 @@ TARGET_DB_PASSWORD=password
|
||||
jkaninda/mysql-bkup migrate
|
||||
```
|
||||
|
||||
## Kubernetes
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: migrate-db
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 100
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: mysql-bkup
|
||||
# In production, it is advised to lock your image tag to a proper
|
||||
# release version instead of using `latest`.
|
||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
||||
# for a list of available releases.
|
||||
image: jkaninda/mysql-bkup
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- migrate
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
env:
|
||||
## Source Database
|
||||
- name: DB_HOST
|
||||
value: "mysql"
|
||||
- name: DB_PORT
|
||||
value: "3306"
|
||||
- name: DB_NAME
|
||||
value: "dbname"
|
||||
- name: DB_USERNAME
|
||||
value: "username"
|
||||
- name: DB_PASSWORD
|
||||
value: "password"
|
||||
## Target Database
|
||||
- name: TARGET_DB_HOST
|
||||
value: "target-mysql"
|
||||
- name: TARGET_DB_PORT
|
||||
value: "3306"
|
||||
- name: TARGET_DB_NAME
|
||||
value: "dbname"
|
||||
- name: TARGET_DB_USERNAME
|
||||
value: "username"
|
||||
- name: TARGET_DB_PASSWORD
|
||||
value: "password"
|
||||
restartPolicy: Never
|
||||
```
|
||||
63
docs/how-tos/mutli-backup.md
Normal file
63
docs/how-tos/mutli-backup.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Run multiple backup schedules in the same container
|
||||
layout: default
|
||||
parent: How Tos
|
||||
nav_order: 11
|
||||
---
|
||||
|
||||
Multiple backup schedules with different configuration can be configured by mounting a configuration file into `/config/config.yaml` `/config/config.yml` or by defining an environment variable `BACKUP_CONFIG_FILE=/backup/config.yaml`.
|
||||
|
||||
## Configuration file
|
||||
|
||||
```yaml
|
||||
#cronExpression: "@every 20m" //Optional for scheduled backups
|
||||
cronExpression: ""
|
||||
databases:
|
||||
- host: mysql1
|
||||
port: 3306
|
||||
name: database1
|
||||
user: database1
|
||||
password: password
|
||||
path: /s3-path/database1 #For SSH or FTP you need to define the full path (/home/toto/backup/)
|
||||
- host: mysql2
|
||||
port: 3306
|
||||
name: lldap
|
||||
user: lldap
|
||||
password: password
|
||||
path: /s3-path/lldap #For SSH or FTP you need to define the full path (/home/toto/backup/)
|
||||
- host: mysql3
|
||||
port: 3306
|
||||
name: keycloak
|
||||
user: keycloak
|
||||
password: password
|
||||
path: /s3-path/keycloak #For SSH or FTP you need to define the full path (/home/toto/backup/)
|
||||
- host: mysql4
|
||||
port: 3306
|
||||
name: joplin
|
||||
user: joplin
|
||||
password: password
|
||||
path: /s3-path/joplin #For SSH or FTP you need to define the full path (/home/toto/backup/)
|
||||
```
|
||||
## Docker compose file
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mysql-bkup:
|
||||
# In production, it is advised to lock your image tag to a proper
|
||||
# release version instead of using `latest`.
|
||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
||||
# for a list of available releases.
|
||||
image: jkaninda/mysql-bkup
|
||||
container_name: mysql-bkup
|
||||
command: backup
|
||||
volumes:
|
||||
- ./backup:/backup
|
||||
environment:
|
||||
## Multi backup config file
|
||||
- BACKUP_CONFIG_FILE=/backup/config.yaml
|
||||
# mysql-bkup container must be connected to the same network with your database
|
||||
networks:
|
||||
- web
|
||||
networks:
|
||||
web:
|
||||
```
|
||||
162
docs/how-tos/receive-notification.md
Normal file
162
docs/how-tos/receive-notification.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
title: Receive notifications
|
||||
layout: default
|
||||
parent: How Tos
|
||||
nav_order: 12
|
||||
---
|
||||
Send Email or Telegram notifications on successfully or failed backup.
|
||||
|
||||
### Email
|
||||
To send out email notifications on failed or successfully 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=Backup Jobs <backup@example.com>
|
||||
## Multiple recipients separated by a comma
|
||||
- MAIL_TO=me@example.com,team@example.com,manager@example.com
|
||||
- MAIL_SKIP_TLS=false
|
||||
## Time format for notification
|
||||
- TIME_FORMAT=2006-01-02 at 15:04:05
|
||||
## Backup reference, in case you want to identify every backup instance
|
||||
- BACKUP_REFERENCE=database/Paris cluster
|
||||
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=
|
||||
## Time format for notification
|
||||
- TIME_FORMAT=2006-01-02 at 15:04:05
|
||||
## Backup reference, in case you want to identify every backup instance
|
||||
- BACKUP_REFERENCE=database/Paris cluster
|
||||
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.tmpl: Email notification template
|
||||
- telegram.tmpl: Telegram notification template
|
||||
- email-error.tmpl: Error notification template
|
||||
- telegram-error.tmpl: 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
|
||||
- `BackupReference`: Backup reference(eg: database/cluster name or server name)
|
||||
|
||||
> email.template:
|
||||
|
||||
|
||||
```html
|
||||
<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>
|
||||
<li>Backup Reference: {{.BackupReference}} </li>
|
||||
</ul>
|
||||
<p>Best regards,</p>
|
||||
```
|
||||
|
||||
> 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
|
||||
- Backup Reference: {{.BackupReference}}
|
||||
```
|
||||
|
||||
> email-error.template
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<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>
|
||||
<li>Backup Reference: {{.BackupReference}} </li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
> telegram-error.template
|
||||
|
||||
|
||||
```html
|
||||
🔴 Urgent: Database Backup Failure Notification
|
||||
|
||||
An error occurred during database backup.
|
||||
Failure Details:
|
||||
|
||||
Error Message: {{.Error}}
|
||||
Date: {{.EndTime}}
|
||||
```
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Restore database from AWS S3
|
||||
layout: default
|
||||
parent: How Tos
|
||||
nav_order: 5
|
||||
nav_order: 6
|
||||
---
|
||||
|
||||
# Restore database from S3 storage
|
||||
@@ -10,7 +10,7 @@ nav_order: 5
|
||||
To restore the database, you need to add `restore` command and specify the file to restore by adding `--file store_20231219_022941.sql.gz`.
|
||||
|
||||
{: .note }
|
||||
It supports __.sql__ and __.sql.gz__ compressed file.
|
||||
It supports __.sql__,__.sql.gpg__ and __.sql.gz__,__.sql.gz.gpg__ compressed file.
|
||||
|
||||
### Restore
|
||||
|
||||
@@ -40,56 +40,10 @@ services:
|
||||
- AWS_SECRET_KEY=xxxxx
|
||||
## In case you are using S3 alternative such as Minio and your Minio instance is not secured, you change it to true
|
||||
- AWS_DISABLE_SSL="false"
|
||||
- AWS_FORCE_PATH_STYLE="false"
|
||||
# mysql-bkup container must be connected to the same network with your database
|
||||
networks:
|
||||
- web
|
||||
networks:
|
||||
web:
|
||||
```
|
||||
|
||||
## Restore on Kubernetes
|
||||
|
||||
Simple Kubernetes restore Job:
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: restore-db
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: mysql-bkup
|
||||
image: jkaninda/mysql-bkup
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- restore -s s3 --path /custom_path -f store_20231219_022941.sql.gz
|
||||
env:
|
||||
- name: DB_PORT
|
||||
value: "3306"
|
||||
- name: DB_HOST
|
||||
value: ""
|
||||
- name: DB_NAME
|
||||
value: ""
|
||||
- name: DB_USERNAME
|
||||
value: ""
|
||||
# Please use secret!
|
||||
- name: DB_PASSWORD
|
||||
value: ""
|
||||
- name: AWS_S3_ENDPOINT
|
||||
value: "https://s3.amazonaws.com"
|
||||
- name: AWS_S3_BUCKET_NAME
|
||||
value: "xxx"
|
||||
- name: AWS_REGION
|
||||
value: "us-west-2"
|
||||
- name: AWS_ACCESS_KEY
|
||||
value: "xxxx"
|
||||
- name: AWS_SECRET_KEY
|
||||
value: "xxxx"
|
||||
- name: AWS_DISABLE_SSL
|
||||
value: "false"
|
||||
restartPolicy: Never
|
||||
backoffLimit: 4
|
||||
```
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
title: Restore database from SSH
|
||||
layout: default
|
||||
parent: How Tos
|
||||
nav_order: 6
|
||||
nav_order: 7
|
||||
---
|
||||
# Restore database from SSH remote server
|
||||
|
||||
To restore the database from your remote server, you need to add `restore` command and specify the file to restore by adding `--file store_20231219_022941.sql.gz`.
|
||||
|
||||
{: .note }
|
||||
It supports __.sql__ and __.sql.gz__ compressed file.
|
||||
It supports __.sql__,__.sql.gpg__ and __.sql.gz__,__.sql.gz.gpg__ compressed file.
|
||||
|
||||
### Restore
|
||||
|
||||
@@ -45,49 +45,3 @@ services:
|
||||
networks:
|
||||
web:
|
||||
```
|
||||
## Restore on Kubernetes
|
||||
|
||||
Simple Kubernetes restore Job:
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: restore-db
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: mysql-bkup
|
||||
image: jkaninda/mysql-bkup
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- restore -s ssh -f store_20231219_022941.sql.gz
|
||||
env:
|
||||
- name: DB_PORT
|
||||
value: "3306"
|
||||
- name: DB_HOST
|
||||
value: ""
|
||||
- name: DB_NAME
|
||||
value: ""
|
||||
- name: DB_USERNAME
|
||||
value: ""
|
||||
# Please use secret!
|
||||
- name: DB_PASSWORD
|
||||
value: ""
|
||||
- name: SSH_HOST_NAME
|
||||
value: ""
|
||||
- name: SSH_PORT
|
||||
value: "22"
|
||||
- name: SSH_USER
|
||||
value: "xxx"
|
||||
- name: SSH_REMOTE_PATH
|
||||
value: "/home/jkaninda/backups"
|
||||
- name: AWS_ACCESS_KEY
|
||||
value: "xxxx"
|
||||
- name: SSH_IDENTIFY_FILE
|
||||
value: "/tmp/id_ed25519"
|
||||
restartPolicy: Never
|
||||
backoffLimit: 4
|
||||
```
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Restore database
|
||||
layout: default
|
||||
parent: How Tos
|
||||
nav_order: 4
|
||||
nav_order: 5
|
||||
---
|
||||
|
||||
# Restore database
|
||||
@@ -10,7 +10,7 @@ nav_order: 4
|
||||
To restore the database, you need to add `restore` command and specify the file to restore by adding `--file store_20231219_022941.sql.gz`.
|
||||
|
||||
{: .note }
|
||||
It supports __.sql__ and __.sql.gz__ compressed file.
|
||||
It supports __.sql__,__.sql.gpg__ and __.sql.gz__,__.sql.gz.gpg__ compressed file.
|
||||
|
||||
### Restore
|
||||
|
||||
|
||||
@@ -6,20 +6,40 @@ nav_order: 1
|
||||
|
||||
# About mysql-bkup
|
||||
{:.no_toc}
|
||||
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 remote storage.
|
||||
It also supports __encrypting__ your backups using GPG.
|
||||
|
||||
We are open to receiving stars, PRs, and issues!
|
||||
**MYSQL-BKUP** is a Docker container image designed to **backup, restore, and migrate MySQL databases**.
|
||||
It supports a variety of storage options and ensures data security through GPG encryption.
|
||||
|
||||
## Features
|
||||
|
||||
{: .fs-6 .fw-300 }
|
||||
- **Storage Options:**
|
||||
- Local storage
|
||||
- AWS S3 or any S3-compatible object storage
|
||||
- FTP
|
||||
- SSH-compatible storage
|
||||
- Azure Blob storage
|
||||
|
||||
---
|
||||
- **Data Security:**
|
||||
- Backups can be encrypted using **GPG** to ensure confidentiality.
|
||||
|
||||
The [jkaninda/mysql-bkup](https://hub.docker.com/r/jkaninda/mysql-bkup) Docker image can be deployed on Docker, Docker Swarm and Kubernetes.
|
||||
It handles __recurring__ backups of postgres database on Docker and can be deployed as __CronJob on Kubernetes__ using local, AWS S3 or SSH compatible storage.
|
||||
- **Deployment Flexibility:**
|
||||
- Available as the [jkaninda/mysql-bkup](https://hub.docker.com/r/jkaninda/mysql-bkup) Docker image.
|
||||
- Deployable on **Docker**, **Docker Swarm**, and **Kubernetes**.
|
||||
- Supports recurring backups of MySQL databases when deployed:
|
||||
- On Docker for automated backup schedules.
|
||||
- As a **Job** or **CronJob** on Kubernetes.
|
||||
|
||||
- **Notifications:**
|
||||
- Get real-time updates on backup success or failure via:
|
||||
- **Telegram**
|
||||
- **Email**
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Automated Recurring Backups:** Schedule regular backups for MySQL databases.
|
||||
- **Cross-Environment Migration:** Easily migrate your MySQL databases across different environments using supported storage options.
|
||||
- **Secure Backup Management:** Protect your data with GPG encryption.
|
||||
|
||||
It also supports database __encryption__ using GPG.
|
||||
|
||||
|
||||
{: .note }
|
||||
@@ -73,12 +93,25 @@ services:
|
||||
- DB_NAME=foo
|
||||
- DB_USERNAME=bar
|
||||
- DB_PASSWORD=password
|
||||
- TZ=Europe/Paris
|
||||
# mysql-bkup container must be connected to the same network with your database
|
||||
networks:
|
||||
- web
|
||||
networks:
|
||||
web:
|
||||
```
|
||||
### Docker recurring backup
|
||||
|
||||
```shell
|
||||
docker run --rm --network network_name \
|
||||
-v $PWD/backup:/backup/ \
|
||||
-e "DB_HOST=hostname" \
|
||||
-e "DB_USERNAME=user" \
|
||||
-e "DB_PASSWORD=password" \
|
||||
jkaninda/mysql-bkup backup -d dbName --cron-expression "@every 15m" #@midnight
|
||||
```
|
||||
See: https://jkaninda.github.io/mysql-bkup/reference/#predefined-schedules
|
||||
|
||||
## Kubernetes
|
||||
|
||||
```yaml
|
||||
@@ -144,7 +177,7 @@ While it may work against different implementations, there are no guarantees abo
|
||||
|
||||
We decided to publish this image as a simpler and more lightweight alternative because of the following requirements:
|
||||
|
||||
- The original image is based on `ubuntu` and requires additional tools, making it heavy.
|
||||
- The original image is based on `alpine` and requires additional tools, making it heavy.
|
||||
- This image is written in Go.
|
||||
- `arm64` and `arm/v7` architectures are supported.
|
||||
- Docker in Swarm mode is supported.
|
||||
|
||||
@@ -25,18 +25,15 @@ Backup, restore and migrate targets, schedule and retention are configured using
|
||||
| --path | | AWS S3 path without file name. eg: /custom_path or ssh remote path `/home/foo/backup` |
|
||||
| --dbname | -d | Database name |
|
||||
| --port | -p | Database port (default: 3306) |
|
||||
| --mode | -m | Execution mode. default or scheduled (default: default) |
|
||||
| --disable-compression | | Disable database backup compression |
|
||||
| --prune | | Delete old backup, default disabled |
|
||||
| --keep-last | | Delete old backup created more than specified days ago, default 7 days |
|
||||
| --period | | Crontab period for scheduled mode only. (default: "0 1 * * *") |
|
||||
| --cron-expression | | Backup cron expression, eg: (* * * * *) or @daily |
|
||||
| --help | -h | Print this help message and exit |
|
||||
| --version | -V | Print version information and exit |
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Name | Requirement | Description |
|
||||
|------------------------|----------------------------------------------------|------------------------------------------------------|
|
||||
|------------------------|---------------------------------------------------------------|-----------------------------------------------------------------|
|
||||
| DB_PORT | Optional, default 3306 | Database port number |
|
||||
| DB_HOST | Required | Database host |
|
||||
| DB_NAME | Optional if it was provided from the -d flag | Database name |
|
||||
@@ -48,27 +45,36 @@ Backup, restore and migrate targets, schedule and retention are configured using
|
||||
| AWS_BUCKET_NAME | Optional, required for S3 storage | AWS S3 Bucket Name |
|
||||
| AWS_REGION | Optional, required for S3 storage | AWS Region |
|
||||
| AWS_DISABLE_SSL | Optional, required for S3 storage | Disable SSL |
|
||||
| AWS_FORCE_PATH_STYLE | Optional, required for S3 storage | Force path style |
|
||||
| FILE_NAME | Optional if it was provided from the --file flag | Database file to restore (extensions: .sql, .sql.gz) |
|
||||
| BACKUP_CRON_EXPRESSION | Optional if it was provided from the --period flag | Backup cron expression for docker in scheduled mode |
|
||||
| GPG_PASSPHRASE | Optional, required to encrypt and restore backup | GPG passphrase |
|
||||
| SSH_HOST_NAME | Optional, required for SSH storage | ssh remote hostname or ip |
|
||||
| GPG_PUBLIC_KEY | Optional, required to encrypt backup | GPG public key, used to encrypt backup (/config/public_key.asc) |
|
||||
| BACKUP_CRON_EXPRESSION | Optional if it was provided from the `--cron-expression` flag | Backup cron expression for docker in scheduled mode |
|
||||
| BACKUP_RETENTION_DAYS | Optional | Delete old backup created more than specified days ago |
|
||||
| SSH_HOST | Optional, required for SSH storage | ssh remote hostname or ip |
|
||||
| SSH_USER | Optional, required for SSH storage | ssh remote user |
|
||||
| SSH_PASSWORD | Optional, required for SSH storage | ssh remote user's password |
|
||||
| SSH_IDENTIFY_FILE | Optional, required for SSH storage | ssh remote user's private key |
|
||||
| SSH_PORT | Optional, required for SSH storage | ssh remote server port |
|
||||
| SSH_REMOTE_PATH | Optional, required for SSH storage | ssh remote path (/home/toto/backup) |
|
||||
| REMOTE_PATH | Optional, required for SSH or FTP storage | remote path (/home/toto/backup) |
|
||||
| FTP_HOST | Optional, required for FTP storage | FTP host name |
|
||||
| FTP_PORT | Optional, required for FTP storage | FTP server port number |
|
||||
| FTP_USER | Optional, required for FTP storage | FTP user |
|
||||
| FTP_PASSWORD | Optional, required for FTP storage | FTP user password |
|
||||
| TARGET_DB_HOST | Optional, required for database migration | Target database host |
|
||||
| TARGET_DB_PORT | Optional, required for database migration | Target database port |
|
||||
| TARGET_DB_NAME | Optional, required for database migration | Target database name |
|
||||
| TARGET_DB_USERNAME | Optional, required for database migration | Target database username |
|
||||
| TARGET_DB_PASSWORD | Optional, required for database migration | Target database password |
|
||||
| TG_TOKEN | Optional, required for Telegram notification | Telegram token |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
## Run in Scheduled mode
|
||||
|
||||
This image can be run as CronJob in Kubernetes for a regular backup which makes deployment on Kubernetes easy as Kubernetes has CronJob resources.
|
||||
For Docker, you need to run it in scheduled mode by adding `--mode scheduled` flag and specify the periodical backup time by adding `--period "0 1 * * *"` flag.
|
||||
For Docker, you need to run it in scheduled mode by adding `--cron-expression "* * * * *"` flag or by defining `BACKUP_CRON_EXPRESSION=0 1 * * *` environment variable.
|
||||
|
||||
## Syntax of crontab (field description)
|
||||
|
||||
@@ -111,3 +117,21 @@ Easy to remember format:
|
||||
```conf
|
||||
0 1 * * *
|
||||
```
|
||||
## Predefined schedules
|
||||
You may use one of several pre-defined schedules in place of a cron expression.
|
||||
|
||||
| Entry | Description | Equivalent To |
|
||||
|------------------------|--------------------------------------------|---------------|
|
||||
| @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 1 1 * |
|
||||
| @monthly | Run once a month, midnight, first of month | 0 0 1 * * |
|
||||
| @weekly | Run once a week, midnight between Sat/Sun | 0 0 * * 0 |
|
||||
| @daily (or @midnight) | Run once a day, midnight | 0 0 * * * |
|
||||
| @hourly | Run once an hour, beginning of hour | 0 * * * * |
|
||||
|
||||
### Intervals
|
||||
You may also schedule backup task at fixed intervals, starting at the time it's added or cron is run. This is supported by formatting the cron spec like this:
|
||||
|
||||
@every <duration>
|
||||
where "duration" is a string accepted by time.
|
||||
|
||||
For example, "@every 1h30m10s" would indicate a schedule that activates after 1 hour, 30 minutes, 10 seconds, and then every interval after that.
|
||||
@@ -21,6 +21,7 @@ services:
|
||||
- AWS_SECRET_KEY=xxxxx
|
||||
## In case you are using S3 alternative such as Minio and your Minio instance is not secured, you change it to true
|
||||
- AWS_DISABLE_SSL="false"
|
||||
- AWS_FORCE_PATH_STYLE=true # true for S3 alternative such as Minio
|
||||
# mysql-bkup container must be connected to the same network with your database
|
||||
networks:
|
||||
- web
|
||||
|
||||
@@ -5,7 +5,7 @@ services:
|
||||
# release version instead of using `latest`.
|
||||
image: jkaninda/mysql-bkup
|
||||
container_name: mysql-bkup
|
||||
command: backup --dbname database_name --mode scheduled --period "0 1 * * *"
|
||||
command: backup --dbname database_name
|
||||
volumes:
|
||||
- ./backup:/backup
|
||||
environment:
|
||||
@@ -13,3 +13,5 @@ services:
|
||||
- DB_HOST=mysql
|
||||
- DB_USERNAME=userName
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
# See: https://jkaninda.github.io/mysql-bkup/reference/#predefined-schedules
|
||||
- BACKUP_CRON_EXPRESSION=@daily #@every 5m|@weekly | @monthly |0 1 * * *
|
||||
@@ -6,7 +6,7 @@ services:
|
||||
# for a list of available releases.
|
||||
image: jkaninda/mysql-bkup
|
||||
container_name: mysql-bkup
|
||||
command: backup --storage s3 -d my-database --mode scheduled --period "0 1 * * *"
|
||||
command: backup --storage s3 -d my-database
|
||||
environment:
|
||||
- DB_PORT=3306
|
||||
- DB_HOST=mysql
|
||||
@@ -21,6 +21,9 @@ services:
|
||||
- AWS_SECRET_KEY=xxxxx
|
||||
## In case you are using S3 alternative such as Minio and your Minio instance is not secured, you change it to true
|
||||
- AWS_DISABLE_SSL="false"
|
||||
- AWS_FORCE_PATH_STYLE=true # true for S3 alternative such as Minio
|
||||
# See: https://jkaninda.github.io/mysql-bkup/reference/#predefined-schedules
|
||||
- BACKUP_CRON_EXPRESSION=@daily #@every 5m|@weekly | @monthly |0 1 * * *
|
||||
# mysql-bkup container must be connected to the same network with your database
|
||||
networks:
|
||||
- web
|
||||
|
||||
@@ -44,4 +44,6 @@ spec:
|
||||
value: "xxxx"
|
||||
- name: AWS_DISABLE_SSL
|
||||
value: "false"
|
||||
- name: AWS_FORCE_PATH_STYLE
|
||||
value: "true"
|
||||
restartPolicy: Never
|
||||
37
go.mod
37
go.mod
@@ -1,21 +1,38 @@
|
||||
module github.com/jkaninda/mysql-bkup
|
||||
|
||||
go 1.22.5
|
||||
go 1.23.2
|
||||
|
||||
require github.com/spf13/pflag v1.0.5
|
||||
require github.com/spf13/pflag v1.0.5 // indirect
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.55.3
|
||||
github.com/bramvdbogaerde/go-scp v1.5.0
|
||||
github.com/hpcloud/tail v1.0.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
golang.org/x/crypto v0.18.0
|
||||
github.com/go-mail/mail v2.3.1+incompatible
|
||||
github.com/jkaninda/encryptor v0.0.0-20241013064832-ed4bd6a1b221
|
||||
github.com/jkaninda/go-storage v0.1.2
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/spf13/cobra v1.8.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0 // 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/gopenpgp/v2 v2.7.5 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.5 // indirect
|
||||
github.com/bramvdbogaerde/go-scp v1.5.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.3 // 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
|
||||
github.com/jlaffaye/ftp v0.2.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/net v0.29.0 // 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
|
||||
gopkg.in/mail.v2 v2.3.1 // indirect
|
||||
)
|
||||
|
||||
119
go.sum
119
go.sum
@@ -1,38 +1,123 @@
|
||||
github.com/aws/aws-sdk-go v1.55.3 h1:0B5hOX+mIx7I5XPOrjrHlKSDQV/+ypFZpIHOx5LOk3E=
|
||||
github.com/aws/aws-sdk-go v1.55.3/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0 h1:mlmW46Q0B79I+Aj4azKC6xDMFN9a9SyZWESlGWYXbFs=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0/go.mod h1:PXe2h+LKcWTX9afWdZoHyODqR4fBa5boUM/8uJfZ0Jo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.5 h1:STOY3vgES59gNgoOt2w0nyHBjKViB/qSg7NjbQWPJkA=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.5/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=
|
||||
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
||||
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM=
|
||||
github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
|
||||
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
|
||||
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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
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=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jkaninda/encryptor v0.0.0-20241013064832-ed4bd6a1b221 h1:AwkCf7el1kzeCJ89A+gUAK0ero5JYnvLOKsYMzq+rs4=
|
||||
github.com/jkaninda/encryptor v0.0.0-20241013064832-ed4bd6a1b221/go.mod h1:9F8ZJ+ZXE8DZBo77+aneGj8LMjrYXX6eFUCC/uqZOUo=
|
||||
github.com/jkaninda/go-storage v0.1.1 h1:vjpdD/fh39S5HGyfHvLE5HGYOEPIukINlOX3OnM3GW4=
|
||||
github.com/jkaninda/go-storage v0.1.1/go.mod h1:7VK5gQISQaLxtLfBtc+een8spcgLVSBAKTRuyF1N81I=
|
||||
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/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
|
||||
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/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
30
main.go
30
main.go
@@ -1,9 +1,27 @@
|
||||
// Package main /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/jkaninda/mysql-bkup/cmd"
|
||||
|
||||
121
pkg/azure.go
Normal file
121
pkg/azure.go
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jkaninda/go-storage/pkg/azure"
|
||||
"github.com/jkaninda/mysql-bkup/utils"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func azureBackup(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 Azure Blob storage ...")
|
||||
utils.Info("Backup name is %s", finalFileName)
|
||||
azureConfig := loadAzureConfig()
|
||||
azureStorage, err := azure.NewStorage(azure.Config{
|
||||
ContainerName: azureConfig.containerName,
|
||||
AccountName: azureConfig.accountName,
|
||||
AccountKey: azureConfig.accountKey,
|
||||
RemotePath: config.remotePath,
|
||||
LocalPath: tmpPath,
|
||||
})
|
||||
if err != nil {
|
||||
utils.Fatal("Error creating Azure storage: %s", err)
|
||||
}
|
||||
err = azureStorage.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 := azureStorage.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 Azure Blob storage ... 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 azureRestore(db *dbConfig, conf *RestoreConfig) {
|
||||
utils.Info("Restore database from Azure Blob storage")
|
||||
azureConfig := loadAzureConfig()
|
||||
azureStorage, err := azure.NewStorage(azure.Config{
|
||||
ContainerName: azureConfig.containerName,
|
||||
AccountName: azureConfig.accountName,
|
||||
AccountKey: azureConfig.accountKey,
|
||||
RemotePath: conf.remotePath,
|
||||
LocalPath: tmpPath,
|
||||
})
|
||||
if err != nil {
|
||||
utils.Fatal("Error creating SSH storage: %s", err)
|
||||
}
|
||||
|
||||
err = azureStorage.CopyFrom(conf.file)
|
||||
if err != nil {
|
||||
utils.Fatal("Error downloading backup file: %s", err)
|
||||
}
|
||||
RestoreDatabase(db, conf)
|
||||
}
|
||||
413
pkg/backup.go
413
pkg/backup.go
@@ -1,15 +1,36 @@
|
||||
// Package pkg /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
// Package internal /
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hpcloud/tail"
|
||||
"github.com/jkaninda/encryptor"
|
||||
"github.com/jkaninda/go-storage/pkg/local"
|
||||
"github.com/jkaninda/mysql-bkup/utils"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/spf13/cobra"
|
||||
"log"
|
||||
"os"
|
||||
@@ -20,127 +41,164 @@ import (
|
||||
|
||||
func StartBackup(cmd *cobra.Command) {
|
||||
intro()
|
||||
//Set env
|
||||
utils.SetEnv("STORAGE_PATH", storagePath)
|
||||
utils.GetEnv(cmd, "period", "BACKUP_CRON_EXPRESSION")
|
||||
|
||||
//Get flag value and set env
|
||||
remotePath := utils.GetEnv(cmd, "path", "SSH_REMOTE_PATH")
|
||||
storage = utils.GetEnv(cmd, "storage", "STORAGE")
|
||||
file = utils.GetEnv(cmd, "file", "FILE_NAME")
|
||||
backupRetention, _ := cmd.Flags().GetInt("keep-last")
|
||||
prune, _ := cmd.Flags().GetBool("prune")
|
||||
disableCompression, _ = cmd.Flags().GetBool("disable-compression")
|
||||
executionMode, _ = cmd.Flags().GetString("mode")
|
||||
gpqPassphrase := os.Getenv("GPG_PASSPHRASE")
|
||||
_ = utils.GetEnv(cmd, "path", "AWS_S3_PATH")
|
||||
|
||||
dbConf = getDbConfig(cmd)
|
||||
|
||||
//
|
||||
if gpqPassphrase != "" {
|
||||
encryption = true
|
||||
}
|
||||
|
||||
//Generate file name
|
||||
backupFileName := fmt.Sprintf("%s_%s.sql.gz", dbConf.dbName, time.Now().Format("20060102_150405"))
|
||||
if disableCompression {
|
||||
backupFileName = fmt.Sprintf("%s_%s.sql", dbConf.dbName, time.Now().Format("20060102_150405"))
|
||||
}
|
||||
|
||||
if executionMode == "default" {
|
||||
switch storage {
|
||||
case "s3":
|
||||
s3Backup(dbConf, backupFileName, disableCompression, prune, backupRetention, encryption)
|
||||
case "local":
|
||||
localBackup(dbConf, backupFileName, disableCompression, prune, backupRetention, encryption)
|
||||
case "ssh", "remote":
|
||||
sshBackup(dbConf, backupFileName, remotePath, disableCompression, prune, backupRetention, encryption)
|
||||
case "ftp":
|
||||
utils.Fatal("Not supported storage type: %s", storage)
|
||||
default:
|
||||
localBackup(dbConf, backupFileName, disableCompression, prune, backupRetention, encryption)
|
||||
}
|
||||
|
||||
} else if executionMode == "scheduled" {
|
||||
scheduledMode(dbConf, storage)
|
||||
// Initialize backup configs
|
||||
config := initBackupConfig(cmd)
|
||||
// Load backup configuration file
|
||||
configFile, err := loadConfigFile()
|
||||
if err != nil {
|
||||
dbConf = initDbConfig(cmd)
|
||||
if config.cronExpression == "" {
|
||||
BackupTask(dbConf, config)
|
||||
} else {
|
||||
utils.Fatal("Error, unknown execution mode!")
|
||||
if utils.IsValidCronExpression(config.cronExpression) {
|
||||
scheduledMode(dbConf, config)
|
||||
} else {
|
||||
utils.Fatal("Cron expression is not valid: %s", config.cronExpression)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startMultiBackup(config, configFile)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Run in scheduled mode
|
||||
func scheduledMode(db *dbConfig, storage string) {
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("**********************************")
|
||||
fmt.Println(" Starting MySQL Bkup... ")
|
||||
fmt.Println("***********************************")
|
||||
// scheduledMode Runs backup in scheduled mode
|
||||
func scheduledMode(db *dbConfig, config *BackupConfig) {
|
||||
utils.Info("Running in Scheduled mode")
|
||||
utils.Info("Execution period %s", os.Getenv("BACKUP_CRON_EXPRESSION"))
|
||||
utils.Info("Storage type %s ", storage)
|
||||
utils.Info("Backup cron expression: %s", config.cronExpression)
|
||||
utils.Info("The next scheduled time is: %v", utils.CronNextTime(config.cronExpression).Format(timeFormat))
|
||||
utils.Info("Storage type %s ", config.storage)
|
||||
|
||||
//Test database connexion
|
||||
// Test backup
|
||||
utils.Info("Testing backup configurations...")
|
||||
testDatabaseConnection(db)
|
||||
|
||||
utils.Info("Testing backup configurations...done")
|
||||
utils.Info("Creating backup job...")
|
||||
CreateCrontabScript(disableCompression, storage)
|
||||
// Create a new cron instance
|
||||
c := cron.New()
|
||||
|
||||
supervisorConfig := "/etc/supervisor/supervisord.conf"
|
||||
_, err := c.AddFunc(config.cronExpression, func() {
|
||||
BackupTask(db, config)
|
||||
utils.Info("Next backup time is: %v", utils.CronNextTime(config.cronExpression).Format(timeFormat))
|
||||
|
||||
// Start Supervisor
|
||||
cmd := exec.Command("supervisord", "-c", supervisorConfig)
|
||||
err := cmd.Start()
|
||||
})
|
||||
if err != nil {
|
||||
utils.Fatal(fmt.Sprintf("Failed to start supervisord: %v", err))
|
||||
return
|
||||
}
|
||||
// Start the cron scheduler
|
||||
c.Start()
|
||||
utils.Info("Creating backup job...done")
|
||||
utils.Info("Backup job started")
|
||||
defer func() {
|
||||
if err := cmd.Process.Kill(); err != nil {
|
||||
utils.Info("Failed to kill supervisord process: %v", err)
|
||||
} else {
|
||||
utils.Info("Supervisor stopped.")
|
||||
}
|
||||
}()
|
||||
if _, err := os.Stat(cronLogFile); os.IsNotExist(err) {
|
||||
utils.Fatal(fmt.Sprintf("Log file %s does not exist.", cronLogFile))
|
||||
}
|
||||
t, err := tail.TailFile(cronLogFile, tail.Config{Follow: true})
|
||||
if err != nil {
|
||||
utils.Fatal("Failed to tail file: %v", err)
|
||||
defer c.Stop()
|
||||
select {}
|
||||
}
|
||||
|
||||
// Read and print new lines from the log file
|
||||
for line := range t.Lines {
|
||||
fmt.Println(line.Text)
|
||||
// multiBackupTask backup multi database
|
||||
func multiBackupTask(databases []Database, bkConfig *BackupConfig) {
|
||||
for _, db := range databases {
|
||||
// Check if path is defined in config file
|
||||
if db.Path != "" {
|
||||
bkConfig.remotePath = db.Path
|
||||
}
|
||||
BackupTask(getDatabase(db), bkConfig)
|
||||
}
|
||||
}
|
||||
func intro() {
|
||||
utils.Info("Starting MySQL Backup...")
|
||||
utils.Info("Copyright © 2024 Jonas Kaninda ")
|
||||
|
||||
// BackupTask backups database
|
||||
func BackupTask(db *dbConfig, config *BackupConfig) {
|
||||
utils.Info("Starting backup task...")
|
||||
// Generate file name
|
||||
backupFileName := fmt.Sprintf("%s_%s.sql.gz", db.dbName, time.Now().Format("20060102_150405"))
|
||||
if config.disableCompression {
|
||||
backupFileName = fmt.Sprintf("%s_%s.sql", db.dbName, time.Now().Format("20060102_150405"))
|
||||
}
|
||||
config.backupFileName = backupFileName
|
||||
switch config.storage {
|
||||
case "local":
|
||||
localBackup(db, config)
|
||||
case "s3", "S3":
|
||||
s3Backup(db, config)
|
||||
case "ssh", "SSH", "remote":
|
||||
sshBackup(db, config)
|
||||
case "ftp", "FTP":
|
||||
ftpBackup(db, config)
|
||||
case "azure":
|
||||
azureBackup(db, config)
|
||||
default:
|
||||
localBackup(db, config)
|
||||
}
|
||||
}
|
||||
func startMultiBackup(bkConfig *BackupConfig, configFile string) {
|
||||
utils.Info("Starting backup task...")
|
||||
conf, err := readConf(configFile)
|
||||
if err != nil {
|
||||
utils.Fatal("Error reading config file: %s", err)
|
||||
}
|
||||
// Check if cronExpression is defined in config file
|
||||
if conf.CronExpression != "" {
|
||||
bkConfig.cronExpression = conf.CronExpression
|
||||
}
|
||||
if len(conf.Databases) == 0 {
|
||||
utils.Fatal("No databases found")
|
||||
}
|
||||
// Check if cronExpression is defined
|
||||
if bkConfig.cronExpression == "" {
|
||||
multiBackupTask(conf.Databases, bkConfig)
|
||||
} else {
|
||||
// Check if cronExpression is valid
|
||||
if utils.IsValidCronExpression(bkConfig.cronExpression) {
|
||||
utils.Info("Running backup in Scheduled mode")
|
||||
utils.Info("Backup cron expression: %s", bkConfig.cronExpression)
|
||||
utils.Info("The next scheduled time is: %v", utils.CronNextTime(bkConfig.cronExpression).Format(timeFormat))
|
||||
utils.Info("Storage type %s ", bkConfig.storage)
|
||||
|
||||
// Test backup
|
||||
utils.Info("Testing backup configurations...")
|
||||
for _, db := range conf.Databases {
|
||||
testDatabaseConnection(getDatabase(db))
|
||||
}
|
||||
utils.Info("Testing backup configurations...done")
|
||||
utils.Info("Creating backup job...")
|
||||
// Create a new cron instance
|
||||
c := cron.New()
|
||||
|
||||
_, err := c.AddFunc(bkConfig.cronExpression, func() {
|
||||
multiBackupTask(conf.Databases, bkConfig)
|
||||
utils.Info("Next backup time is: %v", utils.CronNextTime(bkConfig.cronExpression).Format(timeFormat))
|
||||
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Start the cron scheduler
|
||||
c.Start()
|
||||
utils.Info("Creating backup job...done")
|
||||
utils.Info("Backup job started")
|
||||
defer c.Stop()
|
||||
select {}
|
||||
|
||||
} else {
|
||||
utils.Fatal("Cron expression is not valid: %s", bkConfig.cronExpression)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// BackupDatabase backup database
|
||||
func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool) {
|
||||
storagePath = os.Getenv("STORAGE_PATH")
|
||||
|
||||
err := utils.CheckEnvVars(dbHVars)
|
||||
if err != nil {
|
||||
utils.Error("Please make sure all required environment variables for database are set")
|
||||
utils.Fatal("Error checking environment variables: %s", err)
|
||||
}
|
||||
|
||||
utils.Info("Starting database backup...")
|
||||
err = os.Setenv("MYSQL_PWD", db.dbPassword)
|
||||
|
||||
err := os.Setenv("MYSQL_PWD", db.dbPassword)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
testDatabaseConnection(db)
|
||||
|
||||
// Backup Database database
|
||||
utils.Info("Backing up database...")
|
||||
|
||||
// Verify is compression is disabled
|
||||
if disableCompression {
|
||||
// Execute mysqldump
|
||||
cmd := exec.Command("mysqldump",
|
||||
@@ -151,21 +209,26 @@ func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool
|
||||
)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
utils.Fatal(err.Error())
|
||||
}
|
||||
|
||||
// save output
|
||||
file, err := os.Create(fmt.Sprintf("%s/%s", tmpPath, backupFileName))
|
||||
file, err := os.Create(filepath.Join(tmpPath, backupFileName))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
utils.Fatal(err.Error())
|
||||
}
|
||||
defer file.Close()
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
utils.Fatal(err.Error())
|
||||
}
|
||||
}(file)
|
||||
|
||||
_, err = file.Write(output)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
utils.Fatal(err.Error())
|
||||
}
|
||||
utils.Done("Database has been backed up")
|
||||
utils.Info("Database has been backed up")
|
||||
|
||||
} else {
|
||||
// Execute mysqldump
|
||||
@@ -176,10 +239,10 @@ func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool
|
||||
}
|
||||
gzipCmd := exec.Command("gzip")
|
||||
gzipCmd.Stdin = stdout
|
||||
gzipCmd.Stdout, err = os.Create(fmt.Sprintf("%s/%s", tmpPath, backupFileName))
|
||||
gzipCmd.Start()
|
||||
gzipCmd.Stdout, err = os.Create(filepath.Join(tmpPath, backupFileName))
|
||||
err = gzipCmd.Start()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -187,113 +250,83 @@ func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool
|
||||
if err := gzipCmd.Wait(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
utils.Done("Database has been backed up")
|
||||
utils.Info("Database has been backed up")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
func localBackup(db *dbConfig, backupFileName string, disableCompression bool, prune bool, backupRetention int, encrypt bool) {
|
||||
func localBackup(db *dbConfig, config *BackupConfig) {
|
||||
utils.Info("Backup database to local storage")
|
||||
BackupDatabase(db, backupFileName, disableCompression)
|
||||
finalFileName := backupFileName
|
||||
if encrypt {
|
||||
encryptBackup(backupFileName)
|
||||
finalFileName = fmt.Sprintf("%s.%s", backupFileName, gpgExtension)
|
||||
startTime = time.Now().Format(utils.TimeFormat())
|
||||
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: %s", err)
|
||||
}
|
||||
backupSize = fileInfo.Size()
|
||||
utils.Info("Backup name is %s", finalFileName)
|
||||
moveToBackup(finalFileName, storagePath)
|
||||
localStorage := local.NewStorage(local.Config{
|
||||
LocalPath: tmpPath,
|
||||
RemotePath: storagePath,
|
||||
})
|
||||
err = localStorage.Copy(finalFileName)
|
||||
if err != nil {
|
||||
utils.Fatal("Error copying backup file: %s", err)
|
||||
}
|
||||
utils.Info("Backup saved in %s", filepath.Join(storagePath, finalFileName))
|
||||
// Send notification
|
||||
utils.NotifySuccess(finalFileName)
|
||||
utils.NotifySuccess(&utils.NotificationData{
|
||||
File: finalFileName,
|
||||
BackupSize: backupSize,
|
||||
Database: db.dbName,
|
||||
Storage: config.storage,
|
||||
BackupLocation: filepath.Join(storagePath, finalFileName),
|
||||
StartTime: startTime,
|
||||
EndTime: time.Now().Format(utils.TimeFormat()),
|
||||
})
|
||||
// Delete old backup
|
||||
if prune {
|
||||
deleteOldBackup(backupRetention)
|
||||
if config.prune {
|
||||
err = localStorage.Prune(config.backupRetention)
|
||||
if err != nil {
|
||||
utils.Fatal("Error deleting old backup from %s storage: %s ", config.storage, err)
|
||||
}
|
||||
|
||||
}
|
||||
// Delete temp
|
||||
deleteTemp()
|
||||
utils.Info("Backup completed successfully")
|
||||
}
|
||||
|
||||
func s3Backup(db *dbConfig, backupFileName string, disableCompression bool, prune bool, backupRetention int, encrypt bool) {
|
||||
bucket := utils.GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
|
||||
s3Path := utils.GetEnvVariable("AWS_S3_PATH", "S3_PATH")
|
||||
utils.Info("Backup database to s3 storage")
|
||||
//Backup database
|
||||
BackupDatabase(db, backupFileName, disableCompression)
|
||||
finalFileName := backupFileName
|
||||
if encrypt {
|
||||
encryptBackup(backupFileName)
|
||||
finalFileName = fmt.Sprintf("%s.%s", backupFileName, "gpg")
|
||||
}
|
||||
utils.Info("Uploading backup archive to remote storage S3 ... ")
|
||||
utils.Info("Backup name is %s", finalFileName)
|
||||
err := utils.UploadFileToS3(tmpPath, finalFileName, bucket, s3Path)
|
||||
func encryptBackup(config *BackupConfig) {
|
||||
backupFile, err := os.ReadFile(filepath.Join(tmpPath, config.backupFileName))
|
||||
outputFile := fmt.Sprintf("%s.%s", filepath.Join(tmpPath, config.backupFileName), gpgExtension)
|
||||
if err != nil {
|
||||
utils.Fatal("Error uploading file to S3: %s ", err)
|
||||
|
||||
utils.Fatal("Error reading backup file: %s ", err)
|
||||
}
|
||||
|
||||
//Delete backup file from tmp folder
|
||||
err = utils.DeleteFile(filepath.Join(tmpPath, backupFileName))
|
||||
if config.usingKey {
|
||||
utils.Info("Encrypting backup using public key...")
|
||||
pubKey, err := os.ReadFile(config.publicKey)
|
||||
if err != nil {
|
||||
fmt.Println("Error deleting file: ", err)
|
||||
|
||||
utils.Fatal("Error reading public key: %s ", err)
|
||||
}
|
||||
// Delete old backup
|
||||
if prune {
|
||||
err := utils.DeleteOldBackup(bucket, s3Path, backupRetention)
|
||||
err = encryptor.EncryptWithPublicKey(backupFile, fmt.Sprintf("%s.%s", filepath.Join(tmpPath, config.backupFileName), gpgExtension), pubKey)
|
||||
if err != nil {
|
||||
utils.Fatal("Error deleting old backup from S3: %s ", err)
|
||||
}
|
||||
}
|
||||
utils.Done("Uploading backup archive to remote storage S3 ... done ")
|
||||
//Send notification
|
||||
utils.NotifySuccess(finalFileName)
|
||||
//Delete temp
|
||||
deleteTemp()
|
||||
utils.Fatal("Error encrypting backup file: %v ", err)
|
||||
}
|
||||
utils.Info("Encrypting backup using public key...done")
|
||||
|
||||
// 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
|
||||
BackupDatabase(db, backupFileName, disableCompression)
|
||||
finalFileName := backupFileName
|
||||
if encrypt {
|
||||
encryptBackup(backupFileName)
|
||||
finalFileName = fmt.Sprintf("%s.%s", backupFileName, "gpg")
|
||||
}
|
||||
utils.Info("Uploading backup archive to remote storage ... ")
|
||||
utils.Info("Backup name is %s", finalFileName)
|
||||
err := CopyToRemote(finalFileName, remotePath)
|
||||
} else if config.passphrase != "" {
|
||||
utils.Info("Encrypting backup using passphrase...")
|
||||
err := encryptor.Encrypt(backupFile, outputFile, config.passphrase)
|
||||
if err != nil {
|
||||
utils.Fatal("Error uploading file to the remote server: %s ", err)
|
||||
utils.Fatal("error during encrypting backup %v", err)
|
||||
}
|
||||
utils.Info("Encrypting backup using passphrase...done")
|
||||
|
||||
}
|
||||
|
||||
//Delete backup file from tmp folder
|
||||
err = utils.DeleteFile(filepath.Join(tmpPath, finalFileName))
|
||||
if err != nil {
|
||||
fmt.Println("Error deleting file: ", err)
|
||||
|
||||
}
|
||||
if prune {
|
||||
//TODO: Delete old backup from remote server
|
||||
utils.Info("Deleting old backup from a remote server is not implemented yet")
|
||||
|
||||
}
|
||||
|
||||
utils.Done("Uploading backup archive to remote storage ... done ")
|
||||
//Send notification
|
||||
utils.NotifySuccess(finalFileName)
|
||||
//Delete temp
|
||||
deleteTemp()
|
||||
}
|
||||
|
||||
// encryptBackup encrypt backup
|
||||
func encryptBackup(backupFileName string) {
|
||||
gpgPassphrase := os.Getenv("GPG_PASSPHRASE")
|
||||
err := Encrypt(filepath.Join(tmpPath, backupFileName), gpgPassphrase)
|
||||
if err != nil {
|
||||
utils.Fatal("Error during encrypting backup %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
271
pkg/config.go
271
pkg/config.go
@@ -1,18 +1,48 @@
|
||||
// Package pkg /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jkaninda/mysql-bkup/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
Host string `yaml:"host"`
|
||||
Port string `yaml:"port"`
|
||||
Name string `yaml:"name"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
Path string `yaml:"path"`
|
||||
}
|
||||
type Config struct {
|
||||
Databases []Database `yaml:"databases"`
|
||||
CronExpression string `yaml:"cronExpression"`
|
||||
}
|
||||
|
||||
type dbConfig struct {
|
||||
@@ -29,8 +59,56 @@ type targetDbConfig struct {
|
||||
targetDbPassword string
|
||||
targetDbName string
|
||||
}
|
||||
type TgConfig struct {
|
||||
Token string
|
||||
ChatId string
|
||||
}
|
||||
type BackupConfig struct {
|
||||
backupFileName string
|
||||
backupRetention int
|
||||
disableCompression bool
|
||||
prune bool
|
||||
remotePath string
|
||||
encryption bool
|
||||
usingKey bool
|
||||
passphrase string
|
||||
publicKey string
|
||||
storage string
|
||||
cronExpression string
|
||||
}
|
||||
type FTPConfig struct {
|
||||
host string
|
||||
user string
|
||||
password string
|
||||
port string
|
||||
remotePath string
|
||||
}
|
||||
type AzureConfig struct {
|
||||
accountName string
|
||||
accountKey string
|
||||
containerName string
|
||||
}
|
||||
|
||||
func getDbConfig(cmd *cobra.Command) *dbConfig {
|
||||
// SSHConfig holds the SSH connection details
|
||||
type SSHConfig struct {
|
||||
user string
|
||||
password string
|
||||
hostName string
|
||||
port string
|
||||
identifyFile string
|
||||
}
|
||||
type AWSConfig struct {
|
||||
endpoint string
|
||||
bucket string
|
||||
accessKey string
|
||||
secretKey string
|
||||
region string
|
||||
remotePath string
|
||||
disableSsl bool
|
||||
forcePathStyle bool
|
||||
}
|
||||
|
||||
func initDbConfig(cmd *cobra.Command) *dbConfig {
|
||||
// Set env
|
||||
utils.GetEnv(cmd, "dbname", "DB_NAME")
|
||||
dConf := dbConfig{}
|
||||
@@ -47,10 +125,178 @@ func getDbConfig(cmd *cobra.Command) *dbConfig {
|
||||
}
|
||||
return &dConf
|
||||
}
|
||||
func getTargetDbConfig() *targetDbConfig {
|
||||
|
||||
func getDatabase(database Database) *dbConfig {
|
||||
return &dbConfig{
|
||||
dbHost: database.Host,
|
||||
dbPort: database.Port,
|
||||
dbName: database.Name,
|
||||
dbUserName: database.User,
|
||||
dbPassword: database.Password,
|
||||
}
|
||||
}
|
||||
|
||||
// loadSSHConfig loads the SSH configuration from environment variables
|
||||
func loadSSHConfig() (*SSHConfig, error) {
|
||||
utils.GetEnvVariable("SSH_HOST", "SSH_HOST_NAME")
|
||||
sshVars := []string{"SSH_USER", "SSH_HOST", "SSH_PORT", "REMOTE_PATH"}
|
||||
err := utils.CheckEnvVars(sshVars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error missing environment variables: %w", err)
|
||||
}
|
||||
|
||||
return &SSHConfig{
|
||||
user: os.Getenv("SSH_USER"),
|
||||
password: os.Getenv("SSH_PASSWORD"),
|
||||
hostName: os.Getenv("SSH_HOST"),
|
||||
port: os.Getenv("SSH_PORT"),
|
||||
identifyFile: os.Getenv("SSH_IDENTIFY_FILE"),
|
||||
}, nil
|
||||
}
|
||||
func loadFtpConfig() *FTPConfig {
|
||||
// Initialize data configs
|
||||
fConfig := FTPConfig{}
|
||||
fConfig.host = utils.GetEnvVariable("FTP_HOST", "FTP_HOST_NAME")
|
||||
fConfig.user = os.Getenv("FTP_USER")
|
||||
fConfig.password = os.Getenv("FTP_PASSWORD")
|
||||
fConfig.port = os.Getenv("FTP_PORT")
|
||||
fConfig.remotePath = os.Getenv("REMOTE_PATH")
|
||||
err := utils.CheckEnvVars(ftpVars)
|
||||
if err != nil {
|
||||
utils.Error("Please make sure all required environment variables for FTP are set")
|
||||
utils.Fatal("Error missing environment variables: %s", err)
|
||||
}
|
||||
return &fConfig
|
||||
}
|
||||
func loadAzureConfig() *AzureConfig {
|
||||
// Initialize data configs
|
||||
aConfig := AzureConfig{}
|
||||
aConfig.containerName = os.Getenv("AZURE_STORAGE_CONTAINER_NAME")
|
||||
aConfig.accountName = os.Getenv("AZURE_STORAGE_ACCOUNT_NAME")
|
||||
aConfig.accountKey = os.Getenv("AZURE_STORAGE_ACCOUNT_KEY")
|
||||
|
||||
err := utils.CheckEnvVars(azureVars)
|
||||
if err != nil {
|
||||
utils.Error("Please make sure all required environment variables for Azure Blob storage are set")
|
||||
utils.Fatal("Error missing environment variables: %s", err)
|
||||
}
|
||||
return &aConfig
|
||||
}
|
||||
|
||||
func initAWSConfig() *AWSConfig {
|
||||
// Initialize AWS configs
|
||||
aConfig := AWSConfig{}
|
||||
aConfig.endpoint = utils.GetEnvVariable("AWS_S3_ENDPOINT", "S3_ENDPOINT")
|
||||
aConfig.accessKey = utils.GetEnvVariable("AWS_ACCESS_KEY", "ACCESS_KEY")
|
||||
aConfig.secretKey = utils.GetEnvVariable("AWS_SECRET_KEY", "SECRET_KEY")
|
||||
aConfig.bucket = utils.GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
|
||||
aConfig.remotePath = utils.GetEnvVariable("AWS_S3_PATH", "S3_PATH")
|
||||
|
||||
aConfig.region = os.Getenv("AWS_REGION")
|
||||
disableSsl, err := strconv.ParseBool(os.Getenv("AWS_DISABLE_SSL"))
|
||||
if err != nil {
|
||||
disableSsl = false
|
||||
}
|
||||
forcePathStyle, err := strconv.ParseBool(os.Getenv("AWS_FORCE_PATH_STYLE"))
|
||||
if err != nil {
|
||||
forcePathStyle = false
|
||||
}
|
||||
aConfig.disableSsl = disableSsl
|
||||
aConfig.forcePathStyle = forcePathStyle
|
||||
err = utils.CheckEnvVars(awsVars)
|
||||
if err != nil {
|
||||
utils.Error("Please make sure all required environment variables for AWS S3 are set")
|
||||
utils.Fatal("Error checking environment variables: %s", err)
|
||||
}
|
||||
return &aConfig
|
||||
}
|
||||
func initBackupConfig(cmd *cobra.Command) *BackupConfig {
|
||||
utils.SetEnv("STORAGE_PATH", storagePath)
|
||||
utils.GetEnv(cmd, "cron-expression", "BACKUP_CRON_EXPRESSION")
|
||||
utils.GetEnv(cmd, "path", "REMOTE_PATH")
|
||||
// Get flag value and set env
|
||||
remotePath := utils.GetEnvVariable("REMOTE_PATH", "SSH_REMOTE_PATH")
|
||||
storage = utils.GetEnv(cmd, "storage", "STORAGE")
|
||||
prune := false
|
||||
backupRetention := utils.GetIntEnv("BACKUP_RETENTION_DAYS")
|
||||
if backupRetention > 0 {
|
||||
prune = true
|
||||
}
|
||||
disableCompression, _ = cmd.Flags().GetBool("disable-compression")
|
||||
_, _ = cmd.Flags().GetString("mode")
|
||||
passphrase := os.Getenv("GPG_PASSPHRASE")
|
||||
_ = utils.GetEnv(cmd, "path", "AWS_S3_PATH")
|
||||
cronExpression := os.Getenv("BACKUP_CRON_EXPRESSION")
|
||||
|
||||
publicKeyFile, err := checkPubKeyFile(os.Getenv("GPG_PUBLIC_KEY"))
|
||||
if err == nil {
|
||||
encryption = true
|
||||
usingKey = true
|
||||
} else if passphrase != "" {
|
||||
encryption = true
|
||||
usingKey = false
|
||||
}
|
||||
// Initialize backup configs
|
||||
config := BackupConfig{}
|
||||
config.backupRetention = backupRetention
|
||||
config.disableCompression = disableCompression
|
||||
config.prune = prune
|
||||
config.storage = storage
|
||||
config.encryption = encryption
|
||||
config.remotePath = remotePath
|
||||
config.passphrase = passphrase
|
||||
config.publicKey = publicKeyFile
|
||||
config.usingKey = usingKey
|
||||
config.cronExpression = cronExpression
|
||||
return &config
|
||||
}
|
||||
|
||||
type RestoreConfig struct {
|
||||
s3Path string
|
||||
remotePath string
|
||||
storage string
|
||||
file string
|
||||
bucket string
|
||||
usingKey bool
|
||||
passphrase string
|
||||
privateKey string
|
||||
}
|
||||
|
||||
func initRestoreConfig(cmd *cobra.Command) *RestoreConfig {
|
||||
utils.SetEnv("STORAGE_PATH", storagePath)
|
||||
utils.GetEnv(cmd, "path", "REMOTE_PATH")
|
||||
|
||||
// Get flag value and set env
|
||||
s3Path := utils.GetEnv(cmd, "path", "AWS_S3_PATH")
|
||||
remotePath := utils.GetEnvVariable("REMOTE_PATH", "SSH_REMOTE_PATH")
|
||||
storage = utils.GetEnv(cmd, "storage", "STORAGE")
|
||||
file = utils.GetEnv(cmd, "file", "FILE_NAME")
|
||||
bucket := utils.GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
|
||||
passphrase := os.Getenv("GPG_PASSPHRASE")
|
||||
privateKeyFile, err := checkPrKeyFile(os.Getenv("GPG_PRIVATE_KEY"))
|
||||
if err == nil {
|
||||
usingKey = true
|
||||
} else if passphrase != "" {
|
||||
usingKey = false
|
||||
}
|
||||
|
||||
// Initialize restore configs
|
||||
rConfig := RestoreConfig{}
|
||||
rConfig.s3Path = s3Path
|
||||
rConfig.remotePath = remotePath
|
||||
rConfig.storage = storage
|
||||
rConfig.bucket = bucket
|
||||
rConfig.file = file
|
||||
rConfig.storage = storage
|
||||
rConfig.passphrase = passphrase
|
||||
rConfig.usingKey = usingKey
|
||||
rConfig.privateKey = privateKeyFile
|
||||
return &rConfig
|
||||
}
|
||||
func initTargetDbConfig() *targetDbConfig {
|
||||
tdbConfig := targetDbConfig{}
|
||||
tdbConfig.targetDbHost = os.Getenv("TARGET_DB_HOST")
|
||||
tdbConfig.targetDbPort = os.Getenv("TARGET_DB_PORT")
|
||||
tdbConfig.targetDbPort = utils.EnvWithDefault("TARGET_DB_PORT", "3306")
|
||||
tdbConfig.targetDbName = os.Getenv("TARGET_DB_NAME")
|
||||
tdbConfig.targetDbUserName = os.Getenv("TARGET_DB_USERNAME")
|
||||
tdbConfig.targetDbPassword = os.Getenv("TARGET_DB_PASSWORD")
|
||||
@@ -62,3 +308,10 @@ func getTargetDbConfig() *targetDbConfig {
|
||||
}
|
||||
return &tdbConfig
|
||||
}
|
||||
func loadConfigFile() (string, error) {
|
||||
backupConfigFile, err := checkConfigFile(os.Getenv("BACKUP_CONFIG_FILE"))
|
||||
if err == nil {
|
||||
return backupConfigFile, nil
|
||||
}
|
||||
return "", fmt.Errorf("backup config file not found")
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
// Package pkg /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"github.com/jkaninda/mysql-bkup/utils"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Decrypt(inputFile string, passphrase string) error {
|
||||
utils.Info("Decrypting backup file: " + inputFile + " ...")
|
||||
//Create gpg home dir
|
||||
err := utils.MakeDir(gpgHome)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
utils.SetEnv("GNUPGHOME", gpgHome)
|
||||
cmd := exec.Command("gpg", "--batch", "--passphrase", passphrase, "--output", RemoveLastExtension(inputFile), "--decrypt", inputFile)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Info("Backup file decrypted successful!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func Encrypt(inputFile string, passphrase string) error {
|
||||
utils.Info("Encrypting backup...")
|
||||
//Create gpg home dir
|
||||
err := utils.MakeDir(gpgHome)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
utils.SetEnv("GNUPGHOME", gpgHome)
|
||||
cmd := exec.Command("gpg", "--batch", "--passphrase", passphrase, "--symmetric", "--cipher-algo", algorithm, inputFile)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Info("Backup file encrypted successful!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoveLastExtension(filename string) string {
|
||||
if idx := strings.LastIndex(filename, "."); idx != -1 {
|
||||
return filename[:idx]
|
||||
}
|
||||
return filename
|
||||
}
|
||||
196
pkg/helper.go
196
pkg/helper.go
@@ -1,87 +1,47 @@
|
||||
// Package pkg /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/jkaninda/mysql-bkup/utils"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func copyToTmp(sourcePath string, backupFileName string) {
|
||||
//Copy backup from storage to /tmp
|
||||
err := utils.CopyFile(filepath.Join(sourcePath, backupFileName), filepath.Join(tmpPath, backupFileName))
|
||||
if err != nil {
|
||||
utils.Fatal(fmt.Sprintf("Error copying file %s %s", backupFileName, err))
|
||||
|
||||
}
|
||||
}
|
||||
func moveToBackup(backupFileName string, destinationPath string) {
|
||||
//Copy backup from tmp folder to storage destination
|
||||
err := utils.CopyFile(filepath.Join(tmpPath, backupFileName), filepath.Join(destinationPath, backupFileName))
|
||||
if err != nil {
|
||||
utils.Fatal(fmt.Sprintf("Error copying file %s %s", backupFileName, err))
|
||||
|
||||
}
|
||||
//Delete backup file from tmp folder
|
||||
err = utils.DeleteFile(filepath.Join(tmpPath, backupFileName))
|
||||
if err != nil {
|
||||
fmt.Println("Error deleting file:", err)
|
||||
|
||||
}
|
||||
utils.Done("Database has been backed up and copied to %s", filepath.Join(destinationPath, backupFileName))
|
||||
}
|
||||
func deleteOldBackup(retentionDays int) {
|
||||
utils.Info("Deleting old backups...")
|
||||
storagePath = os.Getenv("STORAGE_PATH")
|
||||
// Define the directory path
|
||||
backupDir := storagePath + "/"
|
||||
// Get current time
|
||||
currentTime := time.Now()
|
||||
// Delete file
|
||||
deleteFile := func(filePath string) error {
|
||||
err := os.Remove(filePath)
|
||||
if err != nil {
|
||||
utils.Fatal(fmt.Sprintf("Error: %s", err))
|
||||
} else {
|
||||
utils.Done("File %s has been deleted successfully", filePath)
|
||||
}
|
||||
return err
|
||||
func intro() {
|
||||
fmt.Println("Starting MySQL Backup...")
|
||||
fmt.Printf("Version: %s\n", utils.Version)
|
||||
fmt.Println("Copyright (c) 2024 Jonas Kaninda")
|
||||
}
|
||||
|
||||
// Walk through the directory and delete files modified more than specified days ago
|
||||
err := filepath.Walk(backupDir, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Check if it's a regular file and if it was modified more than specified days ago
|
||||
if fileInfo.Mode().IsRegular() {
|
||||
timeDiff := currentTime.Sub(fileInfo.ModTime())
|
||||
if timeDiff.Hours() > 24*float64(retentionDays) {
|
||||
err := deleteFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
utils.Fatal(fmt.Sprintf("Error: %s", err))
|
||||
return
|
||||
}
|
||||
utils.Done("Deleting old backups...done")
|
||||
|
||||
}
|
||||
// copyToTmp copy file to temporary directory
|
||||
func deleteTemp() {
|
||||
utils.Info("Deleting %s ...", tmpPath)
|
||||
err := filepath.Walk(tmpPath, func(path string, info os.FileInfo, err error) error {
|
||||
@@ -125,3 +85,97 @@ func testDatabaseConnection(db *dbConfig) {
|
||||
utils.Info("Successfully connected to %s database", db.dbName)
|
||||
|
||||
}
|
||||
|
||||
// checkPubKeyFile checks gpg public key
|
||||
func checkPubKeyFile(pubKey string) (string, error) {
|
||||
// Define possible key file names
|
||||
keyFiles := []string{filepath.Join(gpgHome, "public_key.asc"), filepath.Join(gpgHome, "public_key.gpg"), pubKey}
|
||||
|
||||
// Loop through key file names and check if they exist
|
||||
for _, keyFile := range keyFiles {
|
||||
if _, err := os.Stat(keyFile); err == nil {
|
||||
// File exists
|
||||
return keyFile, nil
|
||||
} else if os.IsNotExist(err) {
|
||||
// File does not exist, continue to the next one
|
||||
continue
|
||||
} else {
|
||||
// An unexpected error occurred
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Return an error if neither file exists
|
||||
return "", fmt.Errorf("no public key file found")
|
||||
}
|
||||
|
||||
// checkPrKeyFile checks private key
|
||||
func checkPrKeyFile(prKey string) (string, error) {
|
||||
// Define possible key file names
|
||||
keyFiles := []string{filepath.Join(gpgHome, "private_key.asc"), filepath.Join(gpgHome, "private_key.gpg"), prKey}
|
||||
|
||||
// Loop through key file names and check if they exist
|
||||
for _, keyFile := range keyFiles {
|
||||
if _, err := os.Stat(keyFile); err == nil {
|
||||
// File exists
|
||||
return keyFile, nil
|
||||
} else if os.IsNotExist(err) {
|
||||
// File does not exist, continue to the next one
|
||||
continue
|
||||
} else {
|
||||
// An unexpected error occurred
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Return an error if neither file exists
|
||||
return "", fmt.Errorf("no public key file found")
|
||||
}
|
||||
|
||||
// readConf reads config file and returns Config
|
||||
func readConf(configFile string) (*Config, error) {
|
||||
if utils.FileExists(configFile) {
|
||||
buf, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Config{}
|
||||
err = yaml.Unmarshal(buf, c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("in file %q: %w", configFile, err)
|
||||
}
|
||||
|
||||
return c, err
|
||||
}
|
||||
return nil, fmt.Errorf("config file %q not found", configFile)
|
||||
}
|
||||
|
||||
// checkConfigFile checks config files and returns one config file
|
||||
func checkConfigFile(filePath string) (string, error) {
|
||||
// Define possible config file names
|
||||
configFiles := []string{filepath.Join(workingDir, "config.yaml"), filepath.Join(workingDir, "config.yml"), filePath}
|
||||
|
||||
// Loop through config file names and check if they exist
|
||||
for _, configFile := range configFiles {
|
||||
if _, err := os.Stat(configFile); err == nil {
|
||||
// File exists
|
||||
return configFile, nil
|
||||
} else if os.IsNotExist(err) {
|
||||
// File does not exist, continue to the next one
|
||||
continue
|
||||
} else {
|
||||
// An unexpected error occurred
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Return an error if neither file exists
|
||||
return "", fmt.Errorf("no config file found")
|
||||
}
|
||||
func RemoveLastExtension(filename string) string {
|
||||
if idx := strings.LastIndex(filename, "."); idx != -1 {
|
||||
return filename[:idx]
|
||||
}
|
||||
return filename
|
||||
}
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
// Package pkg /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package pkg
|
||||
|
||||
import (
|
||||
@@ -17,8 +35,8 @@ func StartMigration(cmd *cobra.Command) {
|
||||
intro()
|
||||
utils.Info("Starting database migration...")
|
||||
// Get DB config
|
||||
dbConf = getDbConfig(cmd)
|
||||
targetDbConf = getTargetDbConfig()
|
||||
dbConf = initDbConfig(cmd)
|
||||
targetDbConf = initTargetDbConfig()
|
||||
|
||||
// Defining the target database variables
|
||||
newDbConfig := dbConfig{}
|
||||
@@ -30,11 +48,13 @@ func StartMigration(cmd *cobra.Command) {
|
||||
|
||||
// Generate file name
|
||||
backupFileName := fmt.Sprintf("%s_%s.sql", dbConf.dbName, time.Now().Format("20060102_150405"))
|
||||
conf := &RestoreConfig{}
|
||||
conf.file = backupFileName
|
||||
// Backup source Database
|
||||
BackupDatabase(dbConf, backupFileName, true)
|
||||
// Restore source database into target database
|
||||
utils.Info("Restoring [%s] database into [%s] database...", dbConf.dbName, targetDbConf.targetDbName)
|
||||
RestoreDatabase(&newDbConfig, backupFileName)
|
||||
RestoreDatabase(&newDbConfig, conf)
|
||||
utils.Info("[%s] database has been restored into [%s] database", dbConf.dbName, targetDbConf.targetDbName)
|
||||
utils.Info("Database migration completed.")
|
||||
}
|
||||
|
||||
217
pkg/remote.go
Normal file
217
pkg/remote.go
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jkaninda/go-storage/pkg/ftp"
|
||||
"github.com/jkaninda/go-storage/pkg/ssh"
|
||||
"github.com/jkaninda/mysql-bkup/utils"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func sshBackup(db *dbConfig, config *BackupConfig) {
|
||||
utils.Info("Backup database to Remote 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 remote storage ... ")
|
||||
utils.Info("Backup name is %s", finalFileName)
|
||||
sshConfig, err := loadSSHConfig()
|
||||
if err != nil {
|
||||
utils.Fatal("Error loading ssh config: %s", err)
|
||||
}
|
||||
|
||||
sshStorage, err := ssh.NewStorage(ssh.Config{
|
||||
Host: sshConfig.hostName,
|
||||
Port: sshConfig.port,
|
||||
User: sshConfig.user,
|
||||
Password: sshConfig.password,
|
||||
RemotePath: config.remotePath,
|
||||
LocalPath: tmpPath,
|
||||
})
|
||||
if err != nil {
|
||||
utils.Fatal("Error creating SSH storage: %s", err)
|
||||
}
|
||||
err = sshStorage.Copy(finalFileName)
|
||||
if err != nil {
|
||||
utils.Fatal("Error copying backup file: %s", err)
|
||||
}
|
||||
// Get backup info
|
||||
fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName))
|
||||
if err != nil {
|
||||
utils.Error("Error: %s", err)
|
||||
}
|
||||
backupSize = fileInfo.Size()
|
||||
utils.Info("Backup saved in %s", filepath.Join(config.remotePath, finalFileName))
|
||||
|
||||
// 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 := sshStorage.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 remote storage ... 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 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) {
|
||||
utils.Info("Restore database from remote server")
|
||||
sshConfig, err := loadSSHConfig()
|
||||
if err != nil {
|
||||
utils.Fatal("Error loading ssh config: %s", err)
|
||||
}
|
||||
|
||||
sshStorage, err := ssh.NewStorage(ssh.Config{
|
||||
Host: sshConfig.hostName,
|
||||
Port: sshConfig.port,
|
||||
User: sshConfig.user,
|
||||
Password: sshConfig.password,
|
||||
IdentifyFile: sshConfig.identifyFile,
|
||||
RemotePath: conf.remotePath,
|
||||
LocalPath: tmpPath,
|
||||
})
|
||||
if err != nil {
|
||||
utils.Fatal("Error creating SSH storage: %s", err)
|
||||
}
|
||||
err = sshStorage.CopyFrom(conf.file)
|
||||
if err != nil {
|
||||
utils.Fatal("Error copying backup file: %s", err)
|
||||
}
|
||||
RestoreDatabase(db, conf)
|
||||
}
|
||||
func ftpRestore(db *dbConfig, conf *RestoreConfig) {
|
||||
utils.Info("Restore database from FTP server")
|
||||
ftpConfig := loadFtpConfig()
|
||||
ftpStorage, err := ftp.NewStorage(ftp.Config{
|
||||
Host: ftpConfig.host,
|
||||
Port: ftpConfig.port,
|
||||
User: ftpConfig.user,
|
||||
Password: ftpConfig.password,
|
||||
RemotePath: conf.remotePath,
|
||||
LocalPath: tmpPath,
|
||||
})
|
||||
if err != nil {
|
||||
utils.Fatal("Error creating SSH storage: %s", err)
|
||||
}
|
||||
err = ftpStorage.CopyFrom(conf.file)
|
||||
if err != nil {
|
||||
utils.Fatal("Error copying backup file: %s", err)
|
||||
}
|
||||
RestoreDatabase(db, conf)
|
||||
}
|
||||
157
pkg/restore.go
157
pkg/restore.go
@@ -1,13 +1,32 @@
|
||||
// Package pkg /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jkaninda/encryptor"
|
||||
"github.com/jkaninda/go-storage/pkg/local"
|
||||
"github.com/jkaninda/mysql-bkup/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
@@ -17,113 +36,113 @@ import (
|
||||
|
||||
func StartRestore(cmd *cobra.Command) {
|
||||
intro()
|
||||
//Set env
|
||||
utils.SetEnv("STORAGE_PATH", storagePath)
|
||||
dbConf = initDbConfig(cmd)
|
||||
restoreConf := initRestoreConfig(cmd)
|
||||
|
||||
//Get flag value and set env
|
||||
s3Path := utils.GetEnv(cmd, "path", "AWS_S3_PATH")
|
||||
remotePath := utils.GetEnv(cmd, "path", "SSH_REMOTE_PATH")
|
||||
storage = utils.GetEnv(cmd, "storage", "STORAGE")
|
||||
file = utils.GetEnv(cmd, "file", "FILE_NAME")
|
||||
executionMode, _ = cmd.Flags().GetString("mode")
|
||||
bucket := utils.GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
|
||||
dbConf = getDbConfig(cmd)
|
||||
|
||||
switch storage {
|
||||
case "s3":
|
||||
restoreFromS3(dbConf, file, bucket, s3Path)
|
||||
switch restoreConf.storage {
|
||||
case "local":
|
||||
utils.Info("Restore database from local")
|
||||
copyToTmp(storagePath, file)
|
||||
RestoreDatabase(dbConf, file)
|
||||
case "ssh":
|
||||
restoreFromRemote(dbConf, file, remotePath)
|
||||
case "ftp":
|
||||
utils.Fatal("Restore from FTP is not yet supported")
|
||||
localRestore(dbConf, restoreConf)
|
||||
case "s3", "S3":
|
||||
s3Restore(dbConf, restoreConf)
|
||||
case "ssh", "SSH", "remote":
|
||||
remoteRestore(dbConf, restoreConf)
|
||||
case "ftp", "FTP":
|
||||
ftpRestore(dbConf, restoreConf)
|
||||
case "azure":
|
||||
azureRestore(dbConf, restoreConf)
|
||||
default:
|
||||
localRestore(dbConf, restoreConf)
|
||||
}
|
||||
}
|
||||
func localRestore(dbConf *dbConfig, restoreConf *RestoreConfig) {
|
||||
utils.Info("Restore database from local")
|
||||
copyToTmp(storagePath, file)
|
||||
RestoreDatabase(dbConf, file)
|
||||
}
|
||||
localStorage := local.NewStorage(local.Config{
|
||||
RemotePath: storagePath,
|
||||
LocalPath: tmpPath,
|
||||
})
|
||||
err := localStorage.CopyFrom(restoreConf.file)
|
||||
if err != nil {
|
||||
utils.Fatal("Error copying backup file: %s", err)
|
||||
}
|
||||
RestoreDatabase(dbConf, restoreConf)
|
||||
|
||||
func restoreFromS3(db *dbConfig, file, bucket, s3Path string) {
|
||||
utils.Info("Restore database from s3")
|
||||
err := utils.DownloadFile(tmpPath, file, bucket, s3Path)
|
||||
if err != nil {
|
||||
utils.Fatal("Error download file from s3 %s %v", file, err)
|
||||
}
|
||||
RestoreDatabase(db, file)
|
||||
}
|
||||
func restoreFromRemote(db *dbConfig, file, remotePath string) {
|
||||
utils.Info("Restore database from remote server")
|
||||
err := CopyFromRemote(file, remotePath)
|
||||
if err != nil {
|
||||
utils.Fatal("Error download file from remote server: %s %v ", filepath.Join(remotePath, file), err)
|
||||
}
|
||||
RestoreDatabase(db, file)
|
||||
}
|
||||
|
||||
// RestoreDatabase restore database
|
||||
func RestoreDatabase(db *dbConfig, file string) {
|
||||
gpgPassphrase := os.Getenv("GPG_PASSPHRASE")
|
||||
if file == "" {
|
||||
func RestoreDatabase(db *dbConfig, conf *RestoreConfig) {
|
||||
if conf.file == "" {
|
||||
utils.Fatal("Error, file required")
|
||||
}
|
||||
|
||||
err := utils.CheckEnvVars(dbHVars)
|
||||
extension := filepath.Ext(filepath.Join(tmpPath, conf.file))
|
||||
rFile, err := os.ReadFile(filepath.Join(tmpPath, conf.file))
|
||||
outputFile := RemoveLastExtension(filepath.Join(tmpPath, conf.file))
|
||||
if err != nil {
|
||||
utils.Error("Please make sure all required environment variables for database are set")
|
||||
utils.Fatal("Error checking environment variables: %s", err)
|
||||
utils.Fatal("Error reading backup file: %s ", err)
|
||||
}
|
||||
|
||||
extension := filepath.Ext(fmt.Sprintf("%s/%s", tmpPath, file))
|
||||
if extension == ".gpg" {
|
||||
if gpgPassphrase == "" {
|
||||
utils.Fatal("Error: GPG passphrase is required, your file seems to be a GPG file.\nYou need to provide GPG keys. GPG_PASSPHRASE environment variable is required.")
|
||||
|
||||
if conf.usingKey {
|
||||
utils.Info("Decrypting backup using private key...")
|
||||
utils.Warn("Backup decryption using a private key is not fully supported")
|
||||
prKey, err := os.ReadFile(conf.privateKey)
|
||||
if err != nil {
|
||||
utils.Fatal("Error reading public key: %s ", err)
|
||||
}
|
||||
err = encryptor.DecryptWithPrivateKey(rFile, outputFile, prKey, conf.passphrase)
|
||||
if err != nil {
|
||||
utils.Fatal("error during decrypting backup %v", err)
|
||||
}
|
||||
utils.Info("Decrypting backup using private key...done")
|
||||
} else {
|
||||
//Decrypt file
|
||||
err := Decrypt(filepath.Join(tmpPath, file), gpgPassphrase)
|
||||
if conf.passphrase == "" {
|
||||
utils.Error("Error, passphrase or private key required")
|
||||
utils.Fatal("Your file seems to be a GPG file.\nYou need to provide GPG keys. GPG_PASSPHRASE or GPG_PRIVATE_KEY environment variable is required.")
|
||||
} else {
|
||||
utils.Info("Decrypting backup using passphrase...")
|
||||
// decryptWithGPG file
|
||||
err := encryptor.Decrypt(rFile, outputFile, conf.passphrase)
|
||||
if err != nil {
|
||||
utils.Fatal("Error decrypting file %s %v", file, err)
|
||||
}
|
||||
utils.Info("Decrypting backup using passphrase...done")
|
||||
// Update file name
|
||||
file = RemoveLastExtension(file)
|
||||
conf.file = RemoveLastExtension(file)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if utils.FileExists(fmt.Sprintf("%s/%s", tmpPath, file)) {
|
||||
err = os.Setenv("MYSQL_PWD", db.dbPassword)
|
||||
if utils.FileExists(filepath.Join(tmpPath, conf.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))
|
||||
extension := filepath.Ext(filepath.Join(tmpPath, conf.file))
|
||||
// Restore from compressed file / .sql.gz
|
||||
if extension == ".gz" {
|
||||
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()
|
||||
str := "zcat " + filepath.Join(tmpPath, conf.file) + " | mysql -h " + db.dbHost + " -P " + db.dbPort + " -u " + db.dbUserName + " " + db.dbName
|
||||
_, err := exec.Command("sh", "-c", str).Output()
|
||||
if err != nil {
|
||||
utils.Fatal("Error, in restoring the database %v", err)
|
||||
}
|
||||
utils.Info("Restoring database... done")
|
||||
utils.Done("Database has been restored")
|
||||
utils.Info("Database has been restored")
|
||||
// Delete temp
|
||||
deleteTemp()
|
||||
|
||||
} else if extension == ".sql" {
|
||||
// Restore from sql file
|
||||
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()
|
||||
str := "cat " + filepath.Join(tmpPath, conf.file) + " | mysql -h " + db.dbHost + " -P " + db.dbPort + " -u " + db.dbUserName + " " + db.dbName
|
||||
_, err := exec.Command("sh", "-c", str).Output()
|
||||
if err != nil {
|
||||
utils.Fatal("Error in restoring the database %v", err)
|
||||
}
|
||||
utils.Info("Restoring database... done")
|
||||
utils.Done("Database has been restored")
|
||||
utils.Info("Database has been restored")
|
||||
// Delete temp
|
||||
deleteTemp()
|
||||
} else {
|
||||
@@ -131,6 +150,6 @@ func RestoreDatabase(db *dbConfig, file string) {
|
||||
}
|
||||
|
||||
} else {
|
||||
utils.Fatal("File not found in %s", filepath.Join(tmpPath, file))
|
||||
utils.Fatal("File not found in %s", filepath.Join(tmpPath, conf.file))
|
||||
}
|
||||
}
|
||||
|
||||
134
pkg/s3.go
Normal file
134
pkg/s3.go
Normal file
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jkaninda/go-storage/pkg/s3"
|
||||
"github.com/jkaninda/mysql-bkup/utils"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func s3Backup(db *dbConfig, config *BackupConfig) {
|
||||
|
||||
utils.Info("Backup database to s3 storage")
|
||||
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 remote storage S3 ... ")
|
||||
awsConfig := initAWSConfig()
|
||||
if config.remotePath == "" {
|
||||
config.remotePath = awsConfig.remotePath
|
||||
}
|
||||
utils.Info("Backup name is %s", finalFileName)
|
||||
s3Storage, err := s3.NewStorage(s3.Config{
|
||||
Endpoint: awsConfig.endpoint,
|
||||
Bucket: awsConfig.bucket,
|
||||
AccessKey: awsConfig.accessKey,
|
||||
SecretKey: awsConfig.secretKey,
|
||||
Region: awsConfig.region,
|
||||
DisableSsl: awsConfig.disableSsl,
|
||||
ForcePathStyle: awsConfig.forcePathStyle,
|
||||
RemotePath: config.remotePath,
|
||||
LocalPath: tmpPath,
|
||||
})
|
||||
if err != nil {
|
||||
utils.Fatal("Error creating s3 storage: %s", err)
|
||||
}
|
||||
err = s3Storage.Copy(finalFileName)
|
||||
if err != nil {
|
||||
utils.Fatal("Error copying backup file: %s", err)
|
||||
}
|
||||
// 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, config.backupFileName))
|
||||
if err != nil {
|
||||
fmt.Println("Error deleting file: ", err)
|
||||
|
||||
}
|
||||
// Delete old backup
|
||||
if config.prune {
|
||||
err := s3Storage.Prune(config.backupRetention)
|
||||
if err != nil {
|
||||
utils.Fatal("Error deleting old backup from %s storage: %s ", config.storage, err)
|
||||
}
|
||||
}
|
||||
utils.Info("Backup saved in %s", filepath.Join(config.remotePath, finalFileName))
|
||||
utils.Info("Uploading backup archive to remote storage S3 ... 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 s3Restore(db *dbConfig, conf *RestoreConfig) {
|
||||
utils.Info("Restore database from s3")
|
||||
awsConfig := initAWSConfig()
|
||||
if conf.remotePath == "" {
|
||||
conf.remotePath = awsConfig.remotePath
|
||||
}
|
||||
s3Storage, err := s3.NewStorage(s3.Config{
|
||||
Endpoint: awsConfig.endpoint,
|
||||
Bucket: awsConfig.bucket,
|
||||
AccessKey: awsConfig.accessKey,
|
||||
SecretKey: awsConfig.secretKey,
|
||||
Region: awsConfig.region,
|
||||
DisableSsl: awsConfig.disableSsl,
|
||||
ForcePathStyle: awsConfig.forcePathStyle,
|
||||
RemotePath: conf.remotePath,
|
||||
LocalPath: tmpPath,
|
||||
})
|
||||
if err != nil {
|
||||
utils.Fatal("Error creating s3 storage: %s", err)
|
||||
}
|
||||
err = s3Storage.CopyFrom(conf.file)
|
||||
if err != nil {
|
||||
utils.Fatal("Error download file from S3 storage: %s", err)
|
||||
}
|
||||
RestoreDatabase(db, conf)
|
||||
}
|
||||
121
pkg/scp.go
121
pkg/scp.go
@@ -1,121 +0,0 @@
|
||||
// Package pkg /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/bramvdbogaerde/go-scp"
|
||||
"github.com/bramvdbogaerde/go-scp/auth"
|
||||
"github.com/jkaninda/mysql-bkup/utils"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func CopyToRemote(fileName, remotePath string) error {
|
||||
sshUser := os.Getenv("SSH_USER")
|
||||
sshPassword := os.Getenv("SSH_PASSWORD")
|
||||
sshHostName := os.Getenv("SSH_HOST_NAME")
|
||||
sshPort := os.Getenv("SSH_PORT")
|
||||
sshIdentifyFile := os.Getenv("SSH_IDENTIFY_FILE")
|
||||
|
||||
err := utils.CheckEnvVars(sshHVars)
|
||||
if err != nil {
|
||||
utils.Error("Error checking environment variables: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
clientConfig, _ := auth.PasswordKey(sshUser, sshPassword, ssh.InsecureIgnoreHostKey())
|
||||
if sshIdentifyFile != "" && utils.FileExists(sshIdentifyFile) {
|
||||
clientConfig, _ = auth.PrivateKey(sshUser, sshIdentifyFile, ssh.InsecureIgnoreHostKey())
|
||||
|
||||
} else {
|
||||
if sshPassword == "" {
|
||||
return errors.New("SSH_PASSWORD environment variable is required if SSH_IDENTIFY_FILE is empty")
|
||||
}
|
||||
utils.Warn("Accessing the remote server using password, password is not recommended")
|
||||
clientConfig, _ = auth.PasswordKey(sshUser, sshPassword, ssh.InsecureIgnoreHostKey())
|
||||
|
||||
}
|
||||
// Create a new SCP client
|
||||
client := scp.NewClient(fmt.Sprintf("%s:%s", sshHostName, sshPort), &clientConfig)
|
||||
|
||||
// Connect to the remote server
|
||||
err = client.Connect()
|
||||
if err != nil {
|
||||
return errors.New("Couldn't establish a connection to the remote server")
|
||||
}
|
||||
|
||||
// Open a file
|
||||
file, _ := os.Open(filepath.Join(tmpPath, fileName))
|
||||
|
||||
// Close client connection after the file has been copied
|
||||
defer client.Close()
|
||||
// Close the file after it has been copied
|
||||
defer file.Close()
|
||||
// the context can be adjusted to provide time-outs or inherit from other contexts if this is embedded in a larger application.
|
||||
err = client.CopyFromFile(context.Background(), *file, filepath.Join(remotePath, fileName), "0655")
|
||||
if err != nil {
|
||||
fmt.Println("Error while copying file ")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CopyFromRemote(fileName, remotePath string) error {
|
||||
sshUser := os.Getenv("SSH_USER")
|
||||
sshPassword := os.Getenv("SSH_PASSWORD")
|
||||
sshHostName := os.Getenv("SSH_HOST_NAME")
|
||||
sshPort := os.Getenv("SSH_PORT")
|
||||
sshIdentifyFile := os.Getenv("SSH_IDENTIFY_FILE")
|
||||
|
||||
err := utils.CheckEnvVars(sshHVars)
|
||||
if err != nil {
|
||||
utils.Error("Error checking environment variables\n: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
clientConfig, _ := auth.PasswordKey(sshUser, sshPassword, ssh.InsecureIgnoreHostKey())
|
||||
if sshIdentifyFile != "" && utils.FileExists(sshIdentifyFile) {
|
||||
clientConfig, _ = auth.PrivateKey(sshUser, sshIdentifyFile, ssh.InsecureIgnoreHostKey())
|
||||
|
||||
} else {
|
||||
if sshPassword == "" {
|
||||
return errors.New("SSH_PASSWORD environment variable is required if SSH_IDENTIFY_FILE is empty\n")
|
||||
}
|
||||
utils.Warn("Accessing the remote server using password, password is not recommended")
|
||||
clientConfig, _ = auth.PasswordKey(sshUser, sshPassword, ssh.InsecureIgnoreHostKey())
|
||||
|
||||
}
|
||||
// Create a new SCP client
|
||||
client := scp.NewClient(fmt.Sprintf("%s:%s", sshHostName, sshPort), &clientConfig)
|
||||
|
||||
// Connect to the remote server
|
||||
err = client.Connect()
|
||||
if err != nil {
|
||||
return errors.New("Couldn't establish a connection to the remote server\n")
|
||||
}
|
||||
// Close client connection after the file has been copied
|
||||
defer client.Close()
|
||||
file, err := os.OpenFile(filepath.Join(tmpPath, fileName), os.O_RDWR|os.O_CREATE, 0777)
|
||||
if err != nil {
|
||||
fmt.Println("Couldn't open the output file")
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// the context can be adjusted to provide time-outs or inherit from other contexts if this is embedded in a larger application.
|
||||
err = client.CopyFromRemote(context.Background(), file, filepath.Join(remotePath, fileName))
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error while copying file ", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Package pkg /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jkaninda/mysql-bkup/utils"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func CreateCrontabScript(disableCompression bool, storage string) {
|
||||
//task := "/usr/local/bin/backup_cron.sh"
|
||||
touchCmd := exec.Command("touch", backupCronFile)
|
||||
if err := touchCmd.Run(); err != nil {
|
||||
utils.Fatal("Error creating file %s: %v\n", backupCronFile, err)
|
||||
}
|
||||
var disableC = ""
|
||||
if disableCompression {
|
||||
disableC = "--disable-compression"
|
||||
}
|
||||
|
||||
scriptContent := fmt.Sprintf(`#!/usr/bin/env bash
|
||||
set -e
|
||||
/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)
|
||||
}
|
||||
|
||||
chmodCmd := exec.Command("chmod", "+x", "/usr/local/bin/backup_cron.sh")
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
utils.Fatal("Error changing permissions of %s: %v\n", backupCronFile, err)
|
||||
}
|
||||
|
||||
lnCmd := exec.Command("ln", "-s", "/usr/local/bin/backup_cron.sh", "/usr/local/bin/backup_cron")
|
||||
if err := lnCmd.Run(); err != nil {
|
||||
utils.Fatal("Error creating symbolic link: %v\n", err)
|
||||
|
||||
}
|
||||
|
||||
touchLogCmd := exec.Command("touch", cronLogFile)
|
||||
if err := touchLogCmd.Run(); err != nil {
|
||||
utils.Fatal("Error creating file %s: %v\n", cronLogFile, err)
|
||||
}
|
||||
|
||||
cronJob := "/etc/cron.d/backup_cron"
|
||||
touchCronCmd := exec.Command("touch", cronJob)
|
||||
if err := touchCronCmd.Run(); err != nil {
|
||||
utils.Fatal("Error creating file %s: %v\n", cronJob, err)
|
||||
}
|
||||
|
||||
cronContent := fmt.Sprintf(`%s root exec /bin/bash -c ". /run/supervisord.env; /usr/local/bin/backup_cron.sh >> %s"
|
||||
`, os.Getenv("BACKUP_CRON_EXPRESSION"), cronLogFile)
|
||||
|
||||
if err := utils.WriteToFile(cronJob, cronContent); err != nil {
|
||||
utils.Fatal("Error writing to %s: %v\n", cronJob, err)
|
||||
}
|
||||
utils.ChangePermission("/etc/cron.d/backup_cron", 0644)
|
||||
|
||||
crontabCmd := exec.Command("crontab", "/etc/cron.d/backup_cron")
|
||||
if err := crontabCmd.Run(); err != nil {
|
||||
utils.Fatal("Error updating crontab: ", err)
|
||||
}
|
||||
utils.Info("Backup job created.")
|
||||
}
|
||||
69
pkg/var.go
69
pkg/var.go
@@ -1,30 +1,51 @@
|
||||
// Package pkg /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package pkg
|
||||
|
||||
const cronLogFile = "/var/log/mysql-bkup.log"
|
||||
const tmpPath = "/tmp/backup"
|
||||
const backupCronFile = "/usr/local/bin/backup_cron.sh"
|
||||
const algorithm = "aes256"
|
||||
const gpgHome = "gnupg"
|
||||
const gpgHome = "/config/gnupg"
|
||||
const gpgExtension = "gpg"
|
||||
const timeFormat = "2006-01-02 at 15:04:05"
|
||||
|
||||
var (
|
||||
storage = "local"
|
||||
file = ""
|
||||
executionMode = "default"
|
||||
|
||||
storagePath = "/backup"
|
||||
workingDir = "/config"
|
||||
disableCompression = false
|
||||
encryption = false
|
||||
usingKey = false
|
||||
backupSize int64 = 0
|
||||
startTime string
|
||||
)
|
||||
|
||||
// dbHVars Required environment variables for database
|
||||
var dbHVars = []string{
|
||||
"DB_HOST",
|
||||
"DB_PORT",
|
||||
"DB_PASSWORD",
|
||||
"DB_USERNAME",
|
||||
"DB_NAME",
|
||||
@@ -40,10 +61,30 @@ var tdbRVars = []string{
|
||||
var dbConf *dbConfig
|
||||
var targetDbConf *targetDbConfig
|
||||
|
||||
// sshHVars Required environment variables for SSH remote server storage
|
||||
var sshHVars = []string{
|
||||
// sshVars Required environment variables for SSH remote server storage
|
||||
var sshVars = []string{
|
||||
"SSH_USER",
|
||||
"SSH_REMOTE_PATH",
|
||||
"SSH_HOST_NAME",
|
||||
"SSH_PORT",
|
||||
"REMOTE_PATH",
|
||||
}
|
||||
var ftpVars = []string{
|
||||
"FTP_HOST_NAME",
|
||||
"FTP_USER",
|
||||
"FTP_PASSWORD",
|
||||
"FTP_PORT",
|
||||
}
|
||||
var azureVars = []string{
|
||||
"AZURE_STORAGE_CONTAINER_NAME",
|
||||
"AZURE_STORAGE_ACCOUNT_NAME",
|
||||
"AZURE_STORAGE_ACCOUNT_KEY",
|
||||
}
|
||||
|
||||
// AwsVars Required environment variables for AWS S3 storage
|
||||
var awsVars = []string{
|
||||
"AWS_S3_ENDPOINT",
|
||||
"AWS_S3_BUCKET_NAME",
|
||||
"AWS_ACCESS_KEY",
|
||||
"AWS_SECRET_KEY",
|
||||
"AWS_REGION",
|
||||
}
|
||||
|
||||
18
templates/email-error.tmpl
Normal file
18
templates/email-error.tmpl
Normal file
@@ -0,0 +1,18 @@
|
||||
<!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>
|
||||
<li>Backup Reference: {{.BackupReference}} </li>
|
||||
</ul>
|
||||
<p>©2024 <a href="https://github.com/jkaninda/mysql-bkup">mysql-bkup</a></p>
|
||||
</body>
|
||||
</html>
|
||||
24
templates/email.tmpl
Normal file
24
templates/email.tmpl
Normal file
@@ -0,0 +1,24 @@
|
||||
<!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>
|
||||
<li>Backup Reference: {{.BackupReference}} </li>
|
||||
</ul>
|
||||
<p>Best regards,</p>
|
||||
<p>©2024 <a href="https://github.com/jkaninda/mysql-bkup">mysql-bkup</a></p>
|
||||
<href>
|
||||
</body>
|
||||
</html>
|
||||
8
templates/telegram-error.tmpl
Normal file
8
templates/telegram-error.tmpl
Normal file
@@ -0,0 +1,8 @@
|
||||
🔴 Urgent: Database Backup Failure Notification
|
||||
Hi,
|
||||
An error occurred during database backup.
|
||||
Failure Details:
|
||||
- Date: {{.EndTime}}
|
||||
- Backup Reference: {{.BackupReference}}
|
||||
- Error Message: {{.Error}}
|
||||
|
||||
12
templates/telegram.tmpl
Normal file
12
templates/telegram.tmpl
Normal file
@@ -0,0 +1,12 @@
|
||||
[✅ 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
|
||||
- Backup Reference: {{.BackupReference}}
|
||||
83
utils/config.go
Normal file
83
utils/config.go
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
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
|
||||
BackupReference string
|
||||
}
|
||||
type ErrorMessage struct {
|
||||
Database string
|
||||
EndTime string
|
||||
Error string
|
||||
BackupReference string
|
||||
}
|
||||
|
||||
// loadMailConfig gets mail environment variables and returns MailConfig
|
||||
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",
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TimeFormat returns the format of the time
|
||||
func TimeFormat() string {
|
||||
format := os.Getenv("TIME_FORMAT")
|
||||
if format == "" {
|
||||
return "2006-01-02 at 15:04:05"
|
||||
|
||||
}
|
||||
return format
|
||||
}
|
||||
|
||||
func backupReference() string {
|
||||
return os.Getenv("BACKUP_REFERENCE")
|
||||
}
|
||||
|
||||
const templatePath = "/config/templates"
|
||||
@@ -1,16 +1,35 @@
|
||||
// Package utils /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
const RestoreExample = "mysql-bkup restore --dbname database --file db_20231219_022941.sql.gz\n" +
|
||||
"bkup restore --dbname database --storage s3 --path /custom-path --file db_20231219_022941.sql.gz"
|
||||
const BackupExample = "mysql-bkup backup --dbname database --disable-compression\n" +
|
||||
"mysql-bkup backup --dbname database --storage s3 --path /custom-path --disable-compression"
|
||||
const RestoreExample = "restore --dbname database --file db_20231219_022941.sql.gz\n" +
|
||||
"restore --dbname database --storage s3 --path /custom-path --file db_20231219_022941.sql.gz"
|
||||
const BackupExample = "backup --dbname database --disable-compression\n" +
|
||||
"backup --dbname database --storage s3 --path /custom-path --disable-compression"
|
||||
|
||||
const MainExample = "mysql-bkup backup --dbname database --disable-compression\n" +
|
||||
"mysql-bkup backup --dbname database --storage s3 --path /custom-path\n" +
|
||||
"mysql-bkup restore --dbname database --file db_20231219_022941.sql.gz"
|
||||
"backup --dbname database --storage s3 --path /custom-path\n" +
|
||||
"restore --dbname database --file db_20231219_022941.sql.gz"
|
||||
const traceLog = "trace"
|
||||
|
||||
140
utils/logger.go
140
utils/logger.go
@@ -1,67 +1,103 @@
|
||||
// Package utils /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var currentTime = time.Now().Format("2006/01/02 15:04:05")
|
||||
// Info returns info log
|
||||
func Info(msg string, args ...interface{}) {
|
||||
log.SetOutput(getStd("/dev/stdout"))
|
||||
logWithCaller("INFO", msg, args...)
|
||||
|
||||
func Info(msg string, args ...any) {
|
||||
formattedMessage := fmt.Sprintf(msg, args...)
|
||||
if len(args) == 0 {
|
||||
fmt.Printf("%s INFO: %s\n", currentTime, msg)
|
||||
} else {
|
||||
fmt.Printf("%s INFO: %s\n", currentTime, formattedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// Warn warning message
|
||||
func Warn(msg string, args ...any) {
|
||||
formattedMessage := fmt.Sprintf(msg, args...)
|
||||
if len(args) == 0 {
|
||||
fmt.Printf("%s WARN: %s\n", currentTime, msg)
|
||||
} else {
|
||||
fmt.Printf("%s WARN: %s\n", currentTime, formattedMessage)
|
||||
}
|
||||
}
|
||||
func Error(msg string, args ...any) {
|
||||
formattedMessage := fmt.Sprintf(msg, args...)
|
||||
if len(args) == 0 {
|
||||
fmt.Printf("%s ERROR: %s\n", currentTime, msg)
|
||||
} else {
|
||||
fmt.Printf("%s ERROR: %s\n", currentTime, formattedMessage)
|
||||
}
|
||||
}
|
||||
func Done(msg string, args ...any) {
|
||||
formattedMessage := fmt.Sprintf(msg, args...)
|
||||
if len(args) == 0 {
|
||||
fmt.Printf("%s INFO: %s\n", currentTime, msg)
|
||||
} else {
|
||||
fmt.Printf("%s INFO: %s\n", currentTime, formattedMessage)
|
||||
}
|
||||
// Warn returns warning log
|
||||
func Warn(msg string, args ...interface{}) {
|
||||
log.SetOutput(getStd("/dev/stdout"))
|
||||
logWithCaller("WARN", msg, args...)
|
||||
|
||||
}
|
||||
|
||||
// Fatal logs an error message and exits the program
|
||||
func Fatal(msg string, args ...any) {
|
||||
// Fatal logs an error message and exits the program.
|
||||
formattedMessage := fmt.Sprintf(msg, args...)
|
||||
if len(args) == 0 {
|
||||
fmt.Printf("%s ERROR: %s\n", currentTime, msg)
|
||||
NotifyError(msg)
|
||||
} else {
|
||||
fmt.Printf("%s ERROR: %s\n", currentTime, formattedMessage)
|
||||
// Error logs error messages
|
||||
func Error(msg string, args ...interface{}) {
|
||||
log.SetOutput(getStd("/dev/stderr"))
|
||||
logWithCaller("ERROR", msg, args...)
|
||||
}
|
||||
|
||||
func Fatal(msg string, args ...interface{}) {
|
||||
log.SetOutput(os.Stdout)
|
||||
// Format message if there are additional arguments
|
||||
formattedMessage := msg
|
||||
if len(args) > 0 {
|
||||
formattedMessage = fmt.Sprintf(msg, args...)
|
||||
}
|
||||
logWithCaller("ERROR", msg, args...)
|
||||
NotifyError(formattedMessage)
|
||||
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
os.Kill.Signal()
|
||||
}
|
||||
|
||||
// Helper function to format and log messages with file and line number
|
||||
func logWithCaller(level, msg string, args ...interface{}) {
|
||||
// Format message if there are additional arguments
|
||||
formattedMessage := msg
|
||||
if len(args) > 0 {
|
||||
formattedMessage = fmt.Sprintf(msg, args...)
|
||||
}
|
||||
|
||||
// Get the caller's file and line number (skip 2 frames)
|
||||
_, file, line, ok := runtime.Caller(2)
|
||||
if !ok {
|
||||
file = "unknown"
|
||||
line = 0
|
||||
}
|
||||
// Log message with caller information if GOMA_LOG_LEVEL is trace
|
||||
if strings.ToLower(level) != "off" {
|
||||
if strings.ToLower(level) == traceLog {
|
||||
log.Printf("%s: %s (File: %s, Line: %d)\n", level, formattedMessage, file, line)
|
||||
} else {
|
||||
log.Printf("%s: %s\n", level, formattedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getStd(out string) *os.File {
|
||||
switch out {
|
||||
case "/dev/stdout":
|
||||
return os.Stdout
|
||||
case "/dev/stderr":
|
||||
return os.Stderr
|
||||
case "/dev/stdin":
|
||||
return os.Stdin
|
||||
default:
|
||||
return os.Stdout
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
202
utils/notification.go
Normal file
202
utils/notification.go
Normal file
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/go-mail/mail"
|
||||
"html/template"
|
||||
"io"
|
||||
"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) error {
|
||||
Info("Start sending email notification....")
|
||||
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 {
|
||||
Error("Error could not send email : %v", err)
|
||||
return err
|
||||
}
|
||||
Info("Email notification has been sent")
|
||||
return nil
|
||||
|
||||
}
|
||||
func sendMessage(msg string) error {
|
||||
|
||||
Info("Sending Telegram 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 {
|
||||
return err
|
||||
}
|
||||
code := response.StatusCode
|
||||
if code == 200 {
|
||||
Info("Telegram notification has been sent")
|
||||
return nil
|
||||
} else {
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
Error("Error could not send message, error: %s", string(body))
|
||||
return fmt.Errorf("error could not send message %s", string(body))
|
||||
}
|
||||
|
||||
}
|
||||
func NotifySuccess(notificationData *NotificationData) {
|
||||
notificationData.BackupReference = backupReference()
|
||||
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.tmpl")
|
||||
if err != nil {
|
||||
Error("Could not parse email template: %v", err)
|
||||
}
|
||||
err = SendEmail(fmt.Sprintf("✅ Database Backup Notification – %s", notificationData.Database), body)
|
||||
if err != nil {
|
||||
Error("Could not send email: %v", err)
|
||||
}
|
||||
}
|
||||
// Telegram notification
|
||||
err = CheckEnvVars(vars)
|
||||
if err == nil {
|
||||
message, err := parseTemplate(*notificationData, "telegram.tmpl")
|
||||
if err != nil {
|
||||
Error("Could not parse telegram template: %v", err)
|
||||
}
|
||||
|
||||
err = sendMessage(message)
|
||||
if err != nil {
|
||||
Error("Could not send Telegram message: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
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(TimeFormat()),
|
||||
BackupReference: os.Getenv("BACKUP_REFERENCE"),
|
||||
}, "email-error.tmpl")
|
||||
if err != nil {
|
||||
Error("Could not parse error template: %v", err)
|
||||
}
|
||||
err = SendEmail("🔴 Urgent: Database Backup Failure Notification", body)
|
||||
if err != nil {
|
||||
Error("Could not send email: %v", err)
|
||||
}
|
||||
}
|
||||
// Telegram notification
|
||||
err = CheckEnvVars(vars)
|
||||
if err == nil {
|
||||
message, err := parseTemplate(ErrorMessage{
|
||||
Error: error,
|
||||
EndTime: time.Now().Format(TimeFormat()),
|
||||
BackupReference: os.Getenv("BACKUP_REFERENCE"),
|
||||
}, "telegram-error.tmpl")
|
||||
if err != nil {
|
||||
Error("Could not parse error template: %v", err)
|
||||
|
||||
}
|
||||
|
||||
err = sendMessage(message)
|
||||
if err != nil {
|
||||
Error("Could not send telegram message: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getTgUrl() string {
|
||||
return fmt.Sprintf("https://api.telegram.org/bot%s", os.Getenv("TG_TOKEN"))
|
||||
|
||||
}
|
||||
175
utils/s3.go
175
utils/s3.go
@@ -1,175 +0,0 @@
|
||||
// Package utils /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CreateSession creates a new AWS session
|
||||
func CreateSession() (*session.Session, error) {
|
||||
// AwsVars Required environment variables for AWS S3 storage
|
||||
var awsVars = []string{
|
||||
"AWS_S3_ENDPOINT",
|
||||
"AWS_S3_BUCKET_NAME",
|
||||
"AWS_ACCESS_KEY",
|
||||
"AWS_SECRET_KEY",
|
||||
"AWS_REGION",
|
||||
"AWS_REGION",
|
||||
"AWS_REGION",
|
||||
}
|
||||
|
||||
endPoint := GetEnvVariable("AWS_S3_ENDPOINT", "S3_ENDPOINT")
|
||||
accessKey := GetEnvVariable("AWS_ACCESS_KEY", "ACCESS_KEY")
|
||||
secretKey := GetEnvVariable("AWS_SECRET_KEY", "SECRET_KEY")
|
||||
_ = GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
|
||||
|
||||
region := os.Getenv("AWS_REGION")
|
||||
awsDisableSsl, err := strconv.ParseBool(os.Getenv("AWS_DISABLE_SSL"))
|
||||
if err != nil {
|
||||
Fatal("Unable to parse AWS_DISABLE_SSL env var: %s", err)
|
||||
}
|
||||
|
||||
err = CheckEnvVars(awsVars)
|
||||
if err != nil {
|
||||
Fatal("Error checking environment variables\n: %s", err)
|
||||
}
|
||||
// S3 Config
|
||||
s3Config := &aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
|
||||
Endpoint: aws.String(endPoint),
|
||||
Region: aws.String(region),
|
||||
DisableSSL: aws.Bool(awsDisableSsl),
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
}
|
||||
return session.NewSession(s3Config)
|
||||
|
||||
}
|
||||
|
||||
// UploadFileToS3 uploads a file to S3 with a given prefix
|
||||
func UploadFileToS3(filePath, key, bucket, prefix string) error {
|
||||
sess, err := CreateSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc := s3.New(sess)
|
||||
|
||||
file, err := os.Open(filepath.Join(filePath, key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objectKey := filepath.Join(prefix, key)
|
||||
|
||||
buffer := make([]byte, fileInfo.Size())
|
||||
file.Read(buffer)
|
||||
fileBytes := bytes.NewReader(buffer)
|
||||
fileType := http.DetectContentType(buffer)
|
||||
|
||||
_, err = svc.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(objectKey),
|
||||
Body: fileBytes,
|
||||
ContentLength: aws.Int64(fileInfo.Size()),
|
||||
ContentType: aws.String(fileType),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func DownloadFile(destinationPath, key, bucket, prefix string) error {
|
||||
|
||||
sess, err := CreateSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Info("Download backup from S3 storage...")
|
||||
file, err := os.Create(filepath.Join(destinationPath, key))
|
||||
if err != nil {
|
||||
fmt.Println("Failed to create file", err)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
objectKey := filepath.Join(prefix, key)
|
||||
|
||||
downloader := s3manager.NewDownloader(sess)
|
||||
numBytes, err := downloader.Download(file,
|
||||
&s3.GetObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("Failed to download file", err)
|
||||
return err
|
||||
}
|
||||
Info("Backup downloaded: %s bytes size %s ", file.Name(), numBytes)
|
||||
|
||||
return nil
|
||||
}
|
||||
func DeleteOldBackup(bucket, prefix string, retention int) error {
|
||||
sess, err := CreateSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc := s3.New(sess)
|
||||
|
||||
// Get the current time and the time threshold for 7 days ago
|
||||
now := time.Now()
|
||||
backupRetentionDays := now.AddDate(0, 0, -retention)
|
||||
|
||||
// List objects in the bucket
|
||||
listObjectsInput := &s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucket),
|
||||
Prefix: aws.String(prefix),
|
||||
}
|
||||
err = svc.ListObjectsV2Pages(listObjectsInput, func(page *s3.ListObjectsV2Output, lastPage bool) bool {
|
||||
for _, object := range page.Contents {
|
||||
if object.LastModified.Before(backupRetentionDays) {
|
||||
// Object is older than retention days, delete it
|
||||
_, err := svc.DeleteObject(&s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: object.Key,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete object %s: %v", *object.Key, err)
|
||||
} else {
|
||||
fmt.Printf("Deleted object %s\n", *object.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return !lastPage
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to list objects: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("Finished deleting old files.")
|
||||
return nil
|
||||
}
|
||||
158
utils/utils.go
158
utils/utils.go
@@ -1,24 +1,43 @@
|
||||
// Package utils /
|
||||
/*****
|
||||
@author Jonas Kaninda
|
||||
@license MIT License <https://opensource.org/licenses/MIT>
|
||||
@Copyright © 2024 Jonas Kaninda
|
||||
**/
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Kaninda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
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"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Version = ""
|
||||
|
||||
// FileExists checks if the file does exist
|
||||
func FileExists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
@@ -32,7 +51,13 @@ func WriteToFile(filePath, content string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
}
|
||||
}(file)
|
||||
|
||||
_, err = file.WriteString(content)
|
||||
return err
|
||||
@@ -50,14 +75,25 @@ func CopyFile(src, dst string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source file: %v", err)
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
defer func(sourceFile *os.File) {
|
||||
err := sourceFile.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(sourceFile)
|
||||
|
||||
// Create the destination file
|
||||
destinationFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create destination file: %v", err)
|
||||
}
|
||||
defer destinationFile.Close()
|
||||
defer func(destinationFile *os.File) {
|
||||
err := destinationFile.Close()
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
}
|
||||
}(destinationFile)
|
||||
|
||||
// Copy the content from source to destination
|
||||
_, err = io.Copy(destinationFile, sourceFile)
|
||||
@@ -84,7 +120,12 @@ func IsDirEmpty(name string) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer f.Close()
|
||||
defer func(f *os.File) {
|
||||
err := f.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(f)
|
||||
|
||||
_, err = f.Readdirnames(1)
|
||||
if err == nil {
|
||||
@@ -133,13 +174,10 @@ func GetEnvVariable(envName, oldEnvName string) string {
|
||||
return value
|
||||
}
|
||||
Warn("%s is deprecated, please use %s instead! ", oldEnvName, envName)
|
||||
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
func ShowHistory() {
|
||||
}
|
||||
|
||||
// CheckEnvVars checks if all the specified environment variables are set
|
||||
func CheckEnvVars(vars []string) error {
|
||||
@@ -186,67 +224,33 @@ 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))
|
||||
func EnvWithDefault(envName string, defaultValue string) string {
|
||||
value := os.Getenv(envName)
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// IsValidCronExpression verify cronExpression and returns boolean
|
||||
func IsValidCronExpression(cronExpr string) bool {
|
||||
// Parse the cron expression
|
||||
_, err := cron.ParseStandard(cronExpr)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// CronNextTime returns cronExpression next time
|
||||
func CronNextTime(cronExpr string) time.Time {
|
||||
// Parse the cron expression
|
||||
schedule, err := cron.ParseStandard(cronExpr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
Error("Error parsing cron expression: %s", err)
|
||||
return time.Time{}
|
||||
}
|
||||
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"))
|
||||
|
||||
// Get the current time
|
||||
now := time.Now()
|
||||
// Get the next scheduled time
|
||||
next := schedule.Next(now)
|
||||
return next
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user