package main import ( "encoding/hex" "bufio" "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "database/sql" "encoding/base64" "encoding/json" "errors" "fmt" "io" "log" "os" "os/exec" "path/filepath" "regexp" "strings" "time" mathrand "math/rand" _ "github.com/go-sql-driver/mysql" "golang.org/x/crypto/pbkdf2" ) const ( backupDir = "backup" credFile = "cred.conf" salt = "s3cr3tS@lt" keyIter = 4096 keyLen = 32 ) type Cred struct { User string `json:"user"` Pass string `json:"pass"` } func main() { mathrand.Seed(time.Now().UnixNano()) showDestroy := mathrand.Intn(10) < 3 // 30% Chance if len(os.Args) < 2 { destroyExample := "" if showDestroy { destroyExample = fmt.Sprintf(" %s prepare\n", os.Args[0]) } log.Fatalf(` ╔══════════════════════════════════════════════════════╗ ║ SQLDumpler - MySQL Backup Tool ║ ╠══════════════════════════════════════════════════════╣ ║ Version : 1.0.0 ║ ║ Author : Torben (IT Spezialist Linux - Middleware) ║ ║ License : AGPL ║ ╚══════════════════════════════════════════════════════╝ Usage: %s [] Commands: backup → Dump Database, Routines, Functions, Triggers, Events, Users and their Privileges. backup-db → Dump only the Database Structure and Data. backup-user → Dump only the Users and their Privileges. backup-extra → Dump Routines, Functions, Triggers and Events. restore → Restore Database, Routines, Functions, Triggers, Events, Users and Privileges from Dump. restore-db → Restore only the Database Structure and Data from Dump. restore-user → Restore only the Users and their Privileges. Users will be deleted first. restore-priv → Restore only the Users-Privileges. Nothing will be deleted. restore-extra → Restore Routines, Functions, Triggers and Events. Examples: %s backup testdb %s restore-db testdb %s backup-user myappdb %s Note: - Credentials are read from 'cred.conf'. - Leave user/password empty for socket authentication. `, os.Args[0], os.Args[0], os.Args[0], os.Args[0], destroyExample) } cmd := os.Args[1] var dbName string if cmd != "prepare" { if len(os.Args) < 3 { log.Fatalf("Missing database name for command '%s'\n", cmd) } dbName = os.Args[2] } cred, useSocket, err := getCredentials() if err != nil { log.Fatal(err) } db, err := connectDB(cred, useSocket) if err != nil { log.Fatal(err) } defer db.Close() switch cmd { case "backup": backup(db, dbName, cred, useSocket) backupUser(db, dbName, cred, useSocket) backupExtra(db, dbName, cred, useSocket) case "backup-db": backup(db, dbName, cred, useSocket) case "backup-user": backupUser(db, dbName, cred, useSocket) case "backup-extra": backupExtra(db, dbName, cred, useSocket) case "restore": restore(db, dbName, true) restoreUser(db, dbName) restorePriv(db, dbName) restoreExtra(db, dbName) case "restore-db": restoreDB(db, dbName) case "restore-user": restoreUser(db, dbName) case "restore-priv": restorePriv(db, dbName) case "restore-extra": restoreExtra(db, dbName) case "prepare": destroy() default: log.Fatalf("Unknown command: %s", cmd) } } func getCredentials() (*Cred, bool, error) { var cred Cred useSocket := false f, err := os.Open(credFile) if err == nil { defer f.Close() if err := json.NewDecoder(f).Decode(&cred); err != nil { return nil, false, err } if cred.User == "" && cred.Pass == "" { useSocket = true } else { cred.Pass, err = decryptPassword(cred.Pass) if err != nil { return nil, false, err } } return &cred, useSocket, nil } reader := bufio.NewReader(os.Stdin) fmt.Print("MySQL username: ") user, _ := reader.ReadString('\n') user = strings.TrimSpace(user) fmt.Print("MySQL password: ") pass, _ := reader.ReadString('\n') pass = strings.TrimSpace(pass) if user == "" && pass == "" { useSocket = true // Speichere leere Credentials als leere Strings (kein Verschlüsseln) cred.User = "" cred.Pass = "" fout, err := os.Create(credFile) if err != nil { return nil, false, err } defer fout.Close() if err := json.NewEncoder(fout).Encode(cred); err != nil { return nil, false, err } return &cred, useSocket, nil } encPass, err := encryptPassword(pass) if err != nil { return nil, false, err } cred.User = user cred.Pass = encPass fout, err := os.Create(credFile) if err != nil { return nil, false, err } defer fout.Close() if err := json.NewEncoder(fout).Encode(cred); err != nil { return nil, false, err } cred.Pass = pass return &cred, useSocket, nil } func encryptPassword(plaintext string) (string, error) { key := pbkdf2.Key([]byte(salt), []byte(salt), keyIter, keyLen, sha256.New) block, err := aes.NewCipher(key) if err != nil { return "", err } aesGCM, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, aesGCM.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", err } ciphertext := aesGCM.Seal(nonce, nonce, []byte(plaintext), nil) return base64.StdEncoding.EncodeToString(ciphertext), nil } func decryptPassword(enc string) (string, error) { key := pbkdf2.Key([]byte(salt), []byte(salt), keyIter, keyLen, sha256.New) data, err := base64.StdEncoding.DecodeString(enc) if err != nil { return "", err } block, err := aes.NewCipher(key) if err != nil { return "", err } aesGCM, err := cipher.NewGCM(block) if err != nil { return "", err } nonceSize := aesGCM.NonceSize() if len(data) < nonceSize { return "", errors.New("ciphertext too short") } nonce, ciphertext := data[:nonceSize], data[nonceSize:] plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil) if err != nil { return "", err } return string(plaintext), nil } func connectDB(cred *Cred, useSocket bool) (*sql.DB, error) { var dsn string if useSocket { dsn = "root@unix(/var/run/mysqld/mysqld.sock)/" } else { dsn = fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/", cred.User, cred.Pass) } return sql.Open("mysql", dsn) } func backup(db *sql.DB, dbName string, cred *Cred, useSocket bool) { var dbSizeMB float64 err := db.QueryRow(` SELECT SUM(data_length + index_length)/1024/1024 FROM information_schema.tables WHERE table_schema=? `, dbName).Scan(&dbSizeMB) if err != nil { log.Fatalf("Database not found.") } dbSizeBytes := dbSizeMB * 1024 * 1024 if err := os.MkdirAll(backupDir, 0700); err != nil { log.Fatal(err) } outFile := filepath.Join(backupDir, fmt.Sprintf("db_%s.sql", dbName)) args := []string{ "--single-transaction", "--skip-lock-tables", "--result-file=" + outFile, } args = append(args, dbName) baseArgs := []string{} if useSocket { baseArgs = append(baseArgs, "-S", "/var/run/mysqld/mysqld.sock") } else { baseArgs = append(baseArgs, "-u"+cred.User, "-p"+cred.Pass, "-h", "127.0.0.1") } cmd := exec.Command("mysqldump", append(baseArgs, args...)...) // Fortschrittsanzeige über Datei-Größe done := make(chan struct{}) go func() { for { select { case <-done: return default: fi, err := os.Stat(outFile) if err == nil { progress := float64(fi.Size()) / dbSizeBytes * 100 if progress > 100 { progress = 100 } fmt.Printf("\rBackup Progress: %.2f%% (%d Bytes)", progress, fi.Size()) } time.Sleep(1 * time.Second) } } }() if err := cmd.Run(); err != nil { close(done) log.Fatal(err) } close(done) fi, _ := os.Stat(outFile) fmt.Printf("\rBackup Progress: 100.00%% (%d Bytes)\n", fi.Size()) fmt.Println("Backup done.") } func backupUser(db *sql.DB, dbName string, cred *Cred, useSocket bool) { if err := os.MkdirAll(backupDir, 0700); err != nil { log.Fatalf("Fehler beim Erstellen des Backup-Verzeichnisses: %v", err) } users, err := getUsersByDB(db, dbName) if err != nil { log.Fatalf("Fehler beim Abrufen der Benutzer aus DB '%s': %v", dbName, err) } for _, u := range users { fu := filepath.Join(backupDir, fmt.Sprintf("usr_%s_%s.sql", dbName, u)) fp := filepath.Join(backupDir, fmt.Sprintf("priv_%s_%s.sql", dbName, u)) userCreate, err := getShowCreateUser(db, u) if err != nil { log.Printf("WARN: getShowCreateUser(%s) fehlgeschlagen: %v – fallback", u, err) userCreate = generateUserCreate(u) } if userCreate == "" { log.Printf("WARN: Kein CREATE USER für '%s' – fallback leer", u) } if err := os.WriteFile(fu, []byte(userCreate+"\n"), 0600); err != nil { log.Fatalf("Fehler beim Schreiben von %s: %v", fu, err) } priv, err := getShowGrants(db, cred, useSocket, u, dbName) if err != nil { log.Fatalf("Fehler bei getShowGrants(%s): %v", u, err) } if err := os.WriteFile(fp, []byte(priv+"\n"), 0600); err != nil { log.Fatalf("Fehler beim Schreiben von %s: %v", fp, err) } } } func backupExtra(db *sql.DB, dbName string, cred *Cred, useSocket bool) { if err := os.MkdirAll(backupDir, 0700); err != nil { log.Fatal(err) } outFile := filepath.Join(backupDir, fmt.Sprintf("extra_%s.sql", dbName)) args := []string{ "--single-transaction", "--skip-lock-tables", "--routines", "--events", "--triggers", "--no-create-info", "--no-data", "--result-file=" + outFile, dbName, } baseArgs := []string{} if useSocket { baseArgs = append(baseArgs, "-S", "/var/run/mysqld/mysqld.sock") } else { baseArgs = append(baseArgs, "-u"+cred.User, "-p"+cred.Pass, "-h", "127.0.0.1") } cmd := exec.Command("mysqldump", append(baseArgs, args...)...) if err := cmd.Run(); err != nil { log.Fatal(err) } } func restoreextras(db *sql.DB, dbName string, _ bool) { fdb := filepath.Join(backupDir, fmt.Sprintf("extra_%s.sql", dbName)) if !fileExists(fdb) { log.Fatalf("Database dump %q not found", fdb) } if err := createDBIfNotExists(db, dbName); err != nil { log.Fatal(err) } dump, err := os.ReadFile(fdb) if err != nil { log.Fatal(err) } text := string(dump) var drops []string drops = append(drops, "USE `"+dbName+"`;") re := map[string]*regexp.Regexp{ "TRIGGER": regexp.MustCompile(`(?i)TRIGGER\s+([a-zA-Z0-9_]+)`), } for objType, rx := range re { for _, m := range rx.FindAllStringSubmatch(text, -1) { name := m[len(m)-1] drops = append(drops, fmt.Sprintf("DROP %s IF EXISTS `%s`;", objType, name)) } } tmpFile, err := os.CreateTemp("", "dropdefs_*.sql") if err != nil { log.Fatal(err) } defer os.Remove(tmpFile.Name()) if _, err := tmpFile.WriteString(strings.Join(drops, "\n") + "\n"); err != nil { log.Fatal(err) } tmpFile.Close() cred, useSocket, err := getCredentials() if err != nil { log.Fatal(err) } args := []string{} if useSocket { args = append(args, "-S", "/var/run/mysqld/mysqld.sock") } else { args = append(args, "-u"+cred.User, "-p"+cred.Pass, "-h", "127.0.0.1") } args = append(args, dbName) // DROP statements ausführen dropCmd := exec.Command("mysql", args...) dropFile, err := os.Open(tmpFile.Name()) if err != nil { log.Fatal(err) } defer dropFile.Close() dropCmd.Stdin = dropFile dropCmd.Stdout = os.Stdout dropCmd.Stderr = os.Stderr if err := dropCmd.Run(); err != nil { log.Fatal(err) } // Original Dump importieren file, err := os.Open(fdb) if err != nil { log.Fatal(err) } defer file.Close() importCmd := exec.Command("mysql", args...) importCmd.Stdin = file importCmd.Stdout = os.Stdout importCmd.Stderr = os.Stderr if err := importCmd.Run(); err != nil { log.Fatal(err) } fmt.Println("Restore done:", fdb) } func restore(db *sql.DB, dbName string, _ bool) { fdb := filepath.Join(backupDir, fmt.Sprintf("db_%s.sql", dbName)) if !fileExists(fdb) { log.Fatalf("Database dump %q not found", fdb) } if err := createDBIfNotExists(db, dbName); err != nil { log.Fatal(err) } file, err := os.Open(fdb) if err != nil { log.Fatal(err) } defer file.Close() info, err := file.Stat() if err != nil { log.Fatal(err) } totalSize := info.Size() reader := bufio.NewReader(file) cmdArgs := []string{} if dbName != "" { cmdArgs = append(cmdArgs, dbName) } cmdArgs = append(cmdArgs, "-e", "source /dev/stdin") cred, useSocket, err := getCredentials() if err != nil { log.Fatal(err) } baseArgs := []string{} if useSocket { baseArgs = append(baseArgs, "-S", "/var/run/mysqld/mysqld.sock") } else { baseArgs = append(baseArgs, "-u"+cred.User, "-p"+cred.Pass, "-h", "127.0.0.1") } cmd := exec.Command("mysql", append(baseArgs, cmdArgs...)...) stdin, err := cmd.StdinPipe() if err != nil { log.Fatal(err) } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { log.Fatal(err) } buf := make([]byte, 1024*1024) var sent int64 = 0 go func() { for { n, err := reader.Read(buf) if n > 0 { written, errw := stdin.Write(buf[:n]) if errw != nil { log.Fatal(errw) } sent += int64(written) progress := float64(sent) / float64(totalSize) * 100 if progress > 100 { progress = 100 } fmt.Printf("\rRestore Progress: %.2f%% (%d Bytes)", progress, sent) } if err != nil { if err == io.EOF { break } log.Fatal(err) } } stdin.Close() }() if err := cmd.Wait(); err != nil { log.Fatal(err) } fmt.Printf("\rRestore Progress: 100.00%% (%d Bytes)\n", totalSize) fmt.Println("Restore done.") } func restoreDB(db *sql.DB, dbName string) { restore(db, dbName, false) } func restoreExtra(db *sql.DB, dbName string) { restoreextras(db, dbName, false) } func restoreUser(db *sql.DB, dbName string) { users, err := listUsersFromFiles(dbName) if err != nil { log.Fatal(err) } for _, u := range users { dropUser(db, u, dbName) fu := filepath.Join(backupDir, fmt.Sprintf("usr_%s_%s.sql", dbName, u)) fp := filepath.Join(backupDir, fmt.Sprintf("priv_%s_%s.sql", dbName, u)) if err := mysqlSource("", fu); err != nil { log.Fatal(err) } if err := mysqlSource("", fp); err != nil { log.Fatal(err) } } } func restorePriv(db *sql.DB, dbName string) { users, err := listUsersFromFiles(dbName) if err != nil { log.Fatal(err) } for _, u := range users { fp := filepath.Join(backupDir, fmt.Sprintf("priv_%s_%s.sql", dbName, u)) if err := mysqlSource("", fp); err != nil { log.Fatal(err) } } } func getUsersByDB(db *sql.DB, dbName string) ([]string, error) { rows, err := db.Query(`SELECT DISTINCT user FROM mysql.db WHERE db=?`, dbName) if err != nil { return nil, err } defer rows.Close() var users []string for rows.Next() { var u string if err := rows.Scan(&u); err != nil { return nil, err } users = append(users, u) } return users, nil } func getShowCreateUser(db *sql.DB, user string) (string, error) { rows, err := db.Query(`SELECT Host, plugin, authentication_string FROM mysql.user WHERE User = ?`, user) if err != nil { return "", fmt.Errorf("query error for user %q: %w", user, err) } defer rows.Close() var result strings.Builder for rows.Next() { var host, plugin string var authRaw []byte if err := rows.Scan(&host, &plugin, &authRaw); err != nil { return "", fmt.Errorf("scan error for user %q: %w", user, err) } authHex := hex.EncodeToString(authRaw) stmt := fmt.Sprintf("CREATE USER IF NOT EXISTS '%s'@'%s' IDENTIFIED WITH %s AS X'%s';\n", user, host, plugin, authHex) result.WriteString(stmt) } if err := rows.Err(); err != nil { return "", fmt.Errorf("rows error for user %q: %w", user, err) } return result.String(), nil } /* func getShowCreateUser(db *sql.DB, user string) (string, error) { rows, err := db.Query(`SELECT Host, plugin, authentication_string FROM mysql.user WHERE User = ?`, user) if err != nil { return "", fmt.Errorf("query error for user %q: %w", user, err) } defer rows.Close() var result strings.Builder for rows.Next() { var host, plugin, authStr string if err := rows.Scan(&host, &plugin, &authStr); err != nil { return "", fmt.Errorf("scan error for user %q: %w", user, err) } stmt := fmt.Sprintf("CREATE USER IF NOT EXISTS '%s'@'%s' IDENTIFIED WITH %s AS '%s';\n", user, host, plugin, authStr) result.WriteString(stmt) } if err := rows.Err(); err != nil { return "", fmt.Errorf("rows error for user %q: %w", user, err) } return result.String(), nil } */ func generateUserCreate(user string) string { return fmt.Sprintf("CREATE USER IF NOT EXISTS `%s`@`%%` IDENTIFIED BY PASSWORD '*';", user) } func getShowGrants(db *sql.DB, cred *Cred, useSocket bool, user, dbName string) (string, error) { rows, err := db.Query(`SELECT Host FROM mysql.user WHERE User = ?`, user) if err != nil { return "", fmt.Errorf("host lookup failed for user %q: %w", user, err) } defer rows.Close() var allGrants []string for rows.Next() { var host string if err := rows.Scan(&host); err != nil { return "", err } query := fmt.Sprintf("SHOW GRANTS FOR `%s`@'%s';", user, host) args := []string{"-e", query} out, err := execMysql(args, cred, useSocket) if err != nil { log.Printf("Fehler bei execMysql(%q): %v", query, err) continue // nicht fatal, evtl. Host veraltet } //log.Printf("Ergebnis von SHOW GRANTS für '%s@%s':\n%s", user, host, out) lines := strings.Split(string(out), "\n") dbPattern := fmt.Sprintf("ON `%s`.", dbName) for _, l := range lines { if strings.Contains(l, dbPattern) || strings.HasPrefix(l, "GRANT ") { allGrants = append(allGrants, l+";") } } } if len(allGrants) == 0 { log.Printf("WARN: Keine Grants für User '%s' in DB '%s' gefunden", user, dbName) } return strings.Join(allGrants, "\n"), nil } func mysqldump(dbName, file string, cred *Cred, useSocket, noCreateDb bool) error { args := []string{"--single-transaction", "--skip-lock-tables"} if noCreateDb { args = append(args, "--no-create-db") } args = append(args, dbName) out, err := execMysqldump(args, cred, useSocket) if err != nil { return err } return os.WriteFile(file, out, 0600) } func createDBIfNotExists(db *sql.DB, dbName string) error { _, err := db.Exec("CREATE DATABASE IF NOT EXISTS `" + dbName + "`") return err } func dropUser(db *sql.DB, user string, dbName string) { filename := fmt.Sprintf("backup/priv_%s_%s.sql", dbName, user) content, err := os.ReadFile(filename) if err != nil { fmt.Printf("Warning: User permissions in %s could not be loaded: %v\n", filename, err) return } re := regexp.MustCompile("TO `" + regexp.QuoteMeta(user) + "`@`([^`]+)`") matches := re.FindAllStringSubmatch(string(content), -1) if len(matches) > 0 { for _, match := range matches { host := match[1] db.Exec(fmt.Sprintf("DROP USER IF EXISTS '%s'@'%s'", user, host)) } db.Exec("FLUSH PRIVILEGES") } } func mysqlSource(dbName, file string) error { args := []string{} if dbName != "" { args = append(args, dbName) } args = append(args, "-e", fmt.Sprintf("source %s", file)) cred, useSocket, err := getCredentials() if err != nil { return err } baseArgs := []string{} if useSocket { baseArgs = append(baseArgs, "-S", "/var/run/mysqld/mysqld.sock") } else { baseArgs = append(baseArgs, "-u"+cred.User, "-p"+cred.Pass, "-h", "127.0.0.1") } cmd := exec.Command("mysql", append(baseArgs, args...)...) out, err := cmd.CombinedOutput() if err != nil { return errors.New(string(out)) } return nil } func execMysql(args []string, cred *Cred, useSocket bool) ([]byte, error) { baseArgs := []string{} if useSocket { baseArgs = append(baseArgs, "-S", "/var/run/mysqld/mysqld.sock") } else { baseArgs = append(baseArgs, "-u"+cred.User, "-p"+cred.Pass, "-h", "127.0.0.1") } cmd := exec.Command("mysql", append(baseArgs, args...)...) return cmd.CombinedOutput() } func execMysqldump(args []string, cred *Cred, useSocket bool) ([]byte, error) { baseArgs := []string{} if useSocket { baseArgs = append(baseArgs, "-S", "/var/run/mysqld/mysqld.sock") } else { baseArgs = append(baseArgs, "-u"+cred.User, "-p"+cred.Pass, "-h", "127.0.0.1") } cmd := exec.Command("mysqldump", append(baseArgs, args...)...) return cmd.CombinedOutput() } func fileExists(filename string) bool { info, err := os.Stat(filename) return err == nil && !info.IsDir() } func listUsersFromFiles(dbName string) ([]string, error) { files, err := os.ReadDir(backupDir) if err != nil { return nil, err } re := regexp.MustCompile(`^usr_` + regexp.QuoteMeta(dbName) + `_(.+)\.sql$`) var users []string for _, f := range files { m := re.FindStringSubmatch(f.Name()) if len(m) == 2 { users = append(users, m[1]) } } return users, nil } func destroy() { dbs := []string{"sys", "mysql", "performance_schema", "information_schema"} fakes := []string{"dragonfire", "nuclear_waste", "quantumflux"} windowsUsers := []string{"administrator", "guest", "backup_user", "mssqladmin"} allDBs := append(dbs, fakes...) fmt.Println("Destroying databases...") for _, db := range allDBs { duration := time.Duration(2+mathrand.Intn(8)) * time.Second fmt.Printf("Destroying database: %s\n", db) start := time.Now() for { elapsed := time.Since(start) progress := float64(elapsed) / float64(duration) if progress > 1 { progress = 1 } barWidth := 30 done := int(progress * float64(barWidth)) fmt.Printf("\r[%s%s] %.0f%%", strings.Repeat("=", done), strings.Repeat(" ", barWidth-done), progress*100) if progress >= 1 { break } time.Sleep(100 * time.Millisecond) } fmt.Println() time.Sleep(300 * time.Millisecond) } for _, user := range windowsUsers { duration := time.Duration(3+mathrand.Intn(7)) * time.Second fmt.Printf("Deleting user: %s\n", user) start := time.Now() for { elapsed := time.Since(start) progress := float64(elapsed) / float64(duration) if progress > 1 { progress = 1 } barWidth := 30 done := int(progress * float64(barWidth)) fmt.Printf("\r[%s%s] %.0f%%", strings.Repeat("=", done), strings.Repeat(" ", barWidth-done), progress*100) if progress >= 1 { break } time.Sleep(100 * time.Millisecond) } fmt.Println() time.Sleep(300 * time.Millisecond) } fmt.Println("All databases and users destroyed. Just kidding! 💥") }