mirror of
https://github.com/jkaninda/mysql-bkup.git
synced 2025-12-06 13:39:41 +01:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0df14f37b4 | |||
| 1b60ca6fd2 | |||
| d880f40108 | |||
|
|
c845b36797 | ||
| 63d615f838 | |||
| 6f31d35df2 | |||
| f36d01cc96 | |||
| 07b7f54a75 | |||
| 7ff9a32f08 | |||
| 95a81cb6b7 | |||
| 057d5277b0 | |||
| 8e58d7a4c3 | |||
| 4bd7d9fa72 | |||
|
|
156f22f1e5 | ||
| 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 |
12
.env.example
12
.env.example
@@ -15,6 +15,7 @@ TZ=Europe/Paris
|
|||||||
|
|
||||||
### Backup restoration
|
### Backup restoration
|
||||||
#FILE_NAME=
|
#FILE_NAME=
|
||||||
|
|
||||||
### AWS S3 Storage
|
### AWS S3 Storage
|
||||||
#ACCESS_KEY=
|
#ACCESS_KEY=
|
||||||
#SECRET_KEY=
|
#SECRET_KEY=
|
||||||
@@ -43,19 +44,30 @@ TZ=Europe/Paris
|
|||||||
#FTP_USER=
|
#FTP_USER=
|
||||||
#FTP_PORT=21
|
#FTP_PORT=21
|
||||||
#REMOTE_PATH=
|
#REMOTE_PATH=
|
||||||
|
|
||||||
|
## Azure Blob storage
|
||||||
|
AZURE_STORAGE_CONTAINER_NAME=
|
||||||
|
AZURE_STORAGE_ACCOUNT_NAME=
|
||||||
|
AZURE_STORAGE_ACCOUNT_KEY=
|
||||||
|
|
||||||
#### Backup encryption
|
#### Backup encryption
|
||||||
#GPG_PUBLIC_KEY=/config/public_key.asc
|
#GPG_PUBLIC_KEY=/config/public_key.asc
|
||||||
#GPG_PRIVATE_KEY=/config/private_key.asc
|
#GPG_PRIVATE_KEY=/config/private_key.asc
|
||||||
#GPG_PASSPHRASE=Your strong passphrase
|
#GPG_PASSPHRASE=Your strong passphrase
|
||||||
|
|
||||||
## For multiple database backup on Docker or Docker in Swarm mode
|
## For multiple database backup on Docker or Docker in Swarm mode
|
||||||
#BACKUP_CONFIG_FILE=/config/config.yaml
|
#BACKUP_CONFIG_FILE=/config/config.yaml
|
||||||
|
|
||||||
### Database restoration
|
### Database restoration
|
||||||
#FILE_NAME=
|
#FILE_NAME=
|
||||||
|
|
||||||
### Notification
|
### Notification
|
||||||
#BACKUP_REFERENCE=K8s/Paris cluster
|
#BACKUP_REFERENCE=K8s/Paris cluster
|
||||||
|
|
||||||
## Telegram
|
## Telegram
|
||||||
#TG_TOKEN=
|
#TG_TOKEN=
|
||||||
#TG_CHAT_ID=
|
#TG_CHAT_ID=
|
||||||
|
|
||||||
### Email
|
### Email
|
||||||
#MAIL_HOST=
|
#MAIL_HOST=
|
||||||
#MAIL_PORT=
|
#MAIL_PORT=
|
||||||
|
|||||||
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
|
||||||
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
|
||||||
23
Dockerfile
23
Dockerfile
@@ -1,5 +1,6 @@
|
|||||||
FROM golang:1.22.5 AS build
|
FROM golang:1.23.4 AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
ARG appVersion=""
|
||||||
|
|
||||||
# Copy the source code.
|
# Copy the source code.
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -7,9 +8,9 @@ COPY . .
|
|||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/mysql-bkup
|
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.20.3
|
FROM alpine:3.21.0
|
||||||
ENV TZ=UTC
|
ENV TZ=UTC
|
||||||
ARG WORKDIR="/config"
|
ARG WORKDIR="/config"
|
||||||
ARG BACKUPDIR="/backup"
|
ARG BACKUPDIR="/backup"
|
||||||
@@ -22,20 +23,12 @@ LABEL version=${appVersion}
|
|||||||
LABEL github="github.com/jkaninda/mysql-bkup"
|
LABEL github="github.com/jkaninda/mysql-bkup"
|
||||||
|
|
||||||
RUN apk --update add --no-cache mysql-client mariadb-connector-c tzdata ca-certificates
|
RUN apk --update add --no-cache mysql-client mariadb-connector-c tzdata ca-certificates
|
||||||
RUN mkdir $WORKDIR
|
RUN mkdir -p $WORKDIR $BACKUPDIR $TEMPLATES_DIR $BACKUP_TMP_DIR && \
|
||||||
RUN mkdir $BACKUPDIR
|
chmod a+rw $WORKDIR $BACKUPDIR $BACKUP_TMP_DIR
|
||||||
RUN mkdir $TEMPLATES_DIR
|
|
||||||
RUN mkdir -p $BACKUP_TMP_DIR
|
|
||||||
RUN chmod 777 $WORKDIR
|
|
||||||
RUN chmod 777 $BACKUPDIR
|
|
||||||
RUN chmod 777 $BACKUP_TMP_DIR
|
|
||||||
RUN chmod 777 $WORKDIR
|
|
||||||
|
|
||||||
COPY --from=build /app/mysql-bkup /usr/local/bin/mysql-bkup
|
COPY --from=build /app/mysql-bkup /usr/local/bin/mysql-bkup
|
||||||
COPY ./templates/* $TEMPLATES_DIR/
|
COPY ./templates/* $TEMPLATES_DIR/
|
||||||
RUN chmod +x /usr/local/bin/mysql-bkup
|
RUN chmod +x /usr/local/bin/mysql-bkup && \
|
||||||
|
ln -s /usr/local/bin/mysql-bkup /usr/local/bin/bkup
|
||||||
RUN ln -s /usr/local/bin/mysql-bkup /usr/local/bin/bkup
|
|
||||||
|
|
||||||
# Create backup script and make it executable
|
# Create backup script and make it executable
|
||||||
RUN printf '#!/bin/sh\n/usr/local/bin/mysql-bkup backup "$@"' > /usr/local/bin/backup && \
|
RUN printf '#!/bin/sh\n/usr/local/bin/mysql-bkup backup "$@"' > /usr/local/bin/backup && \
|
||||||
|
|||||||
59
README.md
59
README.md
@@ -1,14 +1,7 @@
|
|||||||
# MySQL Backup
|
# MYSQL-BKUP
|
||||||
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, FTP and SSH compatible storage.
|
|
||||||
It also supports __encrypting__ your backups using GPG.
|
|
||||||
|
|
||||||
The [jkaninda/mysql-bkup](https://hub.docker.com/r/jkaninda/mysql-bkup) Docker image can be deployed on Docker, Docker Swarm and Kubernetes.
|
|
||||||
It handles __recurring__ backups of MySQL or MariaDB database on Docker and can be deployed as __CronJob on Kubernetes__ using local, AWS S3, FTP or SSH compatible storage.
|
|
||||||
|
|
||||||
It also supports database __encryption__ using GPG.
|
|
||||||
|
|
||||||
Telegram and Email notifications on successful and failed backups.
|
|
||||||
|
|
||||||
|
**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://github.com/jkaninda/mysql-bkup/actions/workflows/release.yml)
|
||||||
[](https://goreportcard.com/report/github.com/jkaninda/mysql-bkup)
|
[](https://goreportcard.com/report/github.com/jkaninda/mysql-bkup)
|
||||||
@@ -16,6 +9,37 @@ Telegram and Email notifications on successful and failed backups.
|
|||||||

|

|
||||||
<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>
|
<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:
|
Successfully tested on:
|
||||||
- Docker
|
- Docker
|
||||||
- Docker in Swarm mode
|
- Docker in Swarm mode
|
||||||
@@ -37,8 +61,9 @@ Successfully tested on:
|
|||||||
## Storage:
|
## Storage:
|
||||||
- Local
|
- Local
|
||||||
- AWS S3 or any S3 Alternatives for Object Storage
|
- AWS S3 or any S3 Alternatives for Object Storage
|
||||||
- SSH remote server
|
- SSH remote storage server
|
||||||
|
- FTP remote storage server
|
||||||
|
- Azure Blob storage
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
### Simple backup using Docker CLI
|
### Simple backup using Docker CLI
|
||||||
@@ -86,11 +111,10 @@ services:
|
|||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
# mysql-bkup container must be connected to the same network with your database
|
||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
networks:
|
networks:
|
||||||
web:
|
web:
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker recurring backup
|
### Docker recurring backup
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@@ -162,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.
|
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
|
## References
|
||||||
|
|
||||||
We decided to publish this image as a simpler and more lightweight alternative because of the following requirements:
|
We decided to publish this image as a simpler and more lightweight alternative because of the following requirements:
|
||||||
|
|
||||||
- The original image is based on `alpine` 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.
|
- This image is written in Go.
|
||||||
- `arm64` and `arm/v7` architectures are supported.
|
- `arm64` and `arm/v7` architectures are supported.
|
||||||
- Docker in Swarm mode is supported.
|
- Docker in Swarm mode is supported.
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
// Package cmd /
|
/*
|
||||||
/*****
|
MIT License
|
||||||
@author Jonas Kaninda
|
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
Copyright (c) 2023 Jonas Kaninda
|
||||||
@Copyright © 2024 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
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -27,7 +45,7 @@ var BackupCmd = &cobra.Command{
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
//Backup
|
//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("path", "P", "", "AWS S3 path without file name. eg: /custom_path or ssh remote path `/home/foo/backup`")
|
||||||
BackupCmd.PersistentFlags().StringP("cron-expression", "", "", "Backup cron expression")
|
BackupCmd.PersistentFlags().StringP("cron-expression", "", "", "Backup cron expression")
|
||||||
BackupCmd.PersistentFlags().BoolP("disable-compression", "", false, "Disable backup compression")
|
BackupCmd.PersistentFlags().BoolP("disable-compression", "", false, "Disable backup compression")
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
// Package cmd /
|
/*
|
||||||
/*****
|
MIT License
|
||||||
@author Jonas Kaninda
|
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
Copyright (c) 2023 Jonas Kaninda
|
||||||
@Copyright © 2024 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
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -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
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -24,7 +48,7 @@ var RestoreCmd = &cobra.Command{
|
|||||||
func init() {
|
func init() {
|
||||||
//Restore
|
//Restore
|
||||||
RestoreCmd.PersistentFlags().StringP("file", "f", "", "File name of database")
|
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`")
|
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 /
|
/*
|
||||||
/*****
|
MIT License
|
||||||
@author Jonas Kaninda
|
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
Copyright (c) 2023 Jonas Kaninda
|
||||||
@Copyright © 2024 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
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,13 +1,32 @@
|
|||||||
// Package cmd /
|
/*
|
||||||
/*****
|
MIT License
|
||||||
@author Jonas Kaninda
|
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
Copyright (c) 2023 Jonas Kaninda
|
||||||
@Copyright © 2024 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
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/jkaninda/mysql-bkup/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
@@ -23,6 +42,6 @@ var VersionCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Version() {
|
func Version() {
|
||||||
fmt.Printf("Version: %s \n", appVersion)
|
fmt.Printf("Version: %s \n", utils.Version)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|||||||
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:
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ services:
|
|||||||
- AWS_SECRET_KEY=xxxxx
|
- 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
|
## 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_DISABLE_SSL="false"
|
||||||
- AWS_FORCE_PATH_STYLE="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
|
# mysql-bkup container must be connected to the same network with your database
|
||||||
networks:
|
networks:
|
||||||
@@ -78,6 +78,7 @@ services:
|
|||||||
#- BACKUP_RETENTION_DAYS=7
|
#- 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
|
## 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_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
|
# mysql-bkup container must be connected to the same network with your database
|
||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
|
|||||||
@@ -74,10 +74,10 @@ networks:
|
|||||||
The title and body of the notifications can be tailored to your needs using Go templates.
|
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:
|
Template sources must be mounted inside the container in /config/templates:
|
||||||
|
|
||||||
- email.template: Email notification template
|
- email.tmpl: Email notification template
|
||||||
- telegram.template: Telegram notification template
|
- telegram.tmpl: Telegram notification template
|
||||||
- email-error.template: Error notification template
|
- email-error.tmpl: Error notification template
|
||||||
- telegram-error.template: Error notification template
|
- telegram-error.tmpl: Error notification template
|
||||||
|
|
||||||
### Data
|
### Data
|
||||||
|
|
||||||
|
|||||||
@@ -6,23 +6,40 @@ nav_order: 1
|
|||||||
|
|
||||||
# About mysql-bkup
|
# About mysql-bkup
|
||||||
{:.no_toc}
|
{:.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, FTP and SSH remote storage.
|
|
||||||
It also supports __encrypting__ your backups using GPG.
|
|
||||||
|
|
||||||
Telegram and Email notifications on successful and failed backups.
|
**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
|
||||||
|
|
||||||
We are open to receiving stars, PRs, and issues!
|
- **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.
|
||||||
|
|
||||||
{: .fs-6 .fw-300 }
|
- **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**
|
||||||
|
|
||||||
The [jkaninda/mysql-bkup](https://hub.docker.com/r/jkaninda/mysql-bkup) Docker image can be deployed on Docker, Docker Swarm and Kubernetes.
|
## Use Cases
|
||||||
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.
|
|
||||||
|
- **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 }
|
{: .note }
|
||||||
|
|||||||
@@ -32,44 +32,46 @@ Backup, restore and migrate targets, schedule and retention are configured using
|
|||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
|
|
||||||
| Name | Requirement | Description |
|
| Name | Requirement | Description |
|
||||||
|------------------------|---------------------------------------------------------------|-----------------------------------------------------------------|
|
|------------------------------|---------------------------------------------------------------|-----------------------------------------------------------------|
|
||||||
| DB_PORT | Optional, default 3306 | Database port number |
|
| DB_PORT | Optional, default 3306 | Database port number |
|
||||||
| DB_HOST | Required | Database host |
|
| DB_HOST | Required | Database host |
|
||||||
| DB_NAME | Optional if it was provided from the -d flag | Database name |
|
| DB_NAME | Optional if it was provided from the -d flag | Database name |
|
||||||
| DB_USERNAME | Required | Database user name |
|
| DB_USERNAME | Required | Database user name |
|
||||||
| DB_PASSWORD | Required | Database password |
|
| DB_PASSWORD | Required | Database password |
|
||||||
| AWS_ACCESS_KEY | Optional, required for S3 storage | AWS S3 Access Key |
|
| AWS_ACCESS_KEY | Optional, required for S3 storage | AWS S3 Access Key |
|
||||||
| AWS_SECRET_KEY | Optional, required for S3 storage | AWS S3 Secret Key |
|
| AWS_SECRET_KEY | Optional, required for S3 storage | AWS S3 Secret Key |
|
||||||
| AWS_BUCKET_NAME | Optional, required for S3 storage | AWS S3 Bucket Name |
|
| AWS_BUCKET_NAME | Optional, required for S3 storage | AWS S3 Bucket Name |
|
||||||
| AWS_BUCKET_NAME | Optional, required for S3 storage | AWS S3 Bucket Name |
|
| AWS_BUCKET_NAME | Optional, required for S3 storage | AWS S3 Bucket Name |
|
||||||
| AWS_REGION | Optional, required for S3 storage | AWS Region |
|
| AWS_REGION | Optional, required for S3 storage | AWS Region |
|
||||||
| AWS_DISABLE_SSL | Optional, required for S3 storage | Disable SSL |
|
| AWS_DISABLE_SSL | Optional, required for S3 storage | Disable SSL |
|
||||||
| AWS_FORCE_PATH_STYLE | Optional, required for S3 storage | Force path style |
|
| 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) |
|
| FILE_NAME | Optional if it was provided from the --file flag | Database file to restore (extensions: .sql, .sql.gz) |
|
||||||
| GPG_PASSPHRASE | Optional, required to encrypt and restore backup | GPG passphrase |
|
| GPG_PASSPHRASE | Optional, required to encrypt and restore backup | GPG passphrase |
|
||||||
| GPG_PUBLIC_KEY | Optional, required to encrypt backup | GPG public key, used to encrypt backup (/config/public_key.asc) |
|
| 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_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 |
|
| 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_HOST | Optional, required for SSH storage | ssh remote hostname or ip |
|
||||||
| SSH_USER | Optional, required for SSH storage | ssh remote user |
|
| SSH_USER | Optional, required for SSH storage | ssh remote user |
|
||||||
| SSH_PASSWORD | Optional, required for SSH storage | ssh remote user's password |
|
| 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_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_PORT | Optional, required for SSH storage | ssh remote server port |
|
||||||
| REMOTE_PATH | Optional, required for SSH or FTP storage | 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_HOST | Optional, required for FTP storage | FTP host name |
|
||||||
| FTP_PORT | Optional, required for FTP storage | FTP server port number |
|
| FTP_PORT | Optional, required for FTP storage | FTP server port number |
|
||||||
| FTP_USER | Optional, required for FTP storage | FTP user |
|
| FTP_USER | Optional, required for FTP storage | FTP user |
|
||||||
| FTP_PASSWORD | Optional, required for FTP storage | FTP user password |
|
| FTP_PASSWORD | Optional, required for FTP storage | FTP user password |
|
||||||
| TARGET_DB_HOST | Optional, required for database migration | Target database host |
|
| TARGET_DB_HOST | Optional, required for database migration | Target database host |
|
||||||
| TARGET_DB_PORT | Optional, required for database migration | Target database port |
|
| TARGET_DB_PORT | Optional, required for database migration | Target database port |
|
||||||
| TARGET_DB_NAME | Optional, required for database migration | Target database name |
|
| TARGET_DB_NAME | Optional, required for database migration | Target database name |
|
||||||
| TARGET_DB_USERNAME | Optional, required for database migration | Target database username |
|
| TARGET_DB_USERNAME | Optional, required for database migration | Target database username |
|
||||||
| TARGET_DB_PASSWORD | Optional, required for database migration | Target database password |
|
| TARGET_DB_PASSWORD | Optional, required for database migration | Target database password |
|
||||||
| TG_TOKEN | Optional, required for Telegram notification | Telegram token (`BOT-ID:BOT-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 |
|
| TG_CHAT_ID | Optional, required for Telegram notification | Telegram Chat ID |
|
||||||
| TZ | Optional | Time Zone |
|
| TZ | Optional | Time Zone |
|
||||||
|
| AZURE_STORAGE_CONTAINER_NAME | Optional, required for Azure Blob Storage storage | Azure storage container name |
|
||||||
|
| AZURE_STORAGE_ACCOUNT_NAME | Optional, required for Azure Blob Storage storage | Azure storage account name |
|
||||||
|
| AZURE_STORAGE_ACCOUNT_KEY | Optional, required for Azure Blob Storage storage | Azure storage account key |
|
||||||
---
|
---
|
||||||
## Run in Scheduled mode
|
## Run in Scheduled mode
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ services:
|
|||||||
- AWS_SECRET_KEY=xxxxx
|
- 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
|
## 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_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
|
# mysql-bkup container must be connected to the same network with your database
|
||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ services:
|
|||||||
- AWS_SECRET_KEY=xxxxx
|
- 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
|
## 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_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
|
# See: https://jkaninda.github.io/mysql-bkup/reference/#predefined-schedules
|
||||||
- BACKUP_CRON_EXPRESSION=@daily #@every 5m|@weekly | @monthly |0 1 * * *
|
- BACKUP_CRON_EXPRESSION=@daily #@every 5m|@weekly | @monthly |0 1 * * *
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
# mysql-bkup container must be connected to the same network with your database
|
||||||
|
|||||||
@@ -45,5 +45,5 @@ spec:
|
|||||||
- name: AWS_DISABLE_SSL
|
- name: AWS_DISABLE_SSL
|
||||||
value: "false"
|
value: "false"
|
||||||
- name: AWS_FORCE_PATH_STYLE
|
- name: AWS_FORCE_PATH_STYLE
|
||||||
value: "false"
|
value: "true"
|
||||||
restartPolicy: Never
|
restartPolicy: Never
|
||||||
37
go.mod
37
go.mod
@@ -1,35 +1,38 @@
|
|||||||
module github.com/jkaninda/mysql-bkup
|
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 (
|
require (
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.5
|
github.com/go-mail/mail v2.3.1+incompatible
|
||||||
github.com/aws/aws-sdk-go v1.55.3
|
github.com/jkaninda/encryptor v0.0.0-20241111100652-926393c9437e
|
||||||
github.com/bramvdbogaerde/go-scp v1.5.0
|
github.com/jkaninda/go-storage v0.1.3
|
||||||
github.com/hpcloud/tail v1.0.0
|
|
||||||
github.com/jlaffaye/ftp v0.2.0
|
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.1
|
||||||
golang.org/x/crypto v0.18.0
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
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 v1.1.0 // indirect
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||||
github.com/cloudflare/circl v1.3.3 // indirect
|
github.com/ProtonMail/gopenpgp/v2 v2.8.0 // indirect
|
||||||
github.com/go-mail/mail v2.3.1+incompatible // 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.7 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jkaninda/encryptor v0.0.0-20241013064832-ed4bd6a1b221 // indirect
|
github.com/jlaffaye/ftp v0.2.0 // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/crypto v0.28.0 // indirect
|
||||||
golang.org/x/text v0.14.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/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
gopkg.in/mail.v2 v2.3.1 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
96
go.sum
96
go.sum
@@ -1,71 +1,91 @@
|
|||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
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/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI=
|
||||||
|
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/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
|
||||||
|
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/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
|
github.com/ProtonMail/go-crypto v1.1.0 h1:OnlSGxXflfrWJESDsGQOmACNQRM9IflG3q8XTrOqvbE=
|
||||||
|
github.com/ProtonMail/go-crypto v1.1.0/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
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/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.8.0 h1:WvMv3CMcFsqKSM4/Qf8sf3tgyQkzDqQmoSE49bnBuP4=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.5/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=
|
github.com/ProtonMail/gopenpgp/v2 v2.8.0/go.mod h1:qb2GUSnmA9ipBW5GVtCtEhkummSlqs2A8Ar3S0HBgSY=
|
||||||
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/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
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/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 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM=
|
||||||
github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ=
|
github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ=
|
||||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX8AJBAxXExM=
|
github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX8AJBAxXExM=
|
||||||
github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M=
|
github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
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/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
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-20241111100652-926393c9437e h1:jtFKZHt/PLGQWXNgjEFTEwVbxiQQRMoJ7m37trbkJGw=
|
||||||
github.com/jkaninda/encryptor v0.0.0-20241013064832-ed4bd6a1b221/go.mod h1:9F8ZJ+ZXE8DZBo77+aneGj8LMjrYXX6eFUCC/uqZOUo=
|
github.com/jkaninda/encryptor v0.0.0-20241111100652-926393c9437e/go.mod h1:Y1EXpPWQ9PNd7y7E6ez3xgnzZc8fuDWXwX/1/dXNCE4=
|
||||||
|
github.com/jkaninda/go-storage v0.1.3 h1:lEpHVgFLKSvjsi/6tAek96Y07za3vxmsXF2/+jiCMZU=
|
||||||
|
github.com/jkaninda/go-storage v0.1.3/go.mod h1:zVRnLprBk/9AUz2+za6Y03MgoNYrqKLy3edVtjqMaps=
|
||||||
github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
|
github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
|
||||||
github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
|
github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
|
||||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
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/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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/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 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
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 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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-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.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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||||
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
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/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-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-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.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.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-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.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/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -74,25 +94,21 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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-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.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.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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.22.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-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.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.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.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.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.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.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.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
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-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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
@@ -101,11 +117,11 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
|||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
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/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 /
|
/*
|
||||||
/*****
|
MIT License
|
||||||
@author Jonas Kaninda
|
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
Copyright (c) 2023 Jonas Kaninda
|
||||||
@Copyright © 2024 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
|
package main
|
||||||
|
|
||||||
import "github.com/jkaninda/mysql-bkup/cmd"
|
import "github.com/jkaninda/mysql-bkup/cmd"
|
||||||
|
|||||||
123
pkg/azure.go
Normal file
123
pkg/azure.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
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 Azure Blob 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 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("Backup name is %s", finalFileName)
|
||||||
|
utils.Info("Backup size: %s", utils.ConvertBytes(uint64(backupSize)))
|
||||||
|
utils.Info("Uploading backup archive to Azure Blob storage ... done ")
|
||||||
|
|
||||||
|
// Send notification
|
||||||
|
utils.NotifySuccess(&utils.NotificationData{
|
||||||
|
File: finalFileName,
|
||||||
|
BackupSize: utils.ConvertBytes(uint64(backupSize)),
|
||||||
|
Database: db.dbName,
|
||||||
|
Storage: config.storage,
|
||||||
|
BackupLocation: filepath.Join(config.remotePath, finalFileName),
|
||||||
|
StartTime: startTime,
|
||||||
|
EndTime: time.Now().Format(utils.TimeFormat()),
|
||||||
|
})
|
||||||
|
// Delete temp
|
||||||
|
deleteTemp()
|
||||||
|
utils.Info("Backup completed successfully")
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
301
pkg/backup.go
301
pkg/backup.go
@@ -1,14 +1,34 @@
|
|||||||
// Package pkg /
|
// Package internal /
|
||||||
/*****
|
/*
|
||||||
@author Jonas Kaninda
|
MIT License
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
|
||||||
@Copyright © 2024 Jonas Kaninda
|
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
|
package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/jkaninda/encryptor"
|
"github.com/jkaninda/encryptor"
|
||||||
|
"github.com/jkaninda/go-storage/pkg/local"
|
||||||
"github.com/jkaninda/mysql-bkup/utils"
|
"github.com/jkaninda/mysql-bkup/utils"
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -21,9 +41,9 @@ import (
|
|||||||
|
|
||||||
func StartBackup(cmd *cobra.Command) {
|
func StartBackup(cmd *cobra.Command) {
|
||||||
intro()
|
intro()
|
||||||
//Initialize backup configs
|
// Initialize backup configs
|
||||||
config := initBackupConfig(cmd)
|
config := initBackupConfig(cmd)
|
||||||
//Load backup configuration file
|
// Load backup configuration file
|
||||||
configFile, err := loadConfigFile()
|
configFile, err := loadConfigFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dbConf = initDbConfig(cmd)
|
dbConf = initDbConfig(cmd)
|
||||||
@@ -42,16 +62,16 @@ func StartBackup(cmd *cobra.Command) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run in scheduled mode
|
// scheduledMode Runs backup in scheduled mode
|
||||||
func scheduledMode(db *dbConfig, config *BackupConfig) {
|
func scheduledMode(db *dbConfig, config *BackupConfig) {
|
||||||
utils.Info("Running in Scheduled mode")
|
utils.Info("Running in Scheduled mode")
|
||||||
utils.Info("Backup cron expression: %s", config.cronExpression)
|
utils.Info("Backup cron expression: %s", config.cronExpression)
|
||||||
utils.Info("The next scheduled time is: %v", utils.CronNextTime(config.cronExpression).Format(timeFormat))
|
utils.Info("The next scheduled time is: %v", utils.CronNextTime(config.cronExpression).Format(timeFormat))
|
||||||
utils.Info("Storage type %s ", config.storage)
|
utils.Info("Storage type %s ", config.storage)
|
||||||
|
|
||||||
//Test backup
|
// Test backup
|
||||||
utils.Info("Testing backup configurations...")
|
utils.Info("Testing backup configurations...")
|
||||||
BackupTask(db, config)
|
testDatabaseConnection(db)
|
||||||
utils.Info("Testing backup configurations...done")
|
utils.Info("Testing backup configurations...done")
|
||||||
utils.Info("Creating backup job...")
|
utils.Info("Creating backup job...")
|
||||||
// Create a new cron instance
|
// Create a new cron instance
|
||||||
@@ -72,9 +92,22 @@ func scheduledMode(db *dbConfig, config *BackupConfig) {
|
|||||||
defer c.Stop()
|
defer c.Stop()
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackupTask backups database
|
||||||
func BackupTask(db *dbConfig, config *BackupConfig) {
|
func BackupTask(db *dbConfig, config *BackupConfig) {
|
||||||
utils.Info("Starting backup task...")
|
utils.Info("Starting backup task...")
|
||||||
//Generate file name
|
// Generate file name
|
||||||
backupFileName := fmt.Sprintf("%s_%s.sql.gz", db.dbName, time.Now().Format("20060102_150405"))
|
backupFileName := fmt.Sprintf("%s_%s.sql.gz", db.dbName, time.Now().Format("20060102_150405"))
|
||||||
if config.disableCompression {
|
if config.disableCompression {
|
||||||
backupFileName = fmt.Sprintf("%s_%s.sql", db.dbName, time.Now().Format("20060102_150405"))
|
backupFileName = fmt.Sprintf("%s_%s.sql", db.dbName, time.Now().Format("20060102_150405"))
|
||||||
@@ -89,52 +122,47 @@ func BackupTask(db *dbConfig, config *BackupConfig) {
|
|||||||
sshBackup(db, config)
|
sshBackup(db, config)
|
||||||
case "ftp", "FTP":
|
case "ftp", "FTP":
|
||||||
ftpBackup(db, config)
|
ftpBackup(db, config)
|
||||||
//utils.Fatal("Not supported storage type: %s", config.storage)
|
case "azure":
|
||||||
|
azureBackup(db, config)
|
||||||
default:
|
default:
|
||||||
localBackup(db, config)
|
localBackup(db, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 startMultiBackup(bkConfig *BackupConfig, configFile string) {
|
func startMultiBackup(bkConfig *BackupConfig, configFile string) {
|
||||||
utils.Info("Starting multiple backup jobs...")
|
utils.Info("Starting backup task...")
|
||||||
var conf = &Config{}
|
|
||||||
conf, err := readConf(configFile)
|
conf, err := readConf(configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Fatal("Error reading config file: %s", err)
|
utils.Fatal("Error reading config file: %s", err)
|
||||||
}
|
}
|
||||||
//Check if cronExpression is defined in config file
|
// Check if cronExpression is defined in config file
|
||||||
if conf.CronExpression != "" {
|
if conf.CronExpression != "" {
|
||||||
bkConfig.cronExpression = conf.CronExpression
|
bkConfig.cronExpression = conf.CronExpression
|
||||||
}
|
}
|
||||||
|
if len(conf.Databases) == 0 {
|
||||||
|
utils.Fatal("No databases found")
|
||||||
|
}
|
||||||
// Check if cronExpression is defined
|
// Check if cronExpression is defined
|
||||||
if bkConfig.cronExpression == "" {
|
if bkConfig.cronExpression == "" {
|
||||||
multiBackupTask(conf.Databases, bkConfig)
|
multiBackupTask(conf.Databases, bkConfig)
|
||||||
} else {
|
} else {
|
||||||
// Check if cronExpression is valid
|
// Check if cronExpression is valid
|
||||||
if utils.IsValidCronExpression(bkConfig.cronExpression) {
|
if utils.IsValidCronExpression(bkConfig.cronExpression) {
|
||||||
utils.Info("Running MultiBackup in Scheduled mode")
|
utils.Info("Running backup in Scheduled mode")
|
||||||
utils.Info("Backup cron expression: %s", bkConfig.cronExpression)
|
utils.Info("Backup cron expression: %s", bkConfig.cronExpression)
|
||||||
utils.Info("The next scheduled time is: %v", utils.CronNextTime(bkConfig.cronExpression).Format(timeFormat))
|
utils.Info("The next scheduled time is: %v", utils.CronNextTime(bkConfig.cronExpression).Format(timeFormat))
|
||||||
utils.Info("Storage type %s ", bkConfig.storage)
|
utils.Info("Storage type %s ", bkConfig.storage)
|
||||||
|
|
||||||
//Test backup
|
// Test backup
|
||||||
utils.Info("Testing backup configurations...")
|
utils.Info("Testing backup configurations...")
|
||||||
multiBackupTask(conf.Databases, bkConfig)
|
for _, db := range conf.Databases {
|
||||||
|
testDatabaseConnection(getDatabase(db))
|
||||||
|
}
|
||||||
utils.Info("Testing backup configurations...done")
|
utils.Info("Testing backup configurations...done")
|
||||||
utils.Info("Creating multi backup job...")
|
utils.Info("Creating backup job...")
|
||||||
// Create a new cron instance
|
// Create a new cron instance
|
||||||
c := cron.New()
|
c := cron.New()
|
||||||
|
|
||||||
_, err := c.AddFunc(bkConfig.cronExpression, func() {
|
_, err := c.AddFunc(bkConfig.cronExpression, func() {
|
||||||
// Create a channel
|
|
||||||
multiBackupTask(conf.Databases, bkConfig)
|
multiBackupTask(conf.Databases, bkConfig)
|
||||||
utils.Info("Next backup time is: %v", utils.CronNextTime(bkConfig.cronExpression).Format(timeFormat))
|
utils.Info("Next backup time is: %v", utils.CronNextTime(bkConfig.cronExpression).Format(timeFormat))
|
||||||
|
|
||||||
@@ -144,7 +172,7 @@ func startMultiBackup(bkConfig *BackupConfig, configFile string) {
|
|||||||
}
|
}
|
||||||
// Start the cron scheduler
|
// Start the cron scheduler
|
||||||
c.Start()
|
c.Start()
|
||||||
utils.Info("Creating multi backup job...done")
|
utils.Info("Creating backup job...done")
|
||||||
utils.Info("Backup job started")
|
utils.Info("Backup job started")
|
||||||
defer c.Stop()
|
defer c.Stop()
|
||||||
select {}
|
select {}
|
||||||
@@ -158,7 +186,6 @@ func startMultiBackup(bkConfig *BackupConfig, configFile string) {
|
|||||||
|
|
||||||
// BackupDatabase backup database
|
// BackupDatabase backup database
|
||||||
func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool) {
|
func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool) {
|
||||||
|
|
||||||
storagePath = os.Getenv("STORAGE_PATH")
|
storagePath = os.Getenv("STORAGE_PATH")
|
||||||
|
|
||||||
utils.Info("Starting database backup...")
|
utils.Info("Starting database backup...")
|
||||||
@@ -182,21 +209,26 @@ func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool
|
|||||||
)
|
)
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
utils.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// save output
|
// save output
|
||||||
file, err := os.Create(filepath.Join(tmpPath, backupFileName))
|
file, err := os.Create(filepath.Join(tmpPath, backupFileName))
|
||||||
if err != nil {
|
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)
|
_, err = file.Write(output)
|
||||||
if err != nil {
|
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 {
|
} else {
|
||||||
// Execute mysqldump
|
// Execute mysqldump
|
||||||
@@ -208,9 +240,9 @@ func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool
|
|||||||
gzipCmd := exec.Command("gzip")
|
gzipCmd := exec.Command("gzip")
|
||||||
gzipCmd.Stdin = stdout
|
gzipCmd.Stdin = stdout
|
||||||
gzipCmd.Stdout, err = os.Create(filepath.Join(tmpPath, backupFileName))
|
gzipCmd.Stdout, err = os.Create(filepath.Join(tmpPath, backupFileName))
|
||||||
gzipCmd.Start()
|
err = gzipCmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return
|
||||||
}
|
}
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
@@ -218,10 +250,9 @@ func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool
|
|||||||
if err := gzipCmd.Wait(); err != nil {
|
if err := gzipCmd.Wait(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
utils.Done("Database has been backed up")
|
utils.Info("Database has been backed up")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
func localBackup(db *dbConfig, config *BackupConfig) {
|
func localBackup(db *dbConfig, config *BackupConfig) {
|
||||||
utils.Info("Backup database to local storage")
|
utils.Info("Backup database to local storage")
|
||||||
@@ -234,193 +265,41 @@ func localBackup(db *dbConfig, config *BackupConfig) {
|
|||||||
}
|
}
|
||||||
fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName))
|
fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Error("Error:", err)
|
utils.Error("Error: %s", err)
|
||||||
}
|
}
|
||||||
//Get backup info
|
|
||||||
backupSize = fileInfo.Size()
|
backupSize = fileInfo.Size()
|
||||||
|
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 name is %s", finalFileName)
|
utils.Info("Backup name is %s", finalFileName)
|
||||||
moveToBackup(finalFileName, storagePath)
|
utils.Info("Backup size: %s", utils.ConvertBytes(uint64(backupSize)))
|
||||||
//Send notification
|
utils.Info("Backup saved in %s", filepath.Join(storagePath, finalFileName))
|
||||||
|
// Send notification
|
||||||
utils.NotifySuccess(&utils.NotificationData{
|
utils.NotifySuccess(&utils.NotificationData{
|
||||||
File: finalFileName,
|
File: finalFileName,
|
||||||
BackupSize: backupSize,
|
BackupSize: utils.ConvertBytes(uint64(backupSize)),
|
||||||
Database: db.dbName,
|
Database: db.dbName,
|
||||||
Storage: config.storage,
|
Storage: config.storage,
|
||||||
BackupLocation: filepath.Join(config.remotePath, finalFileName),
|
BackupLocation: filepath.Join(storagePath, finalFileName),
|
||||||
StartTime: startTime,
|
StartTime: startTime,
|
||||||
EndTime: time.Now().Format(utils.TimeFormat()),
|
EndTime: time.Now().Format(utils.TimeFormat()),
|
||||||
})
|
})
|
||||||
//Delete old backup
|
|
||||||
if config.prune {
|
|
||||||
deleteOldBackup(config.backupRetention)
|
|
||||||
}
|
|
||||||
//Delete temp
|
|
||||||
deleteTemp()
|
|
||||||
utils.Info("Backup completed successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
func s3Backup(db *dbConfig, config *BackupConfig) {
|
|
||||||
bucket := utils.GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
|
|
||||||
s3Path := utils.GetEnvVariable("AWS_S3_PATH", "S3_PATH")
|
|
||||||
if config.remotePath != "" {
|
|
||||||
s3Path = config.remotePath
|
|
||||||
}
|
|
||||||
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 ... ")
|
|
||||||
|
|
||||||
utils.Info("Backup name is %s", finalFileName)
|
|
||||||
err := UploadFileToS3(tmpPath, finalFileName, bucket, s3Path)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal("Error uploading backup archive to S3: %s ", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
//Get backup info
|
|
||||||
fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName))
|
|
||||||
if err != nil {
|
|
||||||
utils.Error("Error:", err)
|
|
||||||
}
|
|
||||||
backupSize = fileInfo.Size()
|
|
||||||
//Delete backup file from tmp folder
|
|
||||||
err = utils.DeleteFile(filepath.Join(tmpPath, config.backupFileName))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error deleting file: ", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
// Delete old backup
|
// Delete old backup
|
||||||
if config.prune {
|
if config.prune {
|
||||||
err := DeleteOldBackup(bucket, s3Path, config.backupRetention)
|
err = localStorage.Prune(config.backupRetention)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Fatal("Error deleting old backup from S3: %s ", err)
|
utils.Fatal("Error deleting old backup from %s storage: %s ", config.storage, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
utils.Done("Uploading backup archive to remote storage S3 ... done ")
|
// Delete temp
|
||||||
//Send notification
|
|
||||||
utils.NotifySuccess(&utils.NotificationData{
|
|
||||||
File: finalFileName,
|
|
||||||
BackupSize: backupSize,
|
|
||||||
Database: db.dbName,
|
|
||||||
Storage: config.storage,
|
|
||||||
BackupLocation: filepath.Join(s3Path, finalFileName),
|
|
||||||
StartTime: startTime,
|
|
||||||
EndTime: time.Now().Format(utils.TimeFormat()),
|
|
||||||
})
|
|
||||||
//Delete temp
|
|
||||||
deleteTemp()
|
deleteTemp()
|
||||||
utils.Info("Backup completed successfully")
|
utils.Info("Backup completed successfully")
|
||||||
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
err := CopyToRemote(finalFileName, config.remotePath)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal("Error uploading file to the remote server: %s ", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
//Get backup info
|
|
||||||
fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName))
|
|
||||||
if err != nil {
|
|
||||||
utils.Error("Error:", err)
|
|
||||||
}
|
|
||||||
backupSize = fileInfo.Size()
|
|
||||||
//Delete backup file from tmp folder
|
|
||||||
err = utils.DeleteFile(filepath.Join(tmpPath, finalFileName))
|
|
||||||
if err != nil {
|
|
||||||
utils.Error("Error deleting file: %v", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
if config.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(&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)
|
|
||||||
err := CopyToFTP(finalFileName, config.remotePath)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal("Error uploading file to the remote FTP server: %s ", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
//Get backup info
|
|
||||||
fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName))
|
|
||||||
if err != nil {
|
|
||||||
utils.Error("Error:", err)
|
|
||||||
}
|
|
||||||
backupSize = fileInfo.Size()
|
|
||||||
//Delete backup file from tmp folder
|
|
||||||
err = utils.DeleteFile(filepath.Join(tmpPath, finalFileName))
|
|
||||||
if err != nil {
|
|
||||||
utils.Error("Error deleting file: %v", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
if config.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 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 encryptBackup(config *BackupConfig) {
|
func encryptBackup(config *BackupConfig) {
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
// Package pkg /
|
/*
|
||||||
/*****
|
MIT License
|
||||||
@author Jonas Kaninda
|
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
Copyright (c) 2023 Jonas Kaninda
|
||||||
@Copyright © 2024 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
|
package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -62,16 +80,21 @@ type FTPConfig struct {
|
|||||||
host string
|
host string
|
||||||
user string
|
user string
|
||||||
password string
|
password string
|
||||||
port string
|
port int
|
||||||
remotePath string
|
remotePath string
|
||||||
}
|
}
|
||||||
|
type AzureConfig struct {
|
||||||
|
accountName string
|
||||||
|
accountKey string
|
||||||
|
containerName string
|
||||||
|
}
|
||||||
|
|
||||||
// SSHConfig holds the SSH connection details
|
// SSHConfig holds the SSH connection details
|
||||||
type SSHConfig struct {
|
type SSHConfig struct {
|
||||||
user string
|
user string
|
||||||
password string
|
password string
|
||||||
hostName string
|
hostName string
|
||||||
port string
|
port int
|
||||||
identifyFile string
|
identifyFile string
|
||||||
}
|
}
|
||||||
type AWSConfig struct {
|
type AWSConfig struct {
|
||||||
@@ -80,12 +103,13 @@ type AWSConfig struct {
|
|||||||
accessKey string
|
accessKey string
|
||||||
secretKey string
|
secretKey string
|
||||||
region string
|
region string
|
||||||
|
remotePath string
|
||||||
disableSsl bool
|
disableSsl bool
|
||||||
forcePathStyle bool
|
forcePathStyle bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDbConfig(cmd *cobra.Command) *dbConfig {
|
func initDbConfig(cmd *cobra.Command) *dbConfig {
|
||||||
//Set env
|
// Set env
|
||||||
utils.GetEnv(cmd, "dbname", "DB_NAME")
|
utils.GetEnv(cmd, "dbname", "DB_NAME")
|
||||||
dConf := dbConfig{}
|
dConf := dbConfig{}
|
||||||
dConf.dbHost = os.Getenv("DB_HOST")
|
dConf.dbHost = os.Getenv("DB_HOST")
|
||||||
@@ -125,17 +149,17 @@ func loadSSHConfig() (*SSHConfig, error) {
|
|||||||
user: os.Getenv("SSH_USER"),
|
user: os.Getenv("SSH_USER"),
|
||||||
password: os.Getenv("SSH_PASSWORD"),
|
password: os.Getenv("SSH_PASSWORD"),
|
||||||
hostName: os.Getenv("SSH_HOST"),
|
hostName: os.Getenv("SSH_HOST"),
|
||||||
port: os.Getenv("SSH_PORT"),
|
port: utils.GetIntEnv("SSH_PORT"),
|
||||||
identifyFile: os.Getenv("SSH_IDENTIFY_FILE"),
|
identifyFile: os.Getenv("SSH_IDENTIFY_FILE"),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
func initFtpConfig() *FTPConfig {
|
func loadFtpConfig() *FTPConfig {
|
||||||
//Initialize data configs
|
// Initialize data configs
|
||||||
fConfig := FTPConfig{}
|
fConfig := FTPConfig{}
|
||||||
fConfig.host = utils.GetEnvVariable("FTP_HOST", "FTP_HOST_NAME")
|
fConfig.host = utils.GetEnvVariable("FTP_HOST", "FTP_HOST_NAME")
|
||||||
fConfig.user = os.Getenv("FTP_USER")
|
fConfig.user = os.Getenv("FTP_USER")
|
||||||
fConfig.password = os.Getenv("FTP_PASSWORD")
|
fConfig.password = os.Getenv("FTP_PASSWORD")
|
||||||
fConfig.port = os.Getenv("FTP_PORT")
|
fConfig.port = utils.GetIntEnv("FTP_PORT")
|
||||||
fConfig.remotePath = os.Getenv("REMOTE_PATH")
|
fConfig.remotePath = os.Getenv("REMOTE_PATH")
|
||||||
err := utils.CheckEnvVars(ftpVars)
|
err := utils.CheckEnvVars(ftpVars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -144,13 +168,30 @@ func initFtpConfig() *FTPConfig {
|
|||||||
}
|
}
|
||||||
return &fConfig
|
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 {
|
func initAWSConfig() *AWSConfig {
|
||||||
//Initialize AWS configs
|
// Initialize AWS configs
|
||||||
aConfig := AWSConfig{}
|
aConfig := AWSConfig{}
|
||||||
aConfig.endpoint = utils.GetEnvVariable("AWS_S3_ENDPOINT", "S3_ENDPOINT")
|
aConfig.endpoint = utils.GetEnvVariable("AWS_S3_ENDPOINT", "S3_ENDPOINT")
|
||||||
aConfig.accessKey = utils.GetEnvVariable("AWS_ACCESS_KEY", "ACCESS_KEY")
|
aConfig.accessKey = utils.GetEnvVariable("AWS_ACCESS_KEY", "ACCESS_KEY")
|
||||||
aConfig.secretKey = utils.GetEnvVariable("AWS_SECRET_KEY", "SECRET_KEY")
|
aConfig.secretKey = utils.GetEnvVariable("AWS_SECRET_KEY", "SECRET_KEY")
|
||||||
aConfig.bucket = utils.GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
|
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")
|
aConfig.region = os.Getenv("AWS_REGION")
|
||||||
disableSsl, err := strconv.ParseBool(os.Getenv("AWS_DISABLE_SSL"))
|
disableSsl, err := strconv.ParseBool(os.Getenv("AWS_DISABLE_SSL"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -173,7 +214,7 @@ func initBackupConfig(cmd *cobra.Command) *BackupConfig {
|
|||||||
utils.SetEnv("STORAGE_PATH", storagePath)
|
utils.SetEnv("STORAGE_PATH", storagePath)
|
||||||
utils.GetEnv(cmd, "cron-expression", "BACKUP_CRON_EXPRESSION")
|
utils.GetEnv(cmd, "cron-expression", "BACKUP_CRON_EXPRESSION")
|
||||||
utils.GetEnv(cmd, "path", "REMOTE_PATH")
|
utils.GetEnv(cmd, "path", "REMOTE_PATH")
|
||||||
//Get flag value and set env
|
// Get flag value and set env
|
||||||
remotePath := utils.GetEnvVariable("REMOTE_PATH", "SSH_REMOTE_PATH")
|
remotePath := utils.GetEnvVariable("REMOTE_PATH", "SSH_REMOTE_PATH")
|
||||||
storage = utils.GetEnv(cmd, "storage", "STORAGE")
|
storage = utils.GetEnv(cmd, "storage", "STORAGE")
|
||||||
prune := false
|
prune := false
|
||||||
@@ -195,7 +236,7 @@ func initBackupConfig(cmd *cobra.Command) *BackupConfig {
|
|||||||
encryption = true
|
encryption = true
|
||||||
usingKey = false
|
usingKey = false
|
||||||
}
|
}
|
||||||
//Initialize backup configs
|
// Initialize backup configs
|
||||||
config := BackupConfig{}
|
config := BackupConfig{}
|
||||||
config.backupRetention = backupRetention
|
config.backupRetention = backupRetention
|
||||||
config.disableCompression = disableCompression
|
config.disableCompression = disableCompression
|
||||||
@@ -225,7 +266,7 @@ func initRestoreConfig(cmd *cobra.Command) *RestoreConfig {
|
|||||||
utils.SetEnv("STORAGE_PATH", storagePath)
|
utils.SetEnv("STORAGE_PATH", storagePath)
|
||||||
utils.GetEnv(cmd, "path", "REMOTE_PATH")
|
utils.GetEnv(cmd, "path", "REMOTE_PATH")
|
||||||
|
|
||||||
//Get flag value and set env
|
// Get flag value and set env
|
||||||
s3Path := utils.GetEnv(cmd, "path", "AWS_S3_PATH")
|
s3Path := utils.GetEnv(cmd, "path", "AWS_S3_PATH")
|
||||||
remotePath := utils.GetEnvVariable("REMOTE_PATH", "SSH_REMOTE_PATH")
|
remotePath := utils.GetEnvVariable("REMOTE_PATH", "SSH_REMOTE_PATH")
|
||||||
storage = utils.GetEnv(cmd, "storage", "STORAGE")
|
storage = utils.GetEnv(cmd, "storage", "STORAGE")
|
||||||
@@ -239,7 +280,7 @@ func initRestoreConfig(cmd *cobra.Command) *RestoreConfig {
|
|||||||
usingKey = false
|
usingKey = false
|
||||||
}
|
}
|
||||||
|
|
||||||
//Initialize restore configs
|
// Initialize restore configs
|
||||||
rConfig := RestoreConfig{}
|
rConfig := RestoreConfig{}
|
||||||
rConfig.s3Path = s3Path
|
rConfig.s3Path = s3Path
|
||||||
rConfig.remotePath = remotePath
|
rConfig.remotePath = remotePath
|
||||||
|
|||||||
81
pkg/ftp.go
81
pkg/ftp.go
@@ -1,81 +0,0 @@
|
|||||||
package pkg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/jlaffaye/ftp"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// initFtpClient initializes and authenticates an FTP client
|
|
||||||
func initFtpClient() (*ftp.ServerConn, error) {
|
|
||||||
ftpConfig := initFtpConfig()
|
|
||||||
ftpClient, err := ftp.Dial(fmt.Sprintf("%s:%s", ftpConfig.host, ftpConfig.port), ftp.DialWithTimeout(5*time.Second))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to connect to FTP: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ftpClient.Login(ftpConfig.user, ftpConfig.password)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to log in to FTP: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ftpClient, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyToFTP uploads a file to the remote FTP server
|
|
||||||
func CopyToFTP(fileName, remotePath string) (err error) {
|
|
||||||
ftpConfig := initFtpConfig()
|
|
||||||
ftpClient, err := initFtpClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer ftpClient.Quit()
|
|
||||||
|
|
||||||
filePath := filepath.Join(tmpPath, fileName)
|
|
||||||
file, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open file %s: %w", fileName, err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
remoteFilePath := filepath.Join(ftpConfig.remotePath, fileName)
|
|
||||||
err = ftpClient.Stor(remoteFilePath, file)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to upload file %s: %w", fileName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyFromFTP downloads a file from the remote FTP server
|
|
||||||
func CopyFromFTP(fileName, remotePath string) (err error) {
|
|
||||||
ftpClient, err := initFtpClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer ftpClient.Quit()
|
|
||||||
|
|
||||||
remoteFilePath := filepath.Join(remotePath, fileName)
|
|
||||||
r, err := ftpClient.Retr(remoteFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to retrieve file %s: %w", fileName, err)
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
localFilePath := filepath.Join(tmpPath, fileName)
|
|
||||||
outFile, err := os.Create(localFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create local file %s: %w", fileName, err)
|
|
||||||
}
|
|
||||||
defer outFile.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(outFile, r)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to copy data to local file %s: %w", fileName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
117
pkg/helper.go
117
pkg/helper.go
@@ -1,9 +1,27 @@
|
|||||||
// Package pkg /
|
/*
|
||||||
/*****
|
MIT License
|
||||||
@author Jonas Kaninda
|
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
Copyright (c) 2023 Jonas Kaninda
|
||||||
@Copyright © 2024 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
|
package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -15,75 +33,15 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func copyToTmp(sourcePath string, backupFileName string) {
|
func intro() {
|
||||||
//Copy backup from storage to /tmp
|
fmt.Println("Starting MySQL Backup...")
|
||||||
err := utils.CopyFile(filepath.Join(sourcePath, backupFileName), filepath.Join(tmpPath, backupFileName))
|
fmt.Printf("Version: %s\n", utils.Version)
|
||||||
if err != nil {
|
fmt.Println("Copyright (c) 2024 Jonas Kaninda")
|
||||||
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))
|
|
||||||
|
|
||||||
}
|
// copyToTmp copy file to temporary directory
|
||||||
//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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
|
|
||||||
}
|
|
||||||
func deleteTemp() {
|
func deleteTemp() {
|
||||||
utils.Info("Deleting %s ...", tmpPath)
|
utils.Info("Deleting %s ...", tmpPath)
|
||||||
err := filepath.Walk(tmpPath, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(tmpPath, func(path string, info os.FileInfo, err error) error {
|
||||||
@@ -114,7 +72,9 @@ func testDatabaseConnection(db *dbConfig) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
utils.Info("Connecting to %s database ...", db.dbName)
|
utils.Info("Connecting to %s database ...", db.dbName)
|
||||||
cmd := exec.Command("mysql", "-h", db.dbHost, "-P", db.dbPort, "-u", db.dbUserName, db.dbName, "-e", "quit")
|
// Set database name for notification error
|
||||||
|
utils.DatabaseName = db.dbName
|
||||||
|
cmd := exec.Command("mariadb", "-h", db.dbHost, "-P", db.dbPort, "-u", db.dbUserName, db.dbName, "-e", "quit")
|
||||||
// Capture the output
|
// Capture the output
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
cmd.Stdout = &out
|
cmd.Stdout = &out
|
||||||
@@ -127,10 +87,8 @@ func testDatabaseConnection(db *dbConfig) {
|
|||||||
utils.Info("Successfully connected to %s database", db.dbName)
|
utils.Info("Successfully connected to %s database", db.dbName)
|
||||||
|
|
||||||
}
|
}
|
||||||
func intro() {
|
|
||||||
utils.Info("Starting MySQL Backup...")
|
// checkPubKeyFile checks gpg public key
|
||||||
utils.Info("Copyright (c) 2024 Jonas Kaninda ")
|
|
||||||
}
|
|
||||||
func checkPubKeyFile(pubKey string) (string, error) {
|
func checkPubKeyFile(pubKey string) (string, error) {
|
||||||
// Define possible key file names
|
// Define possible key file names
|
||||||
keyFiles := []string{filepath.Join(gpgHome, "public_key.asc"), filepath.Join(gpgHome, "public_key.gpg"), pubKey}
|
keyFiles := []string{filepath.Join(gpgHome, "public_key.asc"), filepath.Join(gpgHome, "public_key.gpg"), pubKey}
|
||||||
@@ -152,6 +110,8 @@ func checkPubKeyFile(pubKey string) (string, error) {
|
|||||||
// Return an error if neither file exists
|
// Return an error if neither file exists
|
||||||
return "", fmt.Errorf("no public key file found")
|
return "", fmt.Errorf("no public key file found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkPrKeyFile checks private key
|
||||||
func checkPrKeyFile(prKey string) (string, error) {
|
func checkPrKeyFile(prKey string) (string, error) {
|
||||||
// Define possible key file names
|
// Define possible key file names
|
||||||
keyFiles := []string{filepath.Join(gpgHome, "private_key.asc"), filepath.Join(gpgHome, "private_key.gpg"), prKey}
|
keyFiles := []string{filepath.Join(gpgHome, "private_key.asc"), filepath.Join(gpgHome, "private_key.gpg"), prKey}
|
||||||
@@ -173,8 +133,9 @@ func checkPrKeyFile(prKey string) (string, error) {
|
|||||||
// Return an error if neither file exists
|
// Return an error if neither file exists
|
||||||
return "", fmt.Errorf("no public key file found")
|
return "", fmt.Errorf("no public key file found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readConf reads config file and returns Config
|
||||||
func readConf(configFile string) (*Config, error) {
|
func readConf(configFile string) (*Config, error) {
|
||||||
//configFile := filepath.Join("./", filename)
|
|
||||||
if utils.FileExists(configFile) {
|
if utils.FileExists(configFile) {
|
||||||
buf, err := os.ReadFile(configFile)
|
buf, err := os.ReadFile(configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -191,6 +152,8 @@ func readConf(configFile string) (*Config, error) {
|
|||||||
}
|
}
|
||||||
return nil, fmt.Errorf("config file %q not found", configFile)
|
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) {
|
func checkConfigFile(filePath string) (string, error) {
|
||||||
// Define possible config file names
|
// Define possible config file names
|
||||||
configFiles := []string{filepath.Join(workingDir, "config.yaml"), filepath.Join(workingDir, "config.yml"), filePath}
|
configFiles := []string{filepath.Join(workingDir, "config.yaml"), filepath.Join(workingDir, "config.yml"), filePath}
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
// Package pkg /
|
/*
|
||||||
/*****
|
MIT License
|
||||||
@author Jonas Kaninda
|
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
Copyright (c) 2023 Jonas Kaninda
|
||||||
@Copyright © 2024 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
|
package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -16,11 +34,11 @@ import (
|
|||||||
func StartMigration(cmd *cobra.Command) {
|
func StartMigration(cmd *cobra.Command) {
|
||||||
intro()
|
intro()
|
||||||
utils.Info("Starting database migration...")
|
utils.Info("Starting database migration...")
|
||||||
//Get DB config
|
// Get DB config
|
||||||
dbConf = initDbConfig(cmd)
|
dbConf = initDbConfig(cmd)
|
||||||
targetDbConf = initTargetDbConfig()
|
targetDbConf = initTargetDbConfig()
|
||||||
|
|
||||||
//Defining the target database variables
|
// Defining the target database variables
|
||||||
newDbConfig := dbConfig{}
|
newDbConfig := dbConfig{}
|
||||||
newDbConfig.dbHost = targetDbConf.targetDbHost
|
newDbConfig.dbHost = targetDbConf.targetDbHost
|
||||||
newDbConfig.dbPort = targetDbConf.targetDbPort
|
newDbConfig.dbPort = targetDbConf.targetDbPort
|
||||||
@@ -28,13 +46,13 @@ func StartMigration(cmd *cobra.Command) {
|
|||||||
newDbConfig.dbUserName = targetDbConf.targetDbUserName
|
newDbConfig.dbUserName = targetDbConf.targetDbUserName
|
||||||
newDbConfig.dbPassword = targetDbConf.targetDbPassword
|
newDbConfig.dbPassword = targetDbConf.targetDbPassword
|
||||||
|
|
||||||
//Generate file name
|
// Generate file name
|
||||||
backupFileName := fmt.Sprintf("%s_%s.sql", dbConf.dbName, time.Now().Format("20060102_150405"))
|
backupFileName := fmt.Sprintf("%s_%s.sql", dbConf.dbName, time.Now().Format("20060102_150405"))
|
||||||
conf := &RestoreConfig{}
|
conf := &RestoreConfig{}
|
||||||
conf.file = backupFileName
|
conf.file = backupFileName
|
||||||
//Backup source Database
|
// Backup source Database
|
||||||
BackupDatabase(dbConf, backupFileName, true)
|
BackupDatabase(dbConf, backupFileName, true)
|
||||||
//Restore source database into target database
|
// Restore source database into target database
|
||||||
utils.Info("Restoring [%s] database into [%s] database...", dbConf.dbName, targetDbConf.targetDbName)
|
utils.Info("Restoring [%s] database into [%s] database...", dbConf.dbName, targetDbConf.targetDbName)
|
||||||
RestoreDatabase(&newDbConfig, conf)
|
RestoreDatabase(&newDbConfig, conf)
|
||||||
utils.Info("[%s] database has been restored into [%s] database", dbConf.dbName, targetDbConf.targetDbName)
|
utils.Info("[%s] database has been restored into [%s] database", dbConf.dbName, targetDbConf.targetDbName)
|
||||||
|
|||||||
220
pkg/remote.go
Normal file
220
pkg/remote.go
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
/*
|
||||||
|
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 ... ")
|
||||||
|
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: 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 name is %s", finalFileName)
|
||||||
|
utils.Info("Backup size: %s", utils.ConvertBytes(uint64(backupSize)))
|
||||||
|
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: utils.ConvertBytes(uint64(backupSize)),
|
||||||
|
Database: db.dbName,
|
||||||
|
Storage: config.storage,
|
||||||
|
BackupLocation: filepath.Join(config.remotePath, finalFileName),
|
||||||
|
StartTime: startTime,
|
||||||
|
EndTime: time.Now().Format(utils.TimeFormat()),
|
||||||
|
})
|
||||||
|
// Delete temp
|
||||||
|
deleteTemp()
|
||||||
|
utils.Info("Backup completed successfully")
|
||||||
|
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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("Backup name is %s", finalFileName)
|
||||||
|
utils.Info("Backup size: %s", utils.ConvertBytes(uint64(backupSize)))
|
||||||
|
utils.Info("Uploading backup archive to the remote FTP server ... done ")
|
||||||
|
|
||||||
|
// Send notification
|
||||||
|
utils.NotifySuccess(&utils.NotificationData{
|
||||||
|
File: finalFileName,
|
||||||
|
BackupSize: utils.ConvertBytes(uint64(backupSize)),
|
||||||
|
Database: db.dbName,
|
||||||
|
Storage: config.storage,
|
||||||
|
BackupLocation: filepath.Join(config.remotePath, finalFileName),
|
||||||
|
StartTime: startTime,
|
||||||
|
EndTime: time.Now().Format(utils.TimeFormat()),
|
||||||
|
})
|
||||||
|
// Delete temp
|
||||||
|
deleteTemp()
|
||||||
|
utils.Info("Backup completed successfully")
|
||||||
|
}
|
||||||
@@ -1,13 +1,32 @@
|
|||||||
// Package pkg /
|
/*
|
||||||
/*****
|
MIT License
|
||||||
@author Jonas Kaninda
|
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
Copyright (c) 2023 Jonas Kaninda
|
||||||
@Copyright © 2024 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
|
package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/jkaninda/encryptor"
|
"github.com/jkaninda/encryptor"
|
||||||
|
"github.com/jkaninda/go-storage/pkg/local"
|
||||||
"github.com/jkaninda/mysql-bkup/utils"
|
"github.com/jkaninda/mysql-bkup/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"os"
|
"os"
|
||||||
@@ -22,45 +41,31 @@ func StartRestore(cmd *cobra.Command) {
|
|||||||
|
|
||||||
switch restoreConf.storage {
|
switch restoreConf.storage {
|
||||||
case "local":
|
case "local":
|
||||||
utils.Info("Restore database from local")
|
localRestore(dbConf, restoreConf)
|
||||||
copyToTmp(storagePath, restoreConf.file)
|
|
||||||
RestoreDatabase(dbConf, restoreConf)
|
|
||||||
case "s3", "S3":
|
case "s3", "S3":
|
||||||
restoreFromS3(dbConf, restoreConf)
|
s3Restore(dbConf, restoreConf)
|
||||||
case "ssh", "SSH", "remote":
|
case "ssh", "SSH", "remote":
|
||||||
restoreFromRemote(dbConf, restoreConf)
|
remoteRestore(dbConf, restoreConf)
|
||||||
case "ftp", "FTP":
|
case "ftp", "FTP":
|
||||||
restoreFromFTP(dbConf, restoreConf)
|
ftpRestore(dbConf, restoreConf)
|
||||||
|
case "azure":
|
||||||
|
azureRestore(dbConf, restoreConf)
|
||||||
default:
|
default:
|
||||||
utils.Info("Restore database from local")
|
localRestore(dbConf, restoreConf)
|
||||||
copyToTmp(storagePath, restoreConf.file)
|
|
||||||
RestoreDatabase(dbConf, restoreConf)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func localRestore(dbConf *dbConfig, restoreConf *RestoreConfig) {
|
||||||
|
utils.Info("Restore database from local")
|
||||||
|
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, conf *RestoreConfig) {
|
|
||||||
utils.Info("Restore database from s3")
|
|
||||||
err := DownloadFile(tmpPath, conf.file, conf.bucket, conf.s3Path)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal("Error download file from s3 %s %v ", conf.file, err)
|
|
||||||
}
|
|
||||||
RestoreDatabase(db, conf)
|
|
||||||
}
|
|
||||||
func restoreFromRemote(db *dbConfig, conf *RestoreConfig) {
|
|
||||||
utils.Info("Restore database from remote server")
|
|
||||||
err := CopyFromRemote(conf.file, conf.remotePath)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal("Error download file from remote server: %s %v", filepath.Join(conf.remotePath, conf.file), err)
|
|
||||||
}
|
|
||||||
RestoreDatabase(db, conf)
|
|
||||||
}
|
|
||||||
func restoreFromFTP(db *dbConfig, conf *RestoreConfig) {
|
|
||||||
utils.Info("Restore database from FTP server")
|
|
||||||
err := CopyFromFTP(conf.file, conf.remotePath)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal("Error download file from FTP server: %s %v", filepath.Join(conf.remotePath, conf.file), err)
|
|
||||||
}
|
|
||||||
RestoreDatabase(db, conf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreDatabase restore database
|
// RestoreDatabase restore database
|
||||||
@@ -95,13 +100,13 @@ func RestoreDatabase(db *dbConfig, conf *RestoreConfig) {
|
|||||||
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.")
|
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 {
|
} else {
|
||||||
utils.Info("Decrypting backup using passphrase...")
|
utils.Info("Decrypting backup using passphrase...")
|
||||||
//decryptWithGPG file
|
// decryptWithGPG file
|
||||||
err := encryptor.Decrypt(rFile, outputFile, conf.passphrase)
|
err := encryptor.Decrypt(rFile, outputFile, conf.passphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Fatal("Error decrypting file %s %v", file, err)
|
utils.Fatal("Error decrypting file %s %v", file, err)
|
||||||
}
|
}
|
||||||
utils.Info("Decrypting backup using passphrase...done")
|
utils.Info("Decrypting backup using passphrase...done")
|
||||||
//Update file name
|
// Update file name
|
||||||
conf.file = RemoveLastExtension(file)
|
conf.file = RemoveLastExtension(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,26 +124,26 @@ func RestoreDatabase(db *dbConfig, conf *RestoreConfig) {
|
|||||||
extension := filepath.Ext(filepath.Join(tmpPath, conf.file))
|
extension := filepath.Ext(filepath.Join(tmpPath, conf.file))
|
||||||
// Restore from compressed file / .sql.gz
|
// Restore from compressed file / .sql.gz
|
||||||
if extension == ".gz" {
|
if extension == ".gz" {
|
||||||
str := "zcat " + filepath.Join(tmpPath, conf.file) + " | mysql -h " + db.dbHost + " -P " + db.dbPort + " -u " + db.dbUserName + " " + db.dbName
|
str := "zcat " + filepath.Join(tmpPath, conf.file) + " | mariadb -h " + db.dbHost + " -P " + db.dbPort + " -u " + db.dbUserName + " " + db.dbName
|
||||||
_, err := exec.Command("sh", "-c", str).Output()
|
_, err := exec.Command("sh", "-c", str).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Fatal("Error, in restoring the database %v", err)
|
utils.Fatal("Error, in restoring the database %v", err)
|
||||||
}
|
}
|
||||||
utils.Info("Restoring database... done")
|
utils.Info("Restoring database... done")
|
||||||
utils.Done("Database has been restored")
|
utils.Info("Database has been restored")
|
||||||
//Delete temp
|
// Delete temp
|
||||||
deleteTemp()
|
deleteTemp()
|
||||||
|
|
||||||
} else if extension == ".sql" {
|
} else if extension == ".sql" {
|
||||||
//Restore from sql file
|
// Restore from sql file
|
||||||
str := "cat " + filepath.Join(tmpPath, conf.file) + " | mysql -h " + db.dbHost + " -P " + db.dbPort + " -u " + db.dbUserName + " " + db.dbName
|
str := "cat " + filepath.Join(tmpPath, conf.file) + " | mariadb -h " + db.dbHost + " -P " + db.dbPort + " -u " + db.dbUserName + " " + db.dbName
|
||||||
_, err := exec.Command("sh", "-c", str).Output()
|
_, err := exec.Command("sh", "-c", str).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Fatal("Error in restoring the database %v", err)
|
utils.Fatal("Error in restoring the database %v", err)
|
||||||
}
|
}
|
||||||
utils.Info("Restoring database... done")
|
utils.Info("Restoring database... done")
|
||||||
utils.Done("Database has been restored")
|
utils.Info("Database has been restored")
|
||||||
//Delete temp
|
// Delete temp
|
||||||
deleteTemp()
|
deleteTemp()
|
||||||
} else {
|
} else {
|
||||||
utils.Fatal("Unknown file extension %s", extension)
|
utils.Fatal("Unknown file extension %s", extension)
|
||||||
|
|||||||
230
pkg/s3.go
230
pkg/s3.go
@@ -1,148 +1,134 @@
|
|||||||
// Package pkg
|
/*
|
||||||
/*****
|
MIT License
|
||||||
@author Jonas Kaninda
|
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
Copyright (c) 2023 Jonas Kaninda
|
||||||
@Copyright © 2024 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
|
package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"fmt"
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/jkaninda/go-storage/pkg/s3"
|
||||||
"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"
|
|
||||||
"github.com/jkaninda/mysql-bkup/utils"
|
"github.com/jkaninda/mysql-bkup/utils"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateSession creates a new AWS session
|
func s3Backup(db *dbConfig, config *BackupConfig) {
|
||||||
func CreateSession() (*session.Session, error) {
|
|
||||||
|
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()
|
awsConfig := initAWSConfig()
|
||||||
// Configure to use MinIO Server
|
if config.remotePath == "" {
|
||||||
s3Config := &aws.Config{
|
config.remotePath = awsConfig.remotePath
|
||||||
Credentials: credentials.NewStaticCredentials(awsConfig.accessKey, awsConfig.secretKey, ""),
|
|
||||||
Endpoint: aws.String(awsConfig.endpoint),
|
|
||||||
Region: aws.String(awsConfig.region),
|
|
||||||
DisableSSL: aws.Bool(awsConfig.disableSsl),
|
|
||||||
S3ForcePathStyle: aws.Bool(awsConfig.forcePathStyle),
|
|
||||||
}
|
}
|
||||||
return session.NewSession(s3Config)
|
utils.Info("Backup name is %s", finalFileName)
|
||||||
|
s3Storage, err := s3.NewStorage(s3.Config{
|
||||||
}
|
Endpoint: awsConfig.endpoint,
|
||||||
|
Bucket: awsConfig.bucket,
|
||||||
// UploadFileToS3 uploads a file to S3 with a given prefix
|
AccessKey: awsConfig.accessKey,
|
||||||
func UploadFileToS3(filePath, key, bucket, prefix string) error {
|
SecretKey: awsConfig.secretKey,
|
||||||
sess, err := CreateSession()
|
Region: awsConfig.region,
|
||||||
if err != nil {
|
DisableSsl: awsConfig.disableSsl,
|
||||||
return err
|
ForcePathStyle: awsConfig.forcePathStyle,
|
||||||
}
|
RemotePath: config.remotePath,
|
||||||
|
LocalPath: tmpPath,
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
utils.Fatal("Error creating s3 storage: %s", err)
|
||||||
}
|
}
|
||||||
|
err = s3Storage.Copy(finalFileName)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func DownloadFile(destinationPath, key, bucket, prefix string) error {
|
|
||||||
|
|
||||||
sess, err := CreateSession()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
utils.Fatal("Error copying backup file: %s", err)
|
||||||
}
|
}
|
||||||
utils.Info("Download data from S3 storage...")
|
// Get backup info
|
||||||
file, err := os.Create(filepath.Join(destinationPath, key))
|
fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Error("Failed to create file", err)
|
utils.Error("Error: %s", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
defer file.Close()
|
backupSize = fileInfo.Size()
|
||||||
|
|
||||||
objectKey := filepath.Join(prefix, key)
|
// Delete backup file from tmp folder
|
||||||
|
err = utils.DeleteFile(filepath.Join(tmpPath, config.backupFileName))
|
||||||
downloader := s3manager.NewDownloader(sess)
|
|
||||||
numBytes, err := downloader.Download(file,
|
|
||||||
&s3.GetObjectInput{
|
|
||||||
Bucket: aws.String(bucket),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Error("Failed to download file %s", key)
|
fmt.Println("Error deleting file: ", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
utils.Info("Backup downloaded: %s bytes size %s ", file.Name(), numBytes)
|
// Delete old backup
|
||||||
|
if config.prune {
|
||||||
return nil
|
err := s3Storage.Prune(config.backupRetention)
|
||||||
}
|
if err != nil {
|
||||||
func DeleteOldBackup(bucket, prefix string, retention int) error {
|
utils.Fatal("Error deleting old backup from %s storage: %s ", config.storage, err)
|
||||||
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 {
|
|
||||||
utils.Info("Failed to delete object %s: %v", *object.Key, err)
|
|
||||||
} else {
|
|
||||||
utils.Info("Deleted object %s\n", *object.Key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return !lastPage
|
}
|
||||||
|
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: utils.ConvertBytes(uint64(backupSize)),
|
||||||
|
Database: db.dbName,
|
||||||
|
Storage: config.storage,
|
||||||
|
BackupLocation: filepath.Join(config.remotePath, finalFileName),
|
||||||
|
StartTime: startTime,
|
||||||
|
EndTime: time.Now().Format(utils.TimeFormat()),
|
||||||
|
})
|
||||||
|
// Delete temp
|
||||||
|
deleteTemp()
|
||||||
|
utils.Info("Backup completed successfully")
|
||||||
|
|
||||||
|
}
|
||||||
|
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 {
|
if err != nil {
|
||||||
utils.Error("Failed to list objects: %v", err)
|
utils.Fatal("Error creating s3 storage: %s", err)
|
||||||
}
|
}
|
||||||
|
err = s3Storage.CopyFrom(conf.file)
|
||||||
utils.Info("Finished deleting old files.")
|
if err != nil {
|
||||||
return nil
|
utils.Fatal("Error download file from S3 storage: %s", err)
|
||||||
|
}
|
||||||
|
RestoreDatabase(db, conf)
|
||||||
}
|
}
|
||||||
|
|||||||
111
pkg/scp.go
111
pkg/scp.go
@@ -1,111 +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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// createSSHClientConfig sets up the SSH client configuration based on the provided SSHConfig
|
|
||||||
func createSSHClientConfig(sshConfig *SSHConfig) (ssh.ClientConfig, error) {
|
|
||||||
if sshConfig.identifyFile != "" && utils.FileExists(sshConfig.identifyFile) {
|
|
||||||
return auth.PrivateKey(sshConfig.user, sshConfig.identifyFile, ssh.InsecureIgnoreHostKey())
|
|
||||||
} else {
|
|
||||||
if sshConfig.password == "" {
|
|
||||||
return ssh.ClientConfig{}, errors.New("SSH_PASSWORD environment variable is required if SSH_IDENTIFY_FILE is empty")
|
|
||||||
}
|
|
||||||
utils.Warn("Accessing the remote server using password, which is not recommended.")
|
|
||||||
return auth.PasswordKey(sshConfig.user, sshConfig.password, ssh.InsecureIgnoreHostKey())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyToRemote copies a file to a remote server via SCP
|
|
||||||
func CopyToRemote(fileName, remotePath string) error {
|
|
||||||
// Load environment variables
|
|
||||||
sshConfig, err := loadSSHConfig()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to load SSH configuration: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize SSH client config
|
|
||||||
clientConfig, err := createSSHClientConfig(sshConfig)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create SSH client config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new SCP client
|
|
||||||
client := scp.NewClient(fmt.Sprintf("%s:%s", sshConfig.hostName, sshConfig.port), &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")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the local file
|
|
||||||
filePath := filepath.Join(tmpPath, fileName)
|
|
||||||
file, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open file %s: %w", filePath, err)
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
// Copy file to the remote server
|
|
||||||
err = client.CopyFromFile(context.Background(), *file, filepath.Join(remotePath, fileName), "0655")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to copy file to remote server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyFromRemote(fileName, remotePath string) error {
|
|
||||||
// Load environment variables
|
|
||||||
sshConfig, err := loadSSHConfig()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to load SSH configuration: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize SSH client config
|
|
||||||
clientConfig, err := createSSHClientConfig(sshConfig)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create SSH client config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new SCP client
|
|
||||||
client := scp.NewClient(fmt.Sprintf("%s:%s", sshConfig.hostName, sshConfig.port), &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 {
|
|
||||||
utils.Error("Error while copying file %s ", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
||||||
51
pkg/var.go
51
pkg/var.go
@@ -1,23 +1,40 @@
|
|||||||
// Package pkg /
|
/*
|
||||||
/*****
|
MIT License
|
||||||
@author Jonas Kaninda
|
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
Copyright (c) 2023 Jonas Kaninda
|
||||||
@Copyright © 2024 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
|
package pkg
|
||||||
|
|
||||||
const cronLogFile = "/var/log/mysql-bkup.log"
|
|
||||||
const tmpPath = "/tmp/backup"
|
const tmpPath = "/tmp/backup"
|
||||||
const algorithm = "aes256"
|
|
||||||
const gpgHome = "/config/gnupg"
|
const gpgHome = "/config/gnupg"
|
||||||
const gpgExtension = "gpg"
|
const gpgExtension = "gpg"
|
||||||
const workingDir = "/config"
|
|
||||||
const timeFormat = "2006-01-02 at 15:04:05"
|
const timeFormat = "2006-01-02 at 15:04:05"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
storage = "local"
|
storage = "local"
|
||||||
file = ""
|
file = ""
|
||||||
|
|
||||||
storagePath = "/backup"
|
storagePath = "/backup"
|
||||||
|
workingDir = "/config"
|
||||||
disableCompression = false
|
disableCompression = false
|
||||||
encryption = false
|
encryption = false
|
||||||
usingKey = false
|
usingKey = false
|
||||||
@@ -34,7 +51,6 @@ var dbHVars = []string{
|
|||||||
}
|
}
|
||||||
var tdbRVars = []string{
|
var tdbRVars = []string{
|
||||||
"TARGET_DB_HOST",
|
"TARGET_DB_HOST",
|
||||||
"TARGET_DB_PORT",
|
|
||||||
"TARGET_DB_NAME",
|
"TARGET_DB_NAME",
|
||||||
"TARGET_DB_USERNAME",
|
"TARGET_DB_USERNAME",
|
||||||
"TARGET_DB_PASSWORD",
|
"TARGET_DB_PASSWORD",
|
||||||
@@ -43,12 +59,12 @@ var tdbRVars = []string{
|
|||||||
var dbConf *dbConfig
|
var dbConf *dbConfig
|
||||||
var targetDbConf *targetDbConfig
|
var targetDbConf *targetDbConfig
|
||||||
|
|
||||||
// sshHVars Required environment variables for SSH remote server storage
|
// sshVars Required environment variables for SSH remote server storage
|
||||||
var sshHVars = []string{
|
var sshVars = []string{
|
||||||
"SSH_USER",
|
"SSH_USER",
|
||||||
"REMOTE_PATH",
|
|
||||||
"SSH_HOST_NAME",
|
"SSH_HOST_NAME",
|
||||||
"SSH_PORT",
|
"SSH_PORT",
|
||||||
|
"REMOTE_PATH",
|
||||||
}
|
}
|
||||||
var ftpVars = []string{
|
var ftpVars = []string{
|
||||||
"FTP_HOST_NAME",
|
"FTP_HOST_NAME",
|
||||||
@@ -56,6 +72,11 @@ var ftpVars = []string{
|
|||||||
"FTP_PASSWORD",
|
"FTP_PASSWORD",
|
||||||
"FTP_PORT",
|
"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
|
// AwsVars Required environment variables for AWS S3 storage
|
||||||
var awsVars = []string{
|
var awsVars = []string{
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
<!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>
|
|
||||||
69
templates/email-error.tmpl
Normal file
69
templates/email-error.tmpl
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>🔴 Urgent: Database Backup Failure</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #d9534f;
|
||||||
|
}
|
||||||
|
.details {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.details ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.details li {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #0275d8;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>🔴 Urgent: Database Backup Failure Notification</h2>
|
||||||
|
<p>Hi,</p>
|
||||||
|
<p>An error occurred during the database backup process. Please review the details below and take the necessary actions:</p>
|
||||||
|
|
||||||
|
<div class="details">
|
||||||
|
<h3>Failure Details:</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Database Name:</strong> {{.DatabaseName}}</li>
|
||||||
|
<li><strong>Date:</strong> {{.EndTime}}</li>
|
||||||
|
<li><strong>Backup Reference:</strong> {{.BackupReference}}</li>
|
||||||
|
<li><strong>Error Message:</strong> {{.Error}}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>We recommend investigating the issue as soon as possible to prevent potential data loss or service disruptions.</p>
|
||||||
|
|
||||||
|
<p>For more information, visit the <a href="https://jkaninda.github.io/pg-bkup">pg-bkup documentation</a>.</p>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
© 2024 <a href="https://github.com/jkaninda/pg-bkup">pg-bkup</a> | Automated Backup System
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<!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>
|
|
||||||
70
templates/email.tmpl
Normal file
70
templates/email.tmpl
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>✅ Database Backup Successful – {{.Database}}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #5cb85c;
|
||||||
|
}
|
||||||
|
.details {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.details ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.details li {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #0275d8;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>✅ Database Backup Successful</h2>
|
||||||
|
<p>Hi,</p>
|
||||||
|
<p>The backup process for the <strong>{{.Database}}</strong> database was successfully completed. Please find the details below:</p>
|
||||||
|
|
||||||
|
<div class="details">
|
||||||
|
<h3>Backup Details:</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Database Name:</strong> {{.Database}}</li>
|
||||||
|
<li><strong>Backup Start Time:</strong> {{.StartTime}}</li>
|
||||||
|
<li><strong>Backup End Time:</strong> {{.EndTime}}</li>
|
||||||
|
<li><strong>Backup Storage:</strong> {{.Storage}}</li>
|
||||||
|
<li><strong>Backup Location:</strong> {{.BackupLocation}}</li>
|
||||||
|
<li><strong>Backup Size:</strong> {{.BackupSize}}</li>
|
||||||
|
<li><strong>Backup Reference:</strong> {{.BackupReference}}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>You can access the backup at the specified location if needed. Thank you for using <a href="https://jkaninda.github.io/mysql-bkup/">mysql-bkup</a>.</p>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
© 2024 <a href="https://github.com/jkaninda/mysql-bkup">mysql-bkup</a> | Automated Backup System
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
🔴 Urgent: Database Backup Failure Notification
|
|
||||||
Hi,
|
|
||||||
An error occurred during database backup.
|
|
||||||
Failure Details:
|
|
||||||
- Date: {{.EndTime}}
|
|
||||||
- Backup Reference: {{.BackupReference}}
|
|
||||||
- Error Message: {{.Error}}
|
|
||||||
|
|
||||||
11
templates/telegram-error.tmpl
Normal file
11
templates/telegram-error.tmpl
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
🔴 Urgent: Database Backup Failure Notification
|
||||||
|
|
||||||
|
Hi,
|
||||||
|
An error occurred during the database backup process.
|
||||||
|
Please review the details below and take the necessary actions:
|
||||||
|
Failure Details:
|
||||||
|
- Database Name: {{.DatabaseName}}
|
||||||
|
- Date: {{.EndTime}}
|
||||||
|
- Backup Reference: {{.BackupReference}}
|
||||||
|
- Error Message: {{.Error}}
|
||||||
|
We recommend investigating the issue as soon as possible to prevent potential data loss or service disruptions.
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[✅ 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}}
|
|
||||||
16
templates/telegram.tmpl
Normal file
16
templates/telegram.tmpl
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
✅ Database Backup Successful
|
||||||
|
|
||||||
|
Hi,
|
||||||
|
The backup process for the {{.Database}} database was successfully completed.
|
||||||
|
Please find the details below:
|
||||||
|
|
||||||
|
Backup Details:
|
||||||
|
- Database Name: {{.Database}}
|
||||||
|
- Backup Start Time: {{.StartTime}}
|
||||||
|
- Backup EndTime: {{.EndTime}}
|
||||||
|
- Backup Storage: {{.Storage}}
|
||||||
|
- Backup Location: {{.BackupLocation}}
|
||||||
|
- Backup Size: {{.BackupSize}}
|
||||||
|
- Backup Reference: {{.BackupReference}}
|
||||||
|
|
||||||
|
You can access the backup at the specified location if needed.
|
||||||
@@ -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 utils
|
package utils
|
||||||
|
|
||||||
import "os"
|
import "os"
|
||||||
@@ -13,7 +37,7 @@ type MailConfig struct {
|
|||||||
}
|
}
|
||||||
type NotificationData struct {
|
type NotificationData struct {
|
||||||
File string
|
File string
|
||||||
BackupSize int64
|
BackupSize string
|
||||||
Database string
|
Database string
|
||||||
StartTime string
|
StartTime string
|
||||||
EndTime string
|
EndTime string
|
||||||
@@ -26,6 +50,7 @@ type ErrorMessage struct {
|
|||||||
EndTime string
|
EndTime string
|
||||||
Error string
|
Error string
|
||||||
BackupReference string
|
BackupReference string
|
||||||
|
DatabaseName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadMailConfig gets mail environment variables and returns MailConfig
|
// loadMailConfig gets mail environment variables and returns MailConfig
|
||||||
@@ -57,3 +82,5 @@ func backupReference() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const templatePath = "/config/templates"
|
const templatePath = "/config/templates"
|
||||||
|
|
||||||
|
var DatabaseName = ""
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
// Package utils /
|
/*
|
||||||
/*****
|
MIT License
|
||||||
@author Jonas Kaninda
|
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
Copyright (c) 2023 Jonas Kaninda
|
||||||
@Copyright © 2024 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
|
package utils
|
||||||
|
|
||||||
const RestoreExample = "restore --dbname database --file db_20231219_022941.sql.gz\n" +
|
const RestoreExample = "restore --dbname database --file db_20231219_022941.sql.gz\n" +
|
||||||
@@ -14,3 +32,4 @@ const BackupExample = "backup --dbname database --disable-compression\n" +
|
|||||||
const MainExample = "mysql-bkup backup --dbname database --disable-compression\n" +
|
const MainExample = "mysql-bkup backup --dbname database --disable-compression\n" +
|
||||||
"backup --dbname database --storage s3 --path /custom-path\n" +
|
"backup --dbname database --storage s3 --path /custom-path\n" +
|
||||||
"restore --dbname database --file db_20231219_022941.sql.gz"
|
"restore --dbname database --file db_20231219_022941.sql.gz"
|
||||||
|
const traceLog = "trace"
|
||||||
|
|||||||
142
utils/logger.go
142
utils/logger.go
@@ -1,69 +1,103 @@
|
|||||||
// Package utils /
|
/*
|
||||||
/*****
|
MIT License
|
||||||
@author Jonas Kaninda
|
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
Copyright (c) 2023 Jonas Kaninda
|
||||||
@Copyright © 2024 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
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"runtime"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Info(msg string, args ...any) {
|
// Info returns info log
|
||||||
var currentTime = time.Now().Format("2006/01/02 15:04:05")
|
func Info(msg string, args ...interface{}) {
|
||||||
formattedMessage := fmt.Sprintf(msg, args...)
|
log.SetOutput(getStd("/dev/stdout"))
|
||||||
if len(args) == 0 {
|
logWithCaller("INFO", msg, args...)
|
||||||
fmt.Printf("%s INFO: %s\n", currentTime, msg)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s INFO: %s\n", currentTime, formattedMessage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn warning message
|
// Warn returns warning log
|
||||||
func Warn(msg string, args ...any) {
|
func Warn(msg string, args ...interface{}) {
|
||||||
var currentTime = time.Now().Format("2006/01/02 15:04:05")
|
log.SetOutput(getStd("/dev/stdout"))
|
||||||
formattedMessage := fmt.Sprintf(msg, args...)
|
logWithCaller("WARN", 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) {
|
|
||||||
var currentTime = time.Now().Format("2006/01/02 15:04:05")
|
|
||||||
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) {
|
|
||||||
var currentTime = time.Now().Format("2006/01/02 15:04:05")
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal logs an error message and exits the program
|
// Error logs error messages
|
||||||
func Fatal(msg string, args ...any) {
|
func Error(msg string, args ...interface{}) {
|
||||||
var currentTime = time.Now().Format("2006/01/02 15:04:05")
|
log.SetOutput(getStd("/dev/stderr"))
|
||||||
// Fatal logs an error message and exits the program.
|
logWithCaller("ERROR", msg, args...)
|
||||||
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)
|
|
||||||
NotifyError(formattedMessage)
|
|
||||||
|
|
||||||
|
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.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,7 +31,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/go-mail/mail"
|
"github.com/go-mail/mail"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -75,7 +99,7 @@ func sendMessage(msg string) error {
|
|||||||
Info("Telegram notification has been sent")
|
Info("Telegram notification has been sent")
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
body, _ := io.ReadAll(response.Body)
|
||||||
Error("Error could not send message, error: %s", string(body))
|
Error("Error could not send message, error: %s", string(body))
|
||||||
return fmt.Errorf("error could not send message %s", string(body))
|
return fmt.Errorf("error could not send message %s", string(body))
|
||||||
}
|
}
|
||||||
@@ -96,10 +120,10 @@ func NotifySuccess(notificationData *NotificationData) {
|
|||||||
"MAIL_TO",
|
"MAIL_TO",
|
||||||
}
|
}
|
||||||
|
|
||||||
//Email notification
|
// Email notification
|
||||||
err := CheckEnvVars(mailVars)
|
err := CheckEnvVars(mailVars)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
body, err := parseTemplate(*notificationData, "email.template")
|
body, err := parseTemplate(*notificationData, "email.tmpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error("Could not parse email template: %v", err)
|
Error("Could not parse email template: %v", err)
|
||||||
}
|
}
|
||||||
@@ -108,10 +132,10 @@ func NotifySuccess(notificationData *NotificationData) {
|
|||||||
Error("Could not send email: %v", err)
|
Error("Could not send email: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Telegram notification
|
// Telegram notification
|
||||||
err = CheckEnvVars(vars)
|
err = CheckEnvVars(vars)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
message, err := parseTemplate(*notificationData, "telegram.template")
|
message, err := parseTemplate(*notificationData, "telegram.tmpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error("Could not parse telegram template: %v", err)
|
Error("Could not parse telegram template: %v", err)
|
||||||
}
|
}
|
||||||
@@ -136,30 +160,32 @@ func NotifyError(error string) {
|
|||||||
"MAIL_TO",
|
"MAIL_TO",
|
||||||
}
|
}
|
||||||
|
|
||||||
//Email notification
|
// Email notification
|
||||||
err := CheckEnvVars(mailVars)
|
err := CheckEnvVars(mailVars)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
body, err := parseTemplate(ErrorMessage{
|
body, err := parseTemplate(ErrorMessage{
|
||||||
Error: error,
|
Error: error,
|
||||||
EndTime: time.Now().Format(TimeFormat()),
|
EndTime: time.Now().Format(TimeFormat()),
|
||||||
BackupReference: os.Getenv("BACKUP_REFERENCE"),
|
BackupReference: os.Getenv("BACKUP_REFERENCE"),
|
||||||
}, "email-error.template")
|
DatabaseName: DatabaseName,
|
||||||
|
}, "email-error.tmpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error("Could not parse error template: %v", err)
|
Error("Could not parse error template: %v", err)
|
||||||
}
|
}
|
||||||
err = SendEmail(fmt.Sprintf("🔴 Urgent: Database Backup Failure Notification"), body)
|
err = SendEmail("🔴 Urgent: Database Backup Failure Notification", body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error("Could not send email: %v", err)
|
Error("Could not send email: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Telegram notification
|
// Telegram notification
|
||||||
err = CheckEnvVars(vars)
|
err = CheckEnvVars(vars)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
message, err := parseTemplate(ErrorMessage{
|
message, err := parseTemplate(ErrorMessage{
|
||||||
Error: error,
|
Error: error,
|
||||||
EndTime: time.Now().Format(TimeFormat()),
|
EndTime: time.Now().Format(TimeFormat()),
|
||||||
BackupReference: os.Getenv("BACKUP_REFERENCE"),
|
BackupReference: os.Getenv("BACKUP_REFERENCE"),
|
||||||
}, "telegram-error.template")
|
DatabaseName: DatabaseName,
|
||||||
|
}, "telegram-error.tmpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error("Could not parse error template: %v", err)
|
Error("Could not parse error template: %v", err)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
// Package utils /
|
/*
|
||||||
/*****
|
MIT License
|
||||||
@author Jonas Kaninda
|
|
||||||
@license MIT License <https://opensource.org/licenses/MIT>
|
Copyright (c) 2023 Jonas Kaninda
|
||||||
@Copyright © 2024 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
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -17,6 +35,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var Version = ""
|
||||||
|
|
||||||
// FileExists checks if the file does exist
|
// FileExists checks if the file does exist
|
||||||
func FileExists(filename string) bool {
|
func FileExists(filename string) bool {
|
||||||
info, err := os.Stat(filename)
|
info, err := os.Stat(filename)
|
||||||
@@ -31,7 +51,13 @@ func WriteToFile(filePath, content string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func(file *os.File) {
|
||||||
|
err := file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
}(file)
|
||||||
|
|
||||||
_, err = file.WriteString(content)
|
_, err = file.WriteString(content)
|
||||||
return err
|
return err
|
||||||
@@ -49,14 +75,25 @@ func CopyFile(src, dst string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open source file: %v", err)
|
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
|
// Create the destination file
|
||||||
destinationFile, err := os.Create(dst)
|
destinationFile, err := os.Create(dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create destination file: %v", err)
|
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
|
// Copy the content from source to destination
|
||||||
_, err = io.Copy(destinationFile, sourceFile)
|
_, err = io.Copy(destinationFile, sourceFile)
|
||||||
@@ -83,7 +120,12 @@ func IsDirEmpty(name string) (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer func(f *os.File) {
|
||||||
|
err := f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(f)
|
||||||
|
|
||||||
_, err = f.Readdirnames(1)
|
_, err = f.Readdirnames(1)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -182,6 +224,7 @@ func GetIntEnv(envName string) int {
|
|||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnvWithDefault(envName string, defaultValue string) string {
|
func EnvWithDefault(envName string, defaultValue string) string {
|
||||||
value := os.Getenv(envName)
|
value := os.Getenv(envName)
|
||||||
if value == "" {
|
if value == "" {
|
||||||
@@ -202,13 +245,28 @@ func CronNextTime(cronExpr string) time.Time {
|
|||||||
// Parse the cron expression
|
// Parse the cron expression
|
||||||
schedule, err := cron.ParseStandard(cronExpr)
|
schedule, err := cron.ParseStandard(cronExpr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error("Error parsing cron expression:", err)
|
Error("Error parsing cron expression: %s", err)
|
||||||
return time.Time{}
|
return time.Time{}
|
||||||
}
|
}
|
||||||
// Get the current time
|
// Get the current time
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
// Get the next scheduled time
|
// Get the next scheduled time
|
||||||
next := schedule.Next(now)
|
next := schedule.Next(now)
|
||||||
//Info("The next scheduled time is: %v\n", next)
|
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConvertBytes converts bytes to a human-readable string with the appropriate unit (bytes, MiB, or GiB).
|
||||||
|
func ConvertBytes(bytes uint64) string {
|
||||||
|
const (
|
||||||
|
MiB = 1024 * 1024
|
||||||
|
GiB = MiB * 1024
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case bytes >= GiB:
|
||||||
|
return fmt.Sprintf("%.2f GiB", float64(bytes)/float64(GiB))
|
||||||
|
case bytes >= MiB:
|
||||||
|
return fmt.Sprintf("%.2f MiB", float64(bytes)/float64(MiB))
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d bytes", bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user