package tape import ( "fmt" "os/exec" "strconv" "strings" "github.com/bams/backend/internal/config" "github.com/bams/backend/internal/logger" ) type Library struct { Status string `json:"status"` TotalSlots int `json:"total_slots"` ActiveDrives int `json:"active_drives"` Model string `json:"model"` Serial string `json:"serial"` } type Drive struct { ID string `json:"id"` Status string `json:"status"` LoadedTape string `json:"loaded_tape,omitempty"` Barcode string `json:"barcode,omitempty"` Position int `json:"position"` } type Slot struct { Number int `json:"number"` Barcode string `json:"barcode,omitempty"` Status string `json:"status"` // "empty", "loaded", "import" } type Service struct { config *config.Config logger *logger.Logger } func NewService(cfg *config.Config, log *logger.Logger) *Service { return &Service{ config: cfg, logger: log, } } func (s *Service) GetLibrary() (*Library, error) { s.logger.Debug("Getting tape library info") // Try to detect library using mtx or sg_lib library := &Library{ Status: "unknown", TotalSlots: 0, ActiveDrives: 0, } // Check for mtx (media changer) cmd := exec.Command("mtx", "-f", "/dev/sg0", "status") output, err := cmd.CombinedOutput() if err == nil { // Parse mtx output lines := strings.Split(string(output), "\n") for _, line := range lines { if strings.Contains(line, "Storage Element") { library.TotalSlots++ } if strings.Contains(line, "Data Transfer Element") { library.ActiveDrives++ } } library.Status = "online" } return library, nil } func (s *Service) RunInventory() error { s.logger.Info("Running tape library inventory") // Use mtx to inventory cmd := exec.Command("mtx", "-f", "/dev/sg0", "inventory") output, err := cmd.CombinedOutput() if err != nil { s.logger.Error("Failed to run inventory", "error", string(output)) return fmt.Errorf("inventory failed: %w", err) } return nil } func (s *Service) ListDrives() ([]*Drive, error) { s.logger.Debug("Listing tape drives") drives := []*Drive{} // Detect drives (up to 8) for i := 0; i < 8; i++ { device := fmt.Sprintf("/dev/nst%d", i) cmd := exec.Command("mt", "-f", device, "status") output, err := cmd.CombinedOutput() if err == nil { drive := &Drive{ ID: fmt.Sprintf("drive-%d", i), Status: "online", Position: i, } // Check if tape is loaded if strings.Contains(string(output), "ONLINE") { drive.Status = "loaded" } drives = append(drives, drive) } } return drives, nil } func (s *Service) LoadTape(driveID string, slot int) error { s.logger.Info("Loading tape", "drive", driveID, "slot", slot) // Extract drive number from ID driveNum := 0 fmt.Sscanf(driveID, "drive-%d", &driveNum) // Use mtx to load cmd := exec.Command("mtx", "-f", "/dev/sg0", "load", strconv.Itoa(slot), strconv.Itoa(driveNum)) output, err := cmd.CombinedOutput() if err != nil { s.logger.Error("Failed to load tape", "error", string(output)) return fmt.Errorf("load failed: %w", err) } return nil } func (s *Service) UnloadTape(driveID string, slot int) error { s.logger.Info("Unloading tape", "drive", driveID, "slot", slot) // Extract drive number from ID driveNum := 0 fmt.Sscanf(driveID, "drive-%d", &driveNum) // Use mtx to unload cmd := exec.Command("mtx", "-f", "/dev/sg0", "unload", strconv.Itoa(slot), strconv.Itoa(driveNum)) output, err := cmd.CombinedOutput() if err != nil { s.logger.Error("Failed to unload tape", "error", string(output)) return fmt.Errorf("unload failed: %w", err) } return nil } func (s *Service) ListSlots() ([]*Slot, error) { s.logger.Debug("Listing tape slots") slots := []*Slot{} // Use mtx to get slot status cmd := exec.Command("mtx", "-f", "/dev/sg0", "status") output, err := cmd.CombinedOutput() if err != nil { return slots, fmt.Errorf("failed to get slot status: %w", err) } // Parse mtx output lines := strings.Split(string(output), "\n") for _, line := range lines { if strings.Contains(line, "Storage Element") { // Parse slot information slot := &Slot{ Status: "empty", } // Extract slot number and barcode from line // This is simplified - real parsing would be more complex slots = append(slots, slot) } } return slots, nil }