Final debug pass
This commit is contained in:
@ -50,6 +50,8 @@ dbbackup interactive --database your_database
|
||||
dbbackup interactive --database postgres --host localhost --user postgres
|
||||
```
|
||||
|
||||
> Tip: In the interactive menu, tap the left/right arrows (or `t`) to toggle between PostgreSQL and MySQL/MariaDB before starting a task.
|
||||
|
||||
### 📊 Command Line with Progress
|
||||
```bash
|
||||
# Single backup with live progress
|
||||
@ -118,6 +120,9 @@ dbbackup status --db-type postgres
|
||||
# Single database backup
|
||||
dbbackup backup single myapp_db --db-type mysql
|
||||
|
||||
# Using the short flag for database selection
|
||||
dbbackup backup single myapp_db -d mysql
|
||||
|
||||
# Sample backup
|
||||
dbbackup backup sample myapp_db --sample-ratio 10 --db-type mysql
|
||||
|
||||
@ -145,7 +150,7 @@ dbbackup backup cluster --jobs 16 --dump-jobs 8 --max-cores 32
|
||||
| `--host` | Database host | `--host db.example.com` |
|
||||
| `--port` | Database port | `--port 5432` |
|
||||
| `--user` | Database user | `--user backup_user` |
|
||||
| `--db-type` | Database type | `--db-type mysql` |
|
||||
| `-d`, `--db-type` | Database type (`postgres`, `mysql`, `mariadb`) | `-d mysql` |
|
||||
| `--insecure` | Disable SSL | `--insecure` |
|
||||
| `--jobs` | Parallel jobs | `--jobs 8` |
|
||||
| `--debug` | Debug mode | `--debug` |
|
||||
|
||||
12
README.md
12
README.md
@ -111,6 +111,8 @@ dbbackup menu --database postgres --host localhost --user postgres
|
||||
dbbackup ui --database myapp_db --progress
|
||||
```
|
||||
|
||||
> 💡 In the interactive menu, use the left/right arrow keys (or press `t`) to switch the target engine between PostgreSQL and MySQL/MariaDB before launching an operation.
|
||||
|
||||
### Enhanced Progress Tracking Commands
|
||||
|
||||
#### Real-Time Progress Monitoring
|
||||
@ -160,6 +162,9 @@ dbbackup restore backup.dump --progress --verify --show-steps
|
||||
# Single database backup (auto-optimized for your CPU)
|
||||
dbbackup backup single myapp_db --db-type postgres
|
||||
|
||||
# MySQL/MariaDB backup using the short flag
|
||||
dbbackup backup single myapp_db -d mysql --host mysql.example.com --port 3306
|
||||
|
||||
# Sample backup (10% of data)
|
||||
dbbackup backup sample myapp_db --sample-ratio 10
|
||||
|
||||
@ -245,7 +250,7 @@ dbbackup cpu
|
||||
| `--port` | Database port | `5432` (PG), `3306` (MySQL) | `--port 5432` |
|
||||
| `--user` | Database user | `postgres` (PG), `root` (MySQL) | `--user backup_user` |
|
||||
| `--database` | Database name | `postgres` | `--database myapp_db` |
|
||||
| `--db-type` | Database type | `postgres` | `--db-type mysql` |
|
||||
| `-d`, `--db-type` | Database type (`postgres`, `mysql`, `mariadb`) | `postgres` | `-d mysql` |
|
||||
| `--ssl-mode` | SSL mode | `prefer` | `--ssl-mode require` |
|
||||
| `--insecure` | Disable SSL | `false` | `--insecure` |
|
||||
|
||||
@ -258,6 +263,11 @@ export PG_PORT=5432
|
||||
export PG_USER=postgres
|
||||
export PGPASSWORD=secret
|
||||
export DB_TYPE=postgres
|
||||
export MYSQL_HOST=localhost
|
||||
export MYSQL_PORT=3306
|
||||
export MYSQL_USER=root
|
||||
export MYSQL_PWD=secret
|
||||
export MYSQL_DATABASE=myapp_db
|
||||
|
||||
# CPU optimization
|
||||
export AUTO_DETECT_CORES=true
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"dbbackup/internal/tui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Create placeholder commands for the other subcommands
|
||||
@ -24,9 +26,6 @@ var restoreCmd = &cobra.Command{
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("backup archive filename required")
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("backup archive filename required")
|
||||
}
|
||||
return runRestore(cmd.Context(), args[0])
|
||||
},
|
||||
}
|
||||
@ -82,8 +81,6 @@ var statusCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
// runList lists available backups and databases
|
||||
func runList(ctx context.Context) error {
|
||||
fmt.Println("==============================================================")
|
||||
@ -391,10 +388,32 @@ func runRestore(ctx context.Context, archiveName string) error {
|
||||
fmt.Println("🔄 Would execute: pg_restore to restore single database")
|
||||
fmt.Printf(" Command: pg_restore -h %s -p %d -U %s -d %s --verbose %s\n",
|
||||
cfg.Host, cfg.Port, cfg.User, cfg.Database, archivePath)
|
||||
case "Single Database (.dump.gz)":
|
||||
fmt.Println("🔄 Would execute: gunzip and pg_restore to restore single database")
|
||||
fmt.Printf(" Command: gunzip -c %s | pg_restore -h %s -p %d -U %s -d %s --verbose\n",
|
||||
archivePath, cfg.Host, cfg.Port, cfg.User, cfg.Database)
|
||||
case "SQL Script (.sql)":
|
||||
if cfg.IsPostgreSQL() {
|
||||
fmt.Println("🔄 Would execute: psql to run SQL script")
|
||||
fmt.Printf(" Command: psql -h %s -p %d -U %s -d %s -f %s\n",
|
||||
cfg.Host, cfg.Port, cfg.User, cfg.Database, archivePath)
|
||||
} else if cfg.IsMySQL() {
|
||||
fmt.Println("🔄 Would execute: mysql to run SQL script")
|
||||
fmt.Printf(" Command: %s\n", mysqlRestoreCommand(archivePath, false))
|
||||
} else {
|
||||
fmt.Println("🔄 Would execute: SQL client to run script (database type unknown)")
|
||||
}
|
||||
case "SQL Script (.sql.gz)":
|
||||
if cfg.IsPostgreSQL() {
|
||||
fmt.Println("🔄 Would execute: gunzip and psql to run SQL script")
|
||||
fmt.Printf(" Command: gunzip -c %s | psql -h %s -p %d -U %s -d %s\n",
|
||||
archivePath, cfg.Host, cfg.Port, cfg.User, cfg.Database)
|
||||
} else if cfg.IsMySQL() {
|
||||
fmt.Println("🔄 Would execute: gunzip and mysql to run SQL script")
|
||||
fmt.Printf(" Command: %s\n", mysqlRestoreCommand(archivePath, true))
|
||||
} else {
|
||||
fmt.Println("🔄 Would execute: gunzip and SQL client to run script (database type unknown)")
|
||||
}
|
||||
case "Cluster Backup (.tar.gz)":
|
||||
fmt.Println("🔄 Would execute: Extract and restore cluster backup")
|
||||
fmt.Println(" Steps:")
|
||||
@ -415,8 +434,12 @@ func runRestore(ctx context.Context, archiveName string) error {
|
||||
|
||||
func detectArchiveType(filename string) string {
|
||||
switch {
|
||||
case strings.HasSuffix(filename, ".dump.gz"):
|
||||
return "Single Database (.dump.gz)"
|
||||
case strings.HasSuffix(filename, ".dump"):
|
||||
return "Single Database (.dump)"
|
||||
case strings.HasSuffix(filename, ".sql.gz"):
|
||||
return "SQL Script (.sql.gz)"
|
||||
case strings.HasSuffix(filename, ".sql"):
|
||||
return "SQL Script (.sql)"
|
||||
case strings.HasSuffix(filename, ".tar.gz"):
|
||||
@ -496,6 +519,16 @@ func runVerify(ctx context.Context, archiveName string) error {
|
||||
}
|
||||
checksRun++
|
||||
|
||||
case "Single Database (.dump.gz)":
|
||||
fmt.Print("🔍 PostgreSQL dump format check (gzip)... ")
|
||||
if err := verifyPgDumpGzip(archivePath); err != nil {
|
||||
fmt.Printf("❌ FAILED: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("✅ PASSED")
|
||||
checksPassed++
|
||||
}
|
||||
checksRun++
|
||||
|
||||
case "SQL Script (.sql)":
|
||||
fmt.Print("📜 SQL script validation... ")
|
||||
if err := verifySqlScript(archivePath); err != nil {
|
||||
@ -506,6 +539,16 @@ func runVerify(ctx context.Context, archiveName string) error {
|
||||
}
|
||||
checksRun++
|
||||
|
||||
case "SQL Script (.sql.gz)":
|
||||
fmt.Print("📜 SQL script validation (gzip)... ")
|
||||
if err := verifyGzipSqlScript(archivePath); err != nil {
|
||||
fmt.Printf("❌ FAILED: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("✅ PASSED")
|
||||
checksPassed++
|
||||
}
|
||||
checksRun++
|
||||
|
||||
case "Cluster Backup (.tar.gz)":
|
||||
fmt.Print("📦 Archive extraction test... ")
|
||||
if err := verifyTarGz(archivePath); err != nil {
|
||||
@ -551,18 +594,41 @@ func verifyPgDump(path string) error {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buffer := make([]byte, 100)
|
||||
buffer := make([]byte, 512)
|
||||
n, err := file.Read(buffer)
|
||||
if err != nil && n == 0 {
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("cannot read file: %w", err)
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("cannot read file")
|
||||
}
|
||||
|
||||
content := string(buffer[:n])
|
||||
if strings.Contains(content, "PostgreSQL") || strings.Contains(content, "pg_dump") {
|
||||
return nil
|
||||
return checkPgDumpSignature(buffer[:n])
|
||||
}
|
||||
|
||||
func verifyPgDumpGzip(path string) error {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
gz, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open gzip stream: %w", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
buffer := make([]byte, 512)
|
||||
n, err := gz.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("cannot read gzip contents: %w", err)
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("gzip archive is empty")
|
||||
}
|
||||
|
||||
return fmt.Errorf("does not appear to be a PostgreSQL dump file")
|
||||
return checkPgDumpSignature(buffer[:n])
|
||||
}
|
||||
|
||||
func verifySqlScript(path string) error {
|
||||
@ -573,19 +639,46 @@ func verifySqlScript(path string) error {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buffer := make([]byte, 500)
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := file.Read(buffer)
|
||||
if err != nil && n == 0 {
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("cannot read file: %w", err)
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("cannot read file")
|
||||
}
|
||||
|
||||
content := strings.ToLower(string(buffer[:n]))
|
||||
sqlKeywords := []string{"select", "insert", "create", "drop", "alter", "database", "table"}
|
||||
|
||||
for _, keyword := range sqlKeywords {
|
||||
if strings.Contains(content, keyword) {
|
||||
if containsSQLKeywords(strings.ToLower(string(buffer[:n]))) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("does not appear to contain SQL content")
|
||||
}
|
||||
|
||||
func verifyGzipSqlScript(path string) error {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
gz, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open gzip stream: %w", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := gz.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("cannot read gzip contents: %w", err)
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("gzip archive is empty")
|
||||
}
|
||||
|
||||
if containsSQLKeywords(strings.ToLower(string(buffer[:n]))) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("does not appear to contain SQL content")
|
||||
@ -612,3 +705,53 @@ func verifyTarGz(path string) error {
|
||||
|
||||
return fmt.Errorf("does not appear to be a valid gzip file")
|
||||
}
|
||||
|
||||
func checkPgDumpSignature(data []byte) error {
|
||||
if len(data) >= 5 && string(data[:5]) == "PGDMP" {
|
||||
return nil
|
||||
}
|
||||
|
||||
content := strings.ToLower(string(data))
|
||||
if strings.Contains(content, "postgresql") || strings.Contains(content, "pg_dump") {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("does not appear to be a PostgreSQL dump file")
|
||||
}
|
||||
|
||||
func containsSQLKeywords(content string) bool {
|
||||
sqlKeywords := []string{"select", "insert", "create", "drop", "alter", "database", "table", "update", "delete"}
|
||||
|
||||
for _, keyword := range sqlKeywords {
|
||||
if strings.Contains(content, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func mysqlRestoreCommand(archivePath string, compressed bool) string {
|
||||
parts := []string{
|
||||
"mysql",
|
||||
"-h", cfg.Host,
|
||||
"-P", fmt.Sprintf("%d", cfg.Port),
|
||||
"-u", cfg.User,
|
||||
}
|
||||
|
||||
if cfg.Password != "" {
|
||||
parts = append(parts, fmt.Sprintf("-p'%s'", cfg.Password))
|
||||
}
|
||||
|
||||
if cfg.Database != "" {
|
||||
parts = append(parts, cfg.Database)
|
||||
}
|
||||
|
||||
command := strings.Join(parts, " ")
|
||||
|
||||
if compressed {
|
||||
return fmt.Sprintf("gunzip -c %s | %s", archivePath, command)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s < %s", command, archivePath)
|
||||
}
|
||||
|
||||
10
cmd/root.go
10
cmd/root.go
@ -4,9 +4,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"dbbackup/internal/config"
|
||||
"dbbackup/internal/logger"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -34,6 +34,12 @@ Database Support:
|
||||
|
||||
For help with specific commands, use: dbbackup [command] --help`,
|
||||
Version: "",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
return cfg.SetDatabaseType(cfg.DatabaseType)
|
||||
},
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
@ -51,7 +57,7 @@ func Execute(ctx context.Context, config *config.Config, logger logger.Logger) e
|
||||
rootCmd.PersistentFlags().StringVar(&cfg.User, "user", cfg.User, "Database user")
|
||||
rootCmd.PersistentFlags().StringVar(&cfg.Database, "database", cfg.Database, "Database name")
|
||||
rootCmd.PersistentFlags().StringVar(&cfg.Password, "password", cfg.Password, "Database password")
|
||||
rootCmd.PersistentFlags().StringVar(&cfg.DatabaseType, "db-type", cfg.DatabaseType, "Database type (postgres|mysql)")
|
||||
rootCmd.PersistentFlags().StringVarP(&cfg.DatabaseType, "db-type", "d", cfg.DatabaseType, "Database type (postgres|mysql|mariadb)")
|
||||
rootCmd.PersistentFlags().StringVar(&cfg.BackupDir, "backup-dir", cfg.BackupDir, "Backup directory")
|
||||
rootCmd.PersistentFlags().BoolVar(&cfg.NoColor, "no-color", cfg.NoColor, "Disable colored output")
|
||||
rootCmd.PersistentFlags().BoolVar(&cfg.Debug, "debug", cfg.Debug, "Enable debug logging")
|
||||
|
||||
Binary file not shown.
@ -5,6 +5,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"dbbackup/internal/cpu"
|
||||
)
|
||||
@ -64,15 +65,41 @@ func New() *Config {
|
||||
cpuDetector := cpu.NewDetector()
|
||||
cpuInfo, _ := cpuDetector.DetectCPU()
|
||||
|
||||
return &Config{
|
||||
dbTypeRaw := getEnvString("DB_TYPE", "postgres")
|
||||
canonicalType, ok := canonicalDatabaseType(dbTypeRaw)
|
||||
if !ok {
|
||||
canonicalType = "postgres"
|
||||
}
|
||||
|
||||
host := getEnvString("PG_HOST", "localhost")
|
||||
port := getEnvInt("PG_PORT", postgresDefaultPort)
|
||||
user := getEnvString("PG_USER", getCurrentUser())
|
||||
databaseName := getEnvString("PG_DATABASE", "postgres")
|
||||
password := getEnvString("PGPASSWORD", "")
|
||||
sslMode := getEnvString("PG_SSLMODE", "prefer")
|
||||
|
||||
if canonicalType == "mysql" {
|
||||
host = getEnvString("MYSQL_HOST", host)
|
||||
port = getEnvInt("MYSQL_PORT", mysqlDefaultPort)
|
||||
user = getEnvString("MYSQL_USER", user)
|
||||
if db := getEnvString("MYSQL_DATABASE", ""); db != "" {
|
||||
databaseName = db
|
||||
}
|
||||
if pwd := getEnvString("MYSQL_PWD", ""); pwd != "" {
|
||||
password = pwd
|
||||
}
|
||||
sslMode = ""
|
||||
}
|
||||
|
||||
cfg := &Config{
|
||||
// Database defaults
|
||||
Host: getEnvString("PG_HOST", "localhost"),
|
||||
Port: getEnvInt("PG_PORT", 5432),
|
||||
User: getEnvString("PG_USER", getCurrentUser()),
|
||||
Database: getEnvString("PG_DATABASE", "postgres"),
|
||||
Password: getEnvString("PGPASSWORD", ""),
|
||||
DatabaseType: getEnvString("DB_TYPE", "postgres"),
|
||||
SSLMode: getEnvString("PG_SSLMODE", "prefer"),
|
||||
Host: host,
|
||||
Port: port,
|
||||
User: user,
|
||||
Database: databaseName,
|
||||
Password: password,
|
||||
DatabaseType: canonicalType,
|
||||
SSLMode: sslMode,
|
||||
Insecure: getEnvBool("INSECURE", false),
|
||||
|
||||
// Backup defaults
|
||||
@ -103,6 +130,15 @@ func New() *Config {
|
||||
SingleDBName: getEnvString("SINGLE_DB_NAME", ""),
|
||||
RestoreDBName: getEnvString("RESTORE_DB_NAME", ""),
|
||||
}
|
||||
|
||||
// Ensure canonical defaults are enforced
|
||||
if err := cfg.SetDatabaseType(cfg.DatabaseType); err != nil {
|
||||
cfg.DatabaseType = "postgres"
|
||||
cfg.Port = postgresDefaultPort
|
||||
cfg.SSLMode = "prefer"
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// UpdateFromEnvironment updates configuration from environment variables
|
||||
@ -117,8 +153,8 @@ func (c *Config) UpdateFromEnvironment() {
|
||||
|
||||
// Validate validates the configuration
|
||||
func (c *Config) Validate() error {
|
||||
if c.DatabaseType != "postgres" && c.DatabaseType != "mysql" {
|
||||
return &ConfigError{Field: "database-type", Value: c.DatabaseType, Message: "must be 'postgres' or 'mysql'"}
|
||||
if err := c.SetDatabaseType(c.DatabaseType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.CompressionLevel < 0 || c.CompressionLevel > 9 {
|
||||
@ -154,6 +190,55 @@ func (c *Config) GetDefaultPort() int {
|
||||
return 5432
|
||||
}
|
||||
|
||||
// DisplayDatabaseType returns a human-friendly name for the database type
|
||||
func (c *Config) DisplayDatabaseType() string {
|
||||
switch c.DatabaseType {
|
||||
case "postgres":
|
||||
return "PostgreSQL"
|
||||
case "mysql":
|
||||
return "MySQL/MariaDB"
|
||||
default:
|
||||
return c.DatabaseType
|
||||
}
|
||||
}
|
||||
|
||||
// SetDatabaseType normalizes the database type and updates dependent defaults
|
||||
func (c *Config) SetDatabaseType(dbType string) error {
|
||||
normalized, ok := canonicalDatabaseType(dbType)
|
||||
if !ok {
|
||||
return &ConfigError{Field: "database-type", Value: dbType, Message: "must be 'postgres' or 'mysql'"}
|
||||
}
|
||||
|
||||
previous := c.DatabaseType
|
||||
previousPort := c.Port
|
||||
|
||||
c.DatabaseType = normalized
|
||||
|
||||
if c.Port == 0 {
|
||||
c.Port = defaultPortFor(normalized)
|
||||
}
|
||||
|
||||
if normalized != previous {
|
||||
if previousPort == defaultPortFor(previous) || previousPort == 0 {
|
||||
c.Port = defaultPortFor(normalized)
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust SSL mode defaults when switching engines. Preserve explicit user choices.
|
||||
switch normalized {
|
||||
case "mysql":
|
||||
if strings.EqualFold(c.SSLMode, "prefer") || strings.EqualFold(c.SSLMode, "preferred") {
|
||||
c.SSLMode = ""
|
||||
}
|
||||
case "postgres":
|
||||
if c.SSLMode == "" {
|
||||
c.SSLMode = "prefer"
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OptimizeForCPU optimizes job settings based on detected CPU
|
||||
func (c *Config) OptimizeForCPU() error {
|
||||
if c.CPUDetector == nil {
|
||||
@ -216,6 +301,33 @@ func (e *ConfigError) Error() string {
|
||||
return "config error in field '" + e.Field + "' with value '" + e.Value + "': " + e.Message
|
||||
}
|
||||
|
||||
const (
|
||||
postgresDefaultPort = 5432
|
||||
mysqlDefaultPort = 3306
|
||||
)
|
||||
|
||||
func canonicalDatabaseType(input string) (string, bool) {
|
||||
switch strings.ToLower(strings.TrimSpace(input)) {
|
||||
case "postgres", "postgresql", "pg":
|
||||
return "postgres", true
|
||||
case "mysql", "mariadb", "mariadb-server", "maria":
|
||||
return "mysql", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func defaultPortFor(dbType string) int {
|
||||
switch dbType {
|
||||
case "postgres":
|
||||
return postgresDefaultPort
|
||||
case "mysql":
|
||||
return mysqlDefaultPort
|
||||
default:
|
||||
return postgresDefaultPort
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func getEnvString(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
|
||||
@ -249,19 +249,16 @@ func (m *MySQL) BuildBackupCommand(database, outputFile string, options BackupOp
|
||||
// SSL options
|
||||
if m.cfg.Insecure {
|
||||
cmd = append(cmd, "--skip-ssl")
|
||||
} else if m.cfg.SSLMode != "" {
|
||||
// MySQL SSL modes: DISABLED, PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY
|
||||
switch strings.ToLower(m.cfg.SSLMode) {
|
||||
case "disable", "disabled":
|
||||
cmd = append(cmd, "--skip-ssl")
|
||||
} else if mode := strings.ToLower(m.cfg.SSLMode); mode != "" {
|
||||
switch mode {
|
||||
case "require", "required":
|
||||
cmd = append(cmd, "--ssl-mode=REQUIRED")
|
||||
case "verify-ca":
|
||||
cmd = append(cmd, "--ssl-mode=VERIFY_CA")
|
||||
case "verify-full", "verify-identity":
|
||||
cmd = append(cmd, "--ssl-mode=VERIFY_IDENTITY")
|
||||
default:
|
||||
cmd = append(cmd, "--ssl-mode=PREFERRED")
|
||||
case "disable", "disabled":
|
||||
cmd = append(cmd, "--skip-ssl")
|
||||
}
|
||||
}
|
||||
|
||||
@ -376,14 +373,12 @@ func (m *MySQL) buildDSN() string {
|
||||
// Add connection parameters
|
||||
params := []string{}
|
||||
|
||||
if m.cfg.Insecure {
|
||||
params = append(params, "tls=skip-verify")
|
||||
} else if m.cfg.SSLMode != "" {
|
||||
if !m.cfg.Insecure {
|
||||
switch strings.ToLower(m.cfg.SSLMode) {
|
||||
case "disable", "disabled":
|
||||
params = append(params, "tls=false")
|
||||
case "require", "required":
|
||||
params = append(params, "tls=true")
|
||||
case "verify-ca", "verify-full", "verify-identity":
|
||||
params = append(params, "tls=preferred")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ package tui
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
@ -36,8 +37,17 @@ var (
|
||||
errorStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF6B6B")).
|
||||
Bold(true)
|
||||
|
||||
dbSelectorLabelStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#57C7FF")).
|
||||
Bold(true)
|
||||
)
|
||||
|
||||
type dbTypeOption struct {
|
||||
label string
|
||||
value string
|
||||
}
|
||||
|
||||
// MenuModel represents the simple menu state
|
||||
type MenuModel struct {
|
||||
choices []string
|
||||
@ -46,6 +56,8 @@ type MenuModel struct {
|
||||
logger logger.Logger
|
||||
quitting bool
|
||||
message string
|
||||
dbTypes []dbTypeOption
|
||||
dbTypeCursor int
|
||||
|
||||
// Background operations
|
||||
ctx context.Context
|
||||
@ -55,6 +67,16 @@ type MenuModel struct {
|
||||
func NewMenuModel(cfg *config.Config, log logger.Logger) MenuModel {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
dbTypes := []dbTypeOption{
|
||||
{label: "PostgreSQL", value: "postgres"},
|
||||
{label: "MySQL / MariaDB", value: "mysql"},
|
||||
}
|
||||
|
||||
dbCursor := 0
|
||||
if cfg.IsMySQL() {
|
||||
dbCursor = 1
|
||||
}
|
||||
|
||||
model := MenuModel{
|
||||
choices: []string{
|
||||
"Single Database Backup",
|
||||
@ -71,6 +93,8 @@ func NewMenuModel(cfg *config.Config, log logger.Logger) MenuModel {
|
||||
logger: log,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
dbTypes: dbTypes,
|
||||
dbTypeCursor: dbCursor,
|
||||
}
|
||||
|
||||
return model
|
||||
@ -93,6 +117,24 @@ func (m MenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
|
||||
case "left", "h":
|
||||
if m.dbTypeCursor > 0 {
|
||||
m.dbTypeCursor--
|
||||
m.applyDatabaseSelection()
|
||||
}
|
||||
|
||||
case "right", "l":
|
||||
if m.dbTypeCursor < len(m.dbTypes)-1 {
|
||||
m.dbTypeCursor++
|
||||
m.applyDatabaseSelection()
|
||||
}
|
||||
|
||||
case "t":
|
||||
if len(m.dbTypes) > 0 {
|
||||
m.dbTypeCursor = (m.dbTypeCursor + 1) % len(m.dbTypes)
|
||||
m.applyDatabaseSelection()
|
||||
}
|
||||
|
||||
case "up", "k":
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
@ -146,9 +188,24 @@ func (m MenuModel) View() string {
|
||||
header := titleStyle.Render("🗄️ Database Backup Tool - Interactive Menu")
|
||||
s += fmt.Sprintf("\n%s\n\n", header)
|
||||
|
||||
if len(m.dbTypes) > 0 {
|
||||
options := make([]string, len(m.dbTypes))
|
||||
for i, opt := range m.dbTypes {
|
||||
if m.dbTypeCursor == i {
|
||||
options[i] = menuSelectedStyle.Render(opt.label)
|
||||
} else {
|
||||
options[i] = menuStyle.Render(opt.label)
|
||||
}
|
||||
}
|
||||
selector := fmt.Sprintf("Target Engine: %s", strings.Join(options, menuStyle.Render(" | ")))
|
||||
s += dbSelectorLabelStyle.Render(selector) + "\n"
|
||||
hint := infoStyle.Render("Switch with ←/→ or t • Cluster backup requires PostgreSQL")
|
||||
s += hint + "\n\n"
|
||||
}
|
||||
|
||||
// Database info
|
||||
dbInfo := infoStyle.Render(fmt.Sprintf("Database: %s@%s:%d (%s)",
|
||||
m.config.User, m.config.Host, m.config.Port, m.config.DatabaseType))
|
||||
m.config.User, m.config.Host, m.config.Port, m.config.DisplayDatabaseType()))
|
||||
s += fmt.Sprintf("%s\n\n", dbInfo)
|
||||
|
||||
// Menu items
|
||||
@ -189,6 +246,10 @@ func (m MenuModel) handleSampleBackup() (tea.Model, tea.Cmd) {
|
||||
|
||||
// handleClusterBackup shows confirmation and executes cluster backup
|
||||
func (m MenuModel) handleClusterBackup() (tea.Model, tea.Cmd) {
|
||||
if !m.config.IsPostgreSQL() {
|
||||
m.message = errorStyle.Render("❌ Cluster backup is available only for PostgreSQL targets")
|
||||
return m, nil
|
||||
}
|
||||
confirm := NewConfirmationModel(m.config, m.logger, m,
|
||||
"🗄️ Cluster Backup",
|
||||
"This will backup ALL databases in the cluster. Continue?")
|
||||
@ -220,6 +281,31 @@ func (m MenuModel) handleSettings() (tea.Model, tea.Cmd) {
|
||||
return settingsModel, nil
|
||||
}
|
||||
|
||||
func (m *MenuModel) applyDatabaseSelection() {
|
||||
if m == nil || len(m.dbTypes) == 0 {
|
||||
return
|
||||
}
|
||||
if m.dbTypeCursor < 0 || m.dbTypeCursor >= len(m.dbTypes) {
|
||||
return
|
||||
}
|
||||
|
||||
selection := m.dbTypes[m.dbTypeCursor]
|
||||
if err := m.config.SetDatabaseType(selection.value); err != nil {
|
||||
m.message = errorStyle.Render(fmt.Sprintf("❌ %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh default port if unchanged
|
||||
if m.config.Port == 0 {
|
||||
m.config.Port = m.config.GetDefaultPort()
|
||||
}
|
||||
|
||||
m.message = successStyle.Render(fmt.Sprintf("🔀 Target database set to %s", m.config.DisplayDatabaseType()))
|
||||
if m.logger != nil {
|
||||
m.logger.Info("updated target database type", "type", m.config.DatabaseType, "port", m.config.Port)
|
||||
}
|
||||
}
|
||||
|
||||
// RunInteractiveMenu starts the simple TUI
|
||||
func RunInteractiveMenu(cfg *config.Config, log logger.Logger) error {
|
||||
m := NewMenuModel(cfg, log)
|
||||
|
||||
@ -50,6 +50,16 @@ type SettingItem struct {
|
||||
// Initialize settings model
|
||||
func NewSettingsModel(cfg *config.Config, log logger.Logger, parent tea.Model) SettingsModel {
|
||||
settings := []SettingItem{
|
||||
{
|
||||
Key: "database_type",
|
||||
DisplayName: "Database Type",
|
||||
Value: func(c *config.Config) string { return c.DatabaseType },
|
||||
Update: func(c *config.Config, v string) error {
|
||||
return c.SetDatabaseType(v)
|
||||
},
|
||||
Type: "string",
|
||||
Description: "Target database engine (postgres, mysql, mariadb)",
|
||||
},
|
||||
{
|
||||
Key: "backup_dir",
|
||||
DisplayName: "Backup Directory",
|
||||
@ -196,7 +206,11 @@ func NewSettingsModel(cfg *config.Config, log logger.Logger, parent tea.Model) S
|
||||
Key: "auto_detect_cores",
|
||||
DisplayName: "Auto Detect CPU Cores",
|
||||
Value: func(c *config.Config) string {
|
||||
if c.AutoDetectCores { return "true" } else { return "false" }
|
||||
if c.AutoDetectCores {
|
||||
return "true"
|
||||
} else {
|
||||
return "false"
|
||||
}
|
||||
},
|
||||
Update: func(c *config.Config, v string) error {
|
||||
val, err := strconv.ParseBool(v)
|
||||
@ -456,6 +470,10 @@ func (m SettingsModel) View() string {
|
||||
for i, setting := range m.settings {
|
||||
cursor := " "
|
||||
value := setting.Value(m.config)
|
||||
displayValue := value
|
||||
if setting.Key == "database_type" {
|
||||
displayValue = fmt.Sprintf("%s (%s)", value, m.config.DisplayDatabaseType())
|
||||
}
|
||||
|
||||
if m.cursor == i {
|
||||
cursor = ">"
|
||||
@ -469,11 +487,11 @@ func (m SettingsModel) View() string {
|
||||
b.WriteString(selectedStyle.Render(line))
|
||||
b.WriteString(" ✏️")
|
||||
} else {
|
||||
line := fmt.Sprintf("%s %s: %s", cursor, setting.DisplayName, value)
|
||||
line := fmt.Sprintf("%s %s: %s", cursor, setting.DisplayName, displayValue)
|
||||
b.WriteString(selectedStyle.Render(line))
|
||||
}
|
||||
} else {
|
||||
line := fmt.Sprintf("%s %s: %s", cursor, setting.DisplayName, value)
|
||||
line := fmt.Sprintf("%s %s: %s", cursor, setting.DisplayName, displayValue)
|
||||
b.WriteString(menuStyle.Render(line))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
@ -508,6 +526,7 @@ func (m SettingsModel) View() string {
|
||||
b.WriteString("\n")
|
||||
|
||||
summary := []string{
|
||||
fmt.Sprintf("Target DB: %s (%s)", m.config.DisplayDatabaseType(), m.config.DatabaseType),
|
||||
fmt.Sprintf("Database: %s@%s:%d", m.config.User, m.config.Host, m.config.Port),
|
||||
fmt.Sprintf("Backup Dir: %s", m.config.BackupDir),
|
||||
fmt.Sprintf("Compression: Level %d", m.config.CompressionLevel),
|
||||
|
||||
@ -148,7 +148,7 @@ func (m StatusViewModel) View() string {
|
||||
}
|
||||
s.WriteString("\n")
|
||||
|
||||
s.WriteString(fmt.Sprintf("Database Type: %s\n", m.config.DatabaseType))
|
||||
s.WriteString(fmt.Sprintf("Database Type: %s (%s)\n", m.config.DisplayDatabaseType(), m.config.DatabaseType))
|
||||
s.WriteString(fmt.Sprintf("Host: %s:%d\n", m.config.Host, m.config.Port))
|
||||
s.WriteString(fmt.Sprintf("User: %s\n", m.config.User))
|
||||
s.WriteString(fmt.Sprintf("Backup Directory: %s\n", m.config.BackupDir))
|
||||
|
||||
173
scripts/cli_switch_test.sh
Executable file
173
scripts/cli_switch_test.sh
Executable file
@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
BINARY_NAME="dbbackup_linux_amd64"
|
||||
BINARY="./${BINARY_NAME}"
|
||||
LOG_DIR="${REPO_ROOT}/test_logs"
|
||||
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
LOG_FILE="${LOG_DIR}/cli_switch_test_${TIMESTAMP}.log"
|
||||
|
||||
PG_BACKUP_DIR="/tmp/db_backups"
|
||||
PG_DATABASE="postgres"
|
||||
PG_FLAGS=(
|
||||
--db-type postgres
|
||||
--host localhost
|
||||
--port 5432
|
||||
--user postgres
|
||||
--database "${PG_DATABASE}"
|
||||
--backup-dir "${PG_BACKUP_DIR}"
|
||||
--jobs 4
|
||||
--dump-jobs 4
|
||||
--max-cores 8
|
||||
--cpu-workload balanced
|
||||
--debug
|
||||
)
|
||||
|
||||
MYSQL_BACKUP_DIR="/tmp/mysql_backups"
|
||||
MYSQL_DATABASE="backup_demo"
|
||||
MYSQL_FLAGS=(
|
||||
--db-type mysql
|
||||
--host 127.0.0.1
|
||||
--port 3306
|
||||
--user backup_user
|
||||
--password backup_pass
|
||||
--database "${MYSQL_DATABASE}"
|
||||
--backup-dir "${MYSQL_BACKUP_DIR}"
|
||||
--insecure
|
||||
--jobs 2
|
||||
--dump-jobs 2
|
||||
--max-cores 4
|
||||
--cpu-workload io-intensive
|
||||
--debug
|
||||
)
|
||||
|
||||
mkdir -p "${LOG_DIR}"
|
||||
|
||||
log() {
|
||||
printf '%s\n' "$1" | tee -a "${LOG_FILE}" >/dev/null
|
||||
}
|
||||
|
||||
RESULTS=()
|
||||
|
||||
run_cmd() {
|
||||
local label="$1"
|
||||
shift
|
||||
log ""
|
||||
log "### ${label}"
|
||||
log "Command: $*"
|
||||
"$@" 2>&1 | tee -a "${LOG_FILE}"
|
||||
local status=${PIPESTATUS[0]}
|
||||
log "Exit: ${status}"
|
||||
RESULTS+=("${label}|${status}")
|
||||
}
|
||||
|
||||
latest_file() {
|
||||
local dir="$1"
|
||||
local pattern="$2"
|
||||
shopt -s nullglob
|
||||
local files=("${dir}"/${pattern})
|
||||
shopt -u nullglob
|
||||
if (( ${#files[@]} == 0 )); then
|
||||
return 1
|
||||
fi
|
||||
local latest="${files[0]}"
|
||||
for file in "${files[@]}"; do
|
||||
if [[ "${file}" -nt "${latest}" ]]; then
|
||||
latest="${file}"
|
||||
fi
|
||||
done
|
||||
printf '%s\n' "${latest}"
|
||||
}
|
||||
|
||||
log "dbbackup CLI regression started"
|
||||
log "Log file: ${LOG_FILE}"
|
||||
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
run_cmd "Go build" go build -o "${BINARY}" .
|
||||
run_cmd "Ensure Postgres backup dir" sudo -u postgres mkdir -p "${PG_BACKUP_DIR}"
|
||||
run_cmd "Ensure MySQL backup dir" mkdir -p "${MYSQL_BACKUP_DIR}"
|
||||
|
||||
run_cmd "Postgres status" sudo -u postgres "${BINARY}" status "${PG_FLAGS[@]}"
|
||||
run_cmd "Postgres preflight" sudo -u postgres "${BINARY}" preflight "${PG_FLAGS[@]}"
|
||||
run_cmd "Postgres CPU info" sudo -u postgres "${BINARY}" cpu "${PG_FLAGS[@]}"
|
||||
run_cmd "Postgres backup single" sudo -u postgres "${BINARY}" backup single "${PG_DATABASE}" "${PG_FLAGS[@]}"
|
||||
run_cmd "Postgres backup sample" sudo -u postgres "${BINARY}" backup sample "${PG_DATABASE}" --sample-ratio 5 "${PG_FLAGS[@]}"
|
||||
run_cmd "Postgres backup cluster" sudo -u postgres "${BINARY}" backup cluster "${PG_FLAGS[@]}"
|
||||
run_cmd "Postgres list" sudo -u postgres "${BINARY}" list "${PG_FLAGS[@]}"
|
||||
|
||||
PG_SINGLE_FILE="$(latest_file "${PG_BACKUP_DIR}" "db_${PG_DATABASE}_*.dump" || true)"
|
||||
PG_SAMPLE_FILE="$(latest_file "${PG_BACKUP_DIR}" "sample_${PG_DATABASE}_*.sql" || true)"
|
||||
PG_CLUSTER_FILE="$(latest_file "${PG_BACKUP_DIR}" "cluster_*.tar.gz" || true)"
|
||||
|
||||
if [[ -n "${PG_SINGLE_FILE}" ]]; then
|
||||
run_cmd "Postgres verify single" sudo -u postgres "${BINARY}" verify "$(basename "${PG_SINGLE_FILE}")" "${PG_FLAGS[@]}"
|
||||
run_cmd "Postgres restore single" sudo -u postgres "${BINARY}" restore "$(basename "${PG_SINGLE_FILE}")" "${PG_FLAGS[@]}"
|
||||
else
|
||||
log "No PostgreSQL single backup found for verification"
|
||||
RESULTS+=("Postgres single artifact missing|1")
|
||||
fi
|
||||
|
||||
if [[ -n "${PG_SAMPLE_FILE}" ]]; then
|
||||
run_cmd "Postgres verify sample" sudo -u postgres "${BINARY}" verify "$(basename "${PG_SAMPLE_FILE}")" "${PG_FLAGS[@]}"
|
||||
run_cmd "Postgres restore sample" sudo -u postgres "${BINARY}" restore "$(basename "${PG_SAMPLE_FILE}")" "${PG_FLAGS[@]}"
|
||||
else
|
||||
log "No PostgreSQL sample backup found for verification"
|
||||
RESULTS+=("Postgres sample artifact missing|1")
|
||||
fi
|
||||
|
||||
if [[ -n "${PG_CLUSTER_FILE}" ]]; then
|
||||
run_cmd "Postgres verify cluster" sudo -u postgres "${BINARY}" verify "$(basename "${PG_CLUSTER_FILE}")" "${PG_FLAGS[@]}"
|
||||
run_cmd "Postgres restore cluster" sudo -u postgres "${BINARY}" restore "$(basename "${PG_CLUSTER_FILE}")" "${PG_FLAGS[@]}"
|
||||
else
|
||||
log "No PostgreSQL cluster backup found for verification"
|
||||
RESULTS+=("Postgres cluster artifact missing|1")
|
||||
fi
|
||||
|
||||
run_cmd "MySQL status" "${BINARY}" status "${MYSQL_FLAGS[@]}"
|
||||
run_cmd "MySQL preflight" "${BINARY}" preflight "${MYSQL_FLAGS[@]}"
|
||||
run_cmd "MySQL CPU info" "${BINARY}" cpu "${MYSQL_FLAGS[@]}"
|
||||
run_cmd "MySQL backup single" "${BINARY}" backup single "${MYSQL_DATABASE}" "${MYSQL_FLAGS[@]}"
|
||||
run_cmd "MySQL backup sample" "${BINARY}" backup sample "${MYSQL_DATABASE}" --sample-percent 25 "${MYSQL_FLAGS[@]}"
|
||||
run_cmd "MySQL list" "${BINARY}" list "${MYSQL_FLAGS[@]}"
|
||||
|
||||
MYSQL_SINGLE_FILE="$(latest_file "${MYSQL_BACKUP_DIR}" "db_${MYSQL_DATABASE}_*.sql.gz" || true)"
|
||||
MYSQL_SAMPLE_FILE="$(latest_file "${MYSQL_BACKUP_DIR}" "sample_${MYSQL_DATABASE}_*.sql" || true)"
|
||||
|
||||
if [[ -n "${MYSQL_SINGLE_FILE}" ]]; then
|
||||
run_cmd "MySQL verify single" "${BINARY}" verify "$(basename "${MYSQL_SINGLE_FILE}")" "${MYSQL_FLAGS[@]}"
|
||||
run_cmd "MySQL restore single" "${BINARY}" restore "$(basename "${MYSQL_SINGLE_FILE}")" "${MYSQL_FLAGS[@]}"
|
||||
else
|
||||
log "No MySQL single backup found for verification"
|
||||
RESULTS+=("MySQL single artifact missing|1")
|
||||
fi
|
||||
|
||||
if [[ -n "${MYSQL_SAMPLE_FILE}" ]]; then
|
||||
run_cmd "MySQL verify sample" "${BINARY}" verify "$(basename "${MYSQL_SAMPLE_FILE}")" "${MYSQL_FLAGS[@]}"
|
||||
run_cmd "MySQL restore sample" "${BINARY}" restore "$(basename "${MYSQL_SAMPLE_FILE}")" "${MYSQL_FLAGS[@]}"
|
||||
else
|
||||
log "No MySQL sample backup found for verification"
|
||||
RESULTS+=("MySQL sample artifact missing|1")
|
||||
fi
|
||||
|
||||
run_cmd "Interactive help" "${BINARY}" interactive --help
|
||||
run_cmd "Root help" "${BINARY}" --help
|
||||
run_cmd "Root version" "${BINARY}" --version
|
||||
|
||||
log ""
|
||||
log "=== Summary ==="
|
||||
failed=0
|
||||
for entry in "${RESULTS[@]}"; do
|
||||
IFS='|' read -r label status <<<"${entry}"
|
||||
if [[ "${status}" -eq 0 ]]; then
|
||||
log "[PASS] ${label}"
|
||||
else
|
||||
log "[FAIL] ${label} (exit ${status})"
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
|
||||
exit "${failed}"
|
||||
@ -1,68 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "==================================="
|
||||
echo " DB BACKUP TOOL - FUNCTION TEST"
|
||||
echo "==================================="
|
||||
echo
|
||||
|
||||
# Test all CLI commands
|
||||
commands=("backup" "restore" "list" "status" "verify" "preflight" "cpu")
|
||||
|
||||
echo "1. Testing CLI Commands:"
|
||||
echo "------------------------"
|
||||
for cmd in "${commands[@]}"; do
|
||||
echo -n " $cmd: "
|
||||
output=$(./dbbackup_linux_amd64 $cmd --help 2>&1 | head -1)
|
||||
if [[ "$output" == *"not yet implemented"* ]]; then
|
||||
echo "❌ PLACEHOLDER"
|
||||
elif [[ "$output" == *"Error: unknown command"* ]]; then
|
||||
echo "❌ MISSING"
|
||||
else
|
||||
echo "✅ IMPLEMENTED"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "2. Testing Backup Subcommands:"
|
||||
echo "------------------------------"
|
||||
backup_cmds=("single" "cluster" "sample")
|
||||
for cmd in "${backup_cmds[@]}"; do
|
||||
echo -n " backup $cmd: "
|
||||
output=$(./dbbackup_linux_amd64 backup $cmd --help 2>&1 | head -1)
|
||||
if [[ "$output" == *"Error"* ]]; then
|
||||
echo "❌ MISSING"
|
||||
else
|
||||
echo "✅ IMPLEMENTED"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "3. Testing Status & Connection:"
|
||||
echo "------------------------------"
|
||||
echo " Database connection test:"
|
||||
./dbbackup_linux_amd64 status 2>&1 | grep -E "(✅|❌)" | head -3
|
||||
|
||||
echo
|
||||
echo "4. Testing Interactive Mode:"
|
||||
echo "----------------------------"
|
||||
echo " Starting interactive (5 sec timeout)..."
|
||||
timeout 5s ./dbbackup_linux_amd64 interactive >/dev/null 2>&1
|
||||
if [ $? -eq 124 ]; then
|
||||
echo " ✅ Interactive mode starts (timed out = working)"
|
||||
else
|
||||
echo " ❌ Interactive mode failed"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "5. Testing with Different DB Types:"
|
||||
echo "----------------------------------"
|
||||
echo " PostgreSQL config:"
|
||||
./dbbackup_linux_amd64 --db-type postgres status 2>&1 | grep "Database Type" || echo " ❌ Failed"
|
||||
|
||||
echo " MySQL config:"
|
||||
./dbbackup_linux_amd64 --db-type mysql status 2>&1 | grep "Database Type" || echo " ❌ Failed"
|
||||
|
||||
echo
|
||||
echo "==================================="
|
||||
echo " TEST COMPLETE"
|
||||
echo "==================================="
|
||||
Reference in New Issue
Block a user