This commit is contained in:
350
internal/backup/service.go
Normal file
350
internal/backup/service.go
Normal file
@@ -0,0 +1,350 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user