Files
atlas/internal/backup/service.go
othman.suseno df475bc85e
Some checks failed
CI / test-build (push) Failing after 2m11s
logging and diagnostic features added
2025-12-15 00:45:14 +07:00

351 lines
8.8 KiB
Go

package backup
import (
"archive/tar"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/models"
)
// Service handles configuration backup and restore operations
type Service struct {
backupDir string
}
// BackupMetadata contains information about a backup
type BackupMetadata struct {
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
Version string `json:"version"`
Description string `json:"description,omitempty"`
Size int64 `json:"size"`
Checksum string `json:"checksum,omitempty"`
}
// BackupData contains all configuration data to be backed up
type BackupData struct {
Metadata BackupMetadata `json:"metadata"`
Users []models.User `json:"users,omitempty"`
SMBShares []models.SMBShare `json:"smb_shares,omitempty"`
NFSExports []models.NFSExport `json:"nfs_exports,omitempty"`
ISCSITargets []models.ISCSITarget `json:"iscsi_targets,omitempty"`
Policies []models.SnapshotPolicy `json:"policies,omitempty"`
Config map[string]interface{} `json:"config,omitempty"`
}
// New creates a new backup service
func New(backupDir string) (*Service, error) {
if err := os.MkdirAll(backupDir, 0755); err != nil {
return nil, fmt.Errorf("create backup directory: %w", err)
}
return &Service{
backupDir: backupDir,
}, nil
}
// CreateBackup creates a backup of all system configurations
func (s *Service) CreateBackup(data BackupData, description string) (string, error) {
// Generate backup ID
backupID := fmt.Sprintf("backup-%d", time.Now().Unix())
backupPath := filepath.Join(s.backupDir, backupID+".tar.gz")
// Set metadata
data.Metadata.ID = backupID
data.Metadata.CreatedAt = time.Now()
data.Metadata.Version = "1.0"
data.Metadata.Description = description
// Create backup file
file, err := os.Create(backupPath)
if err != nil {
return "", fmt.Errorf("create backup file: %w", err)
}
defer file.Close()
// Create gzip writer
gzWriter := gzip.NewWriter(file)
defer gzWriter.Close()
// Create tar writer
tarWriter := tar.NewWriter(gzWriter)
defer tarWriter.Close()
// Write metadata
metadataJSON, err := json.MarshalIndent(data.Metadata, "", " ")
if err != nil {
return "", fmt.Errorf("marshal metadata: %w", err)
}
if err := s.writeFileToTar(tarWriter, "metadata.json", metadataJSON); err != nil {
return "", fmt.Errorf("write metadata: %w", err)
}
// Write configuration data
configJSON, err := json.MarshalIndent(data, "", " ")
if err != nil {
return "", fmt.Errorf("marshal config: %w", err)
}
if err := s.writeFileToTar(tarWriter, "config.json", configJSON); err != nil {
return "", fmt.Errorf("write config: %w", err)
}
// Get file size
stat, err := file.Stat()
if err != nil {
return "", fmt.Errorf("get file stat: %w", err)
}
data.Metadata.Size = stat.Size()
// Update metadata with size
metadataJSON, err = json.MarshalIndent(data.Metadata, "", " ")
if err != nil {
return "", fmt.Errorf("marshal updated metadata: %w", err)
}
// Note: We can't update the tar file, so we'll store metadata separately
metadataPath := filepath.Join(s.backupDir, backupID+".meta.json")
if err := os.WriteFile(metadataPath, metadataJSON, 0644); err != nil {
return "", fmt.Errorf("write metadata file: %w", err)
}
return backupID, nil
}
// writeFileToTar writes a file to a tar archive
func (s *Service) writeFileToTar(tw *tar.Writer, filename string, data []byte) error {
header := &tar.Header{
Name: filename,
Size: int64(len(data)),
Mode: 0644,
ModTime: time.Now(),
}
if err := tw.WriteHeader(header); err != nil {
return err
}
if _, err := tw.Write(data); err != nil {
return err
}
return nil
}
// ListBackups returns a list of all available backups
func (s *Service) ListBackups() ([]BackupMetadata, error) {
files, err := os.ReadDir(s.backupDir)
if err != nil {
return nil, fmt.Errorf("read backup directory: %w", err)
}
var backups []BackupMetadata
for _, file := range files {
if file.IsDir() {
continue
}
if filepath.Ext(file.Name()) != ".json" || !strings.HasSuffix(file.Name(), ".meta.json") {
continue
}
metadataPath := filepath.Join(s.backupDir, file.Name())
data, err := os.ReadFile(metadataPath)
if err != nil {
continue // Skip corrupted metadata files
}
var metadata BackupMetadata
if err := json.Unmarshal(data, &metadata); err != nil {
continue // Skip invalid metadata files
}
// Get actual backup file size if it exists
backupPath := filepath.Join(s.backupDir, metadata.ID+".tar.gz")
if stat, err := os.Stat(backupPath); err == nil {
metadata.Size = stat.Size()
}
backups = append(backups, metadata)
}
return backups, nil
}
// GetBackup returns metadata for a specific backup
func (s *Service) GetBackup(backupID string) (*BackupMetadata, error) {
metadataPath := filepath.Join(s.backupDir, backupID+".meta.json")
data, err := os.ReadFile(metadataPath)
if err != nil {
return nil, fmt.Errorf("read metadata: %w", err)
}
var metadata BackupMetadata
if err := json.Unmarshal(data, &metadata); err != nil {
return nil, fmt.Errorf("unmarshal metadata: %w", err)
}
// Get actual backup file size
backupPath := filepath.Join(s.backupDir, backupID+".tar.gz")
if stat, err := os.Stat(backupPath); err == nil {
metadata.Size = stat.Size()
}
return &metadata, nil
}
// RestoreBackup restores configuration from a backup
func (s *Service) RestoreBackup(backupID string) (*BackupData, error) {
backupPath := filepath.Join(s.backupDir, backupID+".tar.gz")
file, err := os.Open(backupPath)
if err != nil {
return nil, fmt.Errorf("open backup file: %w", err)
}
defer file.Close()
// Create gzip reader
gzReader, err := gzip.NewReader(file)
if err != nil {
return nil, fmt.Errorf("create gzip reader: %w", err)
}
defer gzReader.Close()
// Create tar reader
tarReader := tar.NewReader(gzReader)
var configData []byte
var metadataData []byte
// Extract files from tar
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("read tar: %w", err)
}
switch header.Name {
case "config.json":
configData, err = io.ReadAll(tarReader)
if err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
case "metadata.json":
metadataData, err = io.ReadAll(tarReader)
if err != nil {
return nil, fmt.Errorf("read metadata: %w", err)
}
}
}
if configData == nil {
return nil, fmt.Errorf("config.json not found in backup")
}
var backupData BackupData
if err := json.Unmarshal(configData, &backupData); err != nil {
return nil, fmt.Errorf("unmarshal config: %w", err)
}
// Update metadata if available
if metadataData != nil {
if err := json.Unmarshal(metadataData, &backupData.Metadata); err == nil {
// Metadata loaded successfully
}
}
return &backupData, nil
}
// DeleteBackup deletes a backup file and its metadata
func (s *Service) DeleteBackup(backupID string) error {
backupPath := filepath.Join(s.backupDir, backupID+".tar.gz")
metadataPath := filepath.Join(s.backupDir, backupID+".meta.json")
var errors []error
if err := os.Remove(backupPath); err != nil && !os.IsNotExist(err) {
errors = append(errors, fmt.Errorf("remove backup file: %w", err))
}
if err := os.Remove(metadataPath); err != nil && !os.IsNotExist(err) {
errors = append(errors, fmt.Errorf("remove metadata file: %w", err))
}
if len(errors) > 0 {
return fmt.Errorf("delete backup: %v", errors)
}
return nil
}
// VerifyBackup verifies that a backup file is valid and can be restored
func (s *Service) VerifyBackup(backupID string) error {
backupPath := filepath.Join(s.backupDir, backupID+".tar.gz")
file, err := os.Open(backupPath)
if err != nil {
return fmt.Errorf("open backup file: %w", err)
}
defer file.Close()
// Try to read the backup
gzReader, err := gzip.NewReader(file)
if err != nil {
return fmt.Errorf("invalid gzip format: %w", err)
}
defer gzReader.Close()
tarReader := tar.NewReader(gzReader)
hasConfig := false
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("invalid tar format: %w", err)
}
switch header.Name {
case "config.json":
hasConfig = true
// Try to read and parse config
data, err := io.ReadAll(tarReader)
if err != nil {
return fmt.Errorf("read config: %w", err)
}
var backupData BackupData
if err := json.Unmarshal(data, &backupData); err != nil {
return fmt.Errorf("invalid config format: %w", err)
}
case "metadata.json":
// Metadata is optional, just verify it can be read
_, err := io.ReadAll(tarReader)
if err != nil {
return fmt.Errorf("read metadata: %w", err)
}
}
}
if !hasConfig {
return fmt.Errorf("backup missing config.json")
}
return nil
}