Add backup/restore commands
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
1c4eb0653e
commit
d870fc8130
3 changed files with 205 additions and 5 deletions
|
|
@ -33,8 +33,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Println("happyDeliver - Email Deliverability Testing Platform")
|
fmt.Fprintln(os.Stderr, "happyDeliver - Email Deliverability Testing Platform")
|
||||||
fmt.Printf("Version: %s\n", version.Version)
|
fmt.Fprintf(os.Stderr, "Version: %s\n", version.Version)
|
||||||
|
|
||||||
cfg, err := config.ConsolidateConfig()
|
cfg, err := config.ConsolidateConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -52,6 +52,18 @@ func main() {
|
||||||
if err := app.RunAnalyzer(cfg, flag.Args()[1:], os.Stdin, os.Stdout); err != nil {
|
if err := app.RunAnalyzer(cfg, flag.Args()[1:], os.Stdin, os.Stdout); err != nil {
|
||||||
log.Fatalf("Analyzer error: %v", err)
|
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":
|
case "version":
|
||||||
fmt.Println(version.Version)
|
fmt.Println(version.Version)
|
||||||
default:
|
default:
|
||||||
|
|
@ -65,6 +77,8 @@ func printUsage() {
|
||||||
fmt.Println("\nCommand availables:")
|
fmt.Println("\nCommand availables:")
|
||||||
fmt.Println(" happyDeliver server - Start the API server")
|
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 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(" happyDeliver version - Print version information")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
|
|
|
||||||
156
internal/app/cli_backup.go
Normal file
156
internal/app/cli_backup.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -147,3 +147,33 @@ func (s *DBStorage) Close() error {
|
||||||
}
|
}
|
||||||
return sqlDB.Close()
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue