├── README.md ├── backup.sh └── config.json /README.md: -------------------------------------------------------------------------------- 1 | # M03ED Backup 2 | You Can Use This Script To Make Backup From `gorm` or `sqlalchemy` Database On Telegram And Discord. 3 | - MySQL, MariaDB and SQlite3 Are Supported. 4 | 5 | # Usage 6 | ## Step 1 7 | First You Need To Install `tar` And `curl`. 8 | ```bash 9 | apt install tar curl 10 | ``` 11 | Then Change The Directory. 12 | ```bash 13 | cd /opt 14 | ``` 15 | Download Project. 16 | ```bash 17 | git clone "https://github.com/M03ED/M03ED_Backup.git" 18 | ``` 19 | Enter Project Folder 20 | ```bash 21 | cd /opt/M03ED_Backup 22 | ``` 23 | Make A Folder For Temporary Files (You can change this path from config.json). 24 | ```bash 25 | mkdir temp 26 | ``` 27 | 28 | ## Step 2 29 | Set-up Your Config file. 30 | ```json 31 | { 32 | "backup_dir": "/opt/M03ED_Backup/temp", 33 | "backup_interval_time": 60, // interval per minutes 34 | "telegram": { 35 | "bot_token": "your-telegram-bot-token", // replace with telegram bot token, max to 50mb backup 36 | "chat_id": "your-chat-id" // replace with your telegram id, you can find it with https://t.me/username_to_id_bot 37 | }, 38 | "discord": { 39 | "backup_url": "your-discord-webhook-url" // replace with discord webhook, max to 10mb backup 40 | }, 41 | "databases": [ 42 | { 43 | "type": "mariadb", //can be mysql, sqlite or mariadb 44 | "env_path": "/opt/marzban/.env", 45 | "docker_path": "/opt/marzban/docker-compose.yml", 46 | "container_name": "mariadb", // database container name 47 | "url_format":"sqlalchemy", // can be sqlalchemy or gorm, use sqlalchemy for marzban 48 | "external": [ 49 | "/var/lib/marzban/certs", 50 | "/var/lib/marzban/templates", 51 | "/var/lib/marzban/xray_config.json" 52 | ] // any file or folder you need to add to backup file 53 | } 54 | ] // list of database's, you can add many as you want 55 | } 56 | ``` 57 | 58 | ## Step 3 59 | You Should Add Execute Permissions To The Script. 60 | ```bash 61 | chmod +x /opt/M03ED_Backup/backup.sh 62 | ``` 63 | 64 | ## Step 4 65 | Then Run The Program In `nohup` Mode To Stay Active In Background. 66 | ```bash 67 | nohup /opt/M03ED_Backup/backup.sh & 68 | ``` 69 | 70 | - Now You Have Your Backup On Telegram And Discord. 71 | - New File With `nohup.out` Name Gonna Be Created in `/opt/M03ED_Backup` And It Will Record Your Script Log , You Can Delete It When Ever You Want. 72 | -------------------------------------------------------------------------------- /backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CONFIG_FILE="config.json" 4 | 5 | get_config() { 6 | jq -r "$1 // \"\"" "$CONFIG_FILE" 7 | } 8 | 9 | get_env_var() { 10 | local env_file="$1" 11 | shift 12 | 13 | if [[ ! -f "$env_file" ]]; then 14 | echo "Error: Environment file '$env_file' not found." >&2 15 | return 1 16 | fi 17 | 18 | while [[ $# -gt 0 ]]; do 19 | local var_name="$1" 20 | 21 | local var_value=$(grep -E "^$var_name\s*=" "$env_file" | sed -E 's/^[^=]*=\s*//; s/^"//; s/"$//; s/^'"'"'//; s/'"'"'$//') 22 | 23 | if [[ -n "$var_value" ]]; then 24 | DB_URL="$var_value" 25 | return 0 26 | fi 27 | 28 | shift 29 | done 30 | 31 | echo "Error: No non-empty variable found in the environment file." >&2 32 | DB_URL="" 33 | return 1 34 | } 35 | 36 | # Global configurations 37 | BACKUP_DIR=$(get_config '.backup_dir') 38 | BACKUP_INTERVAL_TIME=$(get_config '.backup_interval_time') 39 | BOT_TOKEN=$(get_config '.telegram.bot_token') 40 | CHAT_ID=$(get_config '.telegram.chat_id') 41 | DISCORD_BACKUP_URL=$(get_config '.discord.backup_url') 42 | 43 | SLEEP_TIME=$((BACKUP_INTERVAL_TIME * 60)) 44 | 45 | log() { 46 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] - $1" 47 | } 48 | 49 | send_backup_to_telegram() { 50 | local file_path="$1" 51 | curl -F chat_id="$CHAT_ID" -F document=@"$file_path" "https://api.telegram.org/bot$BOT_TOKEN/sendDocument" 52 | } 53 | 54 | send_backup_to_discord() { 55 | local file_path="$1" 56 | local message="Here is your backup" 57 | 58 | echo 59 | echo "Sending Backup To Discord" 60 | curl -X POST -H "Content-Type: multipart/form-data" -F "content=$messege" -F "file=@$file_path" $DISCORD_BACKUP_URL 61 | } 62 | 63 | backup_sqlite() { 64 | local backup_name=$1 65 | local db_path=$2 66 | shift 67 | 68 | local additional_files=("$@") 69 | 70 | log "Starting SQLite backup for $backup_name..." 71 | 72 | FILE_NAME="$backup_name-$(date '+%Y-%m-%d_%H:%M').tar.gz" 73 | 74 | cp "$db_path" "$BACKUP_DIR/$backup_name.sqlite3" 75 | tar czvf "$BACKUP_DIR/$FILE_NAME" "$BACKUP_DIR/$backup_name.sqlite3" "${additional_files[@]}" 76 | 77 | send_backup_to_telegram "$BACKUP_DIR/$FILE_NAME" 78 | send_backup_to_discord "$BACKUP_DIR/$FILE_NAME" 79 | 80 | rm "$BACKUP_DIR/$backup_name.sqlite3" 81 | rm "$BACKUP_DIR/$FILE_NAME" 82 | 83 | log "SQLite backup for $db_name completed!" 84 | } 85 | 86 | backup_mysql() { 87 | local db_name=$1 88 | local backup_name=$2 89 | local container_name=$3 90 | local docker_path=$4 91 | local user=$5 92 | local password=$6 93 | shift 94 | local additional_files=("$@") 95 | 96 | log "Starting MySQL backup for $db_name..." 97 | FILE_NAME="$backup_name-$(date '+%Y-%m-%d_%H:%M').tar.gz" 98 | 99 | if ! output=$(docker compose -f "$docker_path" exec "$container_name" mysqldump -u root -p"$password" "$db_name" 2>&1 > "$BACKUP_DIR/db_backup.sql"); then 100 | if [[ "$output" == *"Enter password:"* || "$output" == *"Access denied"* ]]; then 101 | log "Error: Authentication failed for MySQL backup. Please check credentials." 102 | return 1 103 | else 104 | log "Error during MySQL backup: $output" 105 | return 1 106 | fi 107 | fi 108 | 109 | tar czvf "$BACKUP_DIR/$FILE_NAME" "$BACKUP_DIR/db_backup.sql" "$docker_path" "${additional_files[@]}" 110 | send_backup_to_telegram "$BACKUP_DIR/$FILE_NAME" 111 | send_backup_to_discord "$BACKUP_DIR/$FILE_NAME" 112 | 113 | rm "$BACKUP_DIR/db_backup.sql" 114 | rm "$BACKUP_DIR/$FILE_NAME" 115 | 116 | log "MySQL backup for $db_name completed!" 117 | return 0 118 | } 119 | 120 | backup_mariadb() { 121 | local db_name=$1 122 | local backup_name=$2 123 | local container_name=$3 124 | local docker_path=$4 125 | local user=$5 126 | local password=$6 127 | shift 128 | local additional_files=("$@") 129 | 130 | log "Starting MariaDB backup for $db_name..." 131 | FILE_NAME="$backup_name-$(date '+%Y-%m-%d_%H:%M').tar.gz" 132 | 133 | if ! output=$(docker compose -f "$docker_path" exec "$container_name" mariadb-dump -u"$user" -p"$password" "$db_name" 2>&1 > "$BACKUP_DIR/db_backup.sql"); then 134 | if [[ "$output" == *"Enter password:"* || "$output" == *"Access denied"* ]]; then 135 | log "Error: Authentication failed for MariaDB backup. Please check credentials." 136 | return 1 137 | else 138 | log "Error during MariaDB backup: $output" 139 | return 1 140 | fi 141 | fi 142 | 143 | tar czvf "$BACKUP_DIR/$FILE_NAME" "$BACKUP_DIR/db_backup.sql" "$env_path" "$docker_path" "${additional_files[@]}" 144 | send_backup_to_telegram "$BACKUP_DIR/$FILE_NAME" 145 | send_backup_to_discord "$BACKUP_DIR/$FILE_NAME" 146 | 147 | rm "$BACKUP_DIR/db_backup.sql" 148 | rm "$BACKUP_DIR/$FILE_NAME" 149 | 150 | log "MariaDB backup for $db_name completed!" 151 | return 0 152 | } 153 | 154 | 155 | parse_sqlalchemy_url() { 156 | local input_string="$1" 157 | 158 | local credentials=$(echo "$input_string" | sed -n 's/.*mysql:\/\/\(.*\)@.*/\1/p') 159 | 160 | local username=$(echo "$credentials" | cut -d ':' -f 1) 161 | local pass=$(echo "$credentials" | cut -d ':' -f 2) 162 | local db=$(echo "$input_string" | sed -n 's/.*\/\([^/]*\)$/\1/p') 163 | 164 | user="$username" 165 | password="$pass" 166 | database="$db" 167 | } 168 | 169 | parse_gorm_url() { 170 | local input_string="$1" 171 | 172 | local username=$(echo "$input_string" | cut -d ':' -f 1) 173 | 174 | local username=$(echo "$input_string" | sed -n 's/^\([^:]*\):.*@tcp.*/\1/p') 175 | local pass=$(echo "$input_string" | sed -n 's/^[^:]*:\([^@]*\)@tcp.*/\1/p') 176 | local db=$(echo "$input_string" | sed -n 's/.*\/\([^?]*\).*/\1/p' | cut -d '?' -f 1) 177 | db=$(echo "$db" | cut -d '?' -f 1) 178 | 179 | user="$username" 180 | password="$pass" 181 | database="$db" 182 | } 183 | 184 | 185 | process_database() { 186 | local index=$1 187 | 188 | local DB_NAME=$(get_config ".databases[$index].db_name") 189 | local DB_TYPE=$(get_config ".databases[$index].type") 190 | local ENV_PATH=$(get_config ".databases[$index].env_path") 191 | local CONTAINER_NAME=$(get_config ".databases[$index].container_name") 192 | local DOCKER_PATH=$(get_config ".databases[$index].docker_path") 193 | local URL_FORMAT=$(get_config ".databases[$index].url_format") 194 | 195 | get_env_var $ENV_PATH "DATABASE_URL" "SQLALCHEMY_DATABASE_URL" 196 | local EXTERNAL_PATHS=$(jq -r ".databases[$index].external | join(\" \")" "$CONFIG_FILE") 197 | 198 | if [[ $DB_TYPE == "sqlite" ]]; then 199 | DB_URL="${DB_URL#sqlite:///}" 200 | else 201 | parse_sqlalchemy_url "$DB_URL" 202 | fi 203 | 204 | if [[ $DB_TYPE == "sqlite" ]]; then 205 | DB_URL="${DB_URL#sqlite:///}" 206 | else 207 | case $URL_FORMAT in 208 | "sqlalchemy") 209 | parse_sqlalchemy_url "$DB_URL" 210 | ;; 211 | "gorm") 212 | parse_gorm_url "$DB_URL" 213 | ;; 214 | *) 215 | log "Unsupported database type: $DB_TYPE" 216 | ;; 217 | esac 218 | fi 219 | 220 | if [[ -z "$DB_NAME" ]]; then 221 | DB_NAME="$database" 222 | fi 223 | 224 | case $DB_TYPE in 225 | "sqlite") 226 | backup_sqlite "$DB_NAME" "$SQLALCHEMY_DATABASE_URL" "$ENV_PATH" "$DOCKER_PATH" $EXTERNAL_PATHS 227 | ;; 228 | "mysql") 229 | backup_mysql "$database" "$DB_NAME" "$CONTAINER_NAME" "$DOCKER_PATH" "$user" "$password" "$ENV_PATH" $EXTERNAL_PATHS 230 | ;; 231 | "mariadb") 232 | backup_mariadb "$database" "$DB_NAME" "$CONTAINER_NAME" "$DOCKER_PATH" "$user" "$password" "$ENV_PATH" $EXTERNAL_PATHS 233 | ;; 234 | *) 235 | log "Unsupported database type: $DB_TYPE" 236 | ;; 237 | esac 238 | } 239 | 240 | # Main loop 241 | while true; do 242 | DATABASE_COUNT=$(jq '.databases | length' "$CONFIG_FILE") 243 | 244 | for ((i = 0; i < DATABASE_COUNT; i++)); do 245 | process_database "$i" 246 | done 247 | 248 | log "Sleeping for $SLEEP_TIME seconds..." 249 | sleep "$SLEEP_TIME" 250 | done -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "backup_dir": "/opt/Marzban_Backup/temp", 3 | "backup_interval_time": 60, 4 | "telegram": { 5 | "bot_token": "your-telegram-bot-token", 6 | "chat_id": "your-chat-id" 7 | }, 8 | "discord": { 9 | "backup_url": "your-discord-webhook-url" 10 | }, 11 | "databases": [ 12 | { 13 | "type": "mariadb", 14 | "env_path": "/opt/marzban/.env", 15 | "docker_path": "/opt/marzban/docker-compose.yml", 16 | "container_name": "mariadb", 17 | "url_format":"sqlalchemy", 18 | "external": [ 19 | "/var/lib/marzban/certs", 20 | "/var/lib/marzban/templates", 21 | "/var/lib/marzban/xray_config.json" 22 | ] 23 | }, 24 | { 25 | "type": "sqlite", 26 | "db_name":"test", 27 | "env_path": "/opt/marzban/.env", 28 | "docker_path": "/opt/marzban/docker-compose.yml", 29 | "container_name": "", 30 | "external": [ 31 | "/var/lib/marzban/certs", 32 | "/var/lib/marzban/templates", 33 | "/var/lib/marzban/xray_config.json" 34 | ] 35 | }, 36 | { 37 | "type": "mysql", 38 | "env_path": "/opt/marzban/.env", 39 | "docker_path": "/opt/marzban/docker-compose.yml", 40 | "container_name": "mysql", 41 | "url_format":"gorm", 42 | "external": [ 43 | "/var/lib/marzban/certs", 44 | "/var/lib/marzban/templates", 45 | "/var/lib/marzban/xray_config.json" 46 | ] 47 | } 48 | ] 49 | } 50 | --------------------------------------------------------------------------------