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 }