Add backup/restore commands

This commit is contained in:
Pierre-Olivier Mercier
2025-11-02 10:36:17 +07:00
parent 1c4eb0653e
commit d870fc8130
3 changed files with 205 additions and 5 deletions

View File

@@ -33,8 +33,8 @@ import (
)
func main() {
fmt.Println("happyDeliver - Email Deliverability Testing Platform")
fmt.Printf("Version: %s\n", version.Version)
fmt.Fprintln(os.Stderr, "happyDeliver - Email Deliverability Testing Platform")
fmt.Fprintf(os.Stderr, "Version: %s\n", version.Version)
cfg, err := config.ConsolidateConfig()
if err != nil {
@@ -52,6 +52,18 @@ func main() {
if err := app.RunAnalyzer(cfg, flag.Args()[1:], os.Stdin, os.Stdout); err != nil {
log.Fatalf("Analyzer error: %v", err)
}
case "backup":
if err := app.RunBackup(cfg); err != nil {
log.Fatalf("Backup error: %v", err)
}
case "restore":
inputFile := ""
if len(flag.Args()) >= 2 {
inputFile = flag.Args()[1]
}
if err := app.RunRestore(cfg, inputFile); err != nil {
log.Fatalf("Restore error: %v", err)
}
case "version":
fmt.Println(version.Version)
default:
@@ -63,9 +75,11 @@ func main() {
func printUsage() {
fmt.Println("\nCommand availables:")
fmt.Println(" happyDeliver server - Start the API server")
fmt.Println(" happyDeliver analyze [-json] - Analyze email from stdin and output results to terminal")
fmt.Println(" happyDeliver version - Print version information")
fmt.Println(" happyDeliver server - Start the API server")
fmt.Println(" happyDeliver analyze [-json] - Analyze email from stdin and output results to terminal")
fmt.Println(" happyDeliver backup - Backup database to stdout as JSON")
fmt.Println(" happyDeliver restore [file] - Restore database from JSON file or stdin")
fmt.Println(" happyDeliver version - Print version information")
fmt.Println("")
flag.Usage()
}

156
internal/app/cli_backup.go Normal file
View File

@@ -0,0 +1,156 @@
// This file is part of the happyDeliver (R) project.
// Copyright (c) 2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package app
import (
"encoding/json"
"fmt"
"io"
"os"
"git.happydns.org/happyDeliver/internal/config"
"git.happydns.org/happyDeliver/internal/storage"
)
// BackupData represents the structure of a backup file
type BackupData struct {
Version string `json:"version"`
Reports []storage.Report `json:"reports"`
}
// RunBackup exports the database to stdout as JSON
func RunBackup(cfg *config.Config) error {
if err := cfg.Validate(); err != nil {
return err
}
// Initialize storage
store, err := storage.NewStorage(cfg.Database.Type, cfg.Database.DSN)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
defer store.Close()
fmt.Fprintf(os.Stderr, "Connected to %s database\n", cfg.Database.Type)
// Get all reports from the database
reports, err := storage.GetAllReports(store)
if err != nil {
return fmt.Errorf("failed to retrieve reports: %w", err)
}
fmt.Fprintf(os.Stderr, "Found %d reports to backup\n", len(reports))
// Create backup data structure
backup := BackupData{
Version: "1.0",
Reports: reports,
}
// Encode to JSON and write to stdout
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(backup); err != nil {
return fmt.Errorf("failed to encode backup data: %w", err)
}
return nil
}
// RunRestore imports the database from a JSON file or stdin
func RunRestore(cfg *config.Config, inputPath string) error {
if err := cfg.Validate(); err != nil {
return err
}
// Determine input source
var reader io.Reader
if inputPath == "" || inputPath == "-" {
fmt.Fprintln(os.Stderr, "Reading backup from stdin...")
reader = os.Stdin
} else {
inFile, err := os.Open(inputPath)
if err != nil {
return fmt.Errorf("failed to open backup file: %w", err)
}
defer inFile.Close()
fmt.Fprintf(os.Stderr, "Reading backup from file: %s\n", inputPath)
reader = inFile
}
// Decode JSON
var backup BackupData
decoder := json.NewDecoder(reader)
if err := decoder.Decode(&backup); err != nil {
if err == io.EOF {
return fmt.Errorf("backup file is empty or corrupted")
}
return fmt.Errorf("failed to decode backup data: %w", err)
}
fmt.Fprintf(os.Stderr, "Backup version: %s\n", backup.Version)
fmt.Fprintf(os.Stderr, "Found %d reports in backup\n", len(backup.Reports))
// Initialize storage
store, err := storage.NewStorage(cfg.Database.Type, cfg.Database.DSN)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
defer store.Close()
fmt.Fprintf(os.Stderr, "Connected to %s database\n", cfg.Database.Type)
// Restore reports
restored, skipped, failed := 0, 0, 0
for _, report := range backup.Reports {
// Check if report already exists
exists, err := store.ReportExists(report.TestID)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to check if report %s exists: %v\n", report.TestID, err)
failed++
continue
}
if exists {
fmt.Fprintf(os.Stderr, "Report %s already exists, skipping\n", report.TestID)
skipped++
continue
}
// Create the report
_, err = storage.CreateReportFromBackup(store, &report)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to restore report %s: %v\n", report.TestID, err)
failed++
continue
}
restored++
}
fmt.Fprintf(os.Stderr, "Restore completed: %d restored, %d skipped, %d failed\n", restored, skipped, failed)
if failed > 0 {
return fmt.Errorf("restore completed with %d failures", failed)
}
return nil
}

View File

@@ -147,3 +147,33 @@ func (s *DBStorage) Close() error {
}
return sqlDB.Close()
}
// GetAllReports retrieves all reports from the database
func GetAllReports(s Storage) ([]Report, error) {
dbStorage, ok := s.(*DBStorage)
if !ok {
return nil, fmt.Errorf("storage type does not support GetAllReports")
}
var reports []Report
if err := dbStorage.db.Find(&reports).Error; err != nil {
return nil, fmt.Errorf("failed to retrieve reports: %w", err)
}
return reports, nil
}
// CreateReportFromBackup creates a report from backup data, preserving timestamps
func CreateReportFromBackup(s Storage, report *Report) (*Report, error) {
dbStorage, ok := s.(*DBStorage)
if !ok {
return nil, fmt.Errorf("storage type does not support CreateReportFromBackup")
}
// Use Create to insert the report with all fields including timestamps
if err := dbStorage.db.Create(report).Error; err != nil {
return nil, fmt.Errorf("failed to create report from backup: %w", err)
}
return report, nil
}