package tape_physical import ( "context" "database/sql" "fmt" "net/http" "github.com/atlasos/calypso/internal/common/database" "github.com/atlasos/calypso/internal/common/logger" "github.com/atlasos/calypso/internal/tasks" "github.com/gin-gonic/gin" ) // Handler handles physical tape library API requests type Handler struct { service *Service taskEngine *tasks.Engine db *database.DB logger *logger.Logger } // NewHandler creates a new physical tape handler func NewHandler(db *database.DB, log *logger.Logger) *Handler { return &Handler{ service: NewService(db, log), taskEngine: tasks.NewEngine(db, log), db: db, logger: log, } } // ListLibraries lists all physical tape libraries func (h *Handler) ListLibraries(c *gin.Context) { query := ` SELECT id, name, serial_number, vendor, model, changer_device_path, changer_stable_path, slot_count, drive_count, is_active, discovered_at, last_inventory_at, created_at, updated_at FROM physical_tape_libraries ORDER BY name ` rows, err := h.db.QueryContext(c.Request.Context(), query) if err != nil { h.logger.Error("Failed to list libraries", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list libraries"}) return } defer rows.Close() var libraries []TapeLibrary for rows.Next() { var lib TapeLibrary var lastInventory sql.NullTime err := rows.Scan( &lib.ID, &lib.Name, &lib.SerialNumber, &lib.Vendor, &lib.Model, &lib.ChangerDevicePath, &lib.ChangerStablePath, &lib.SlotCount, &lib.DriveCount, &lib.IsActive, &lib.DiscoveredAt, &lastInventory, &lib.CreatedAt, &lib.UpdatedAt, ) if err != nil { h.logger.Error("Failed to scan library", "error", err) continue } if lastInventory.Valid { lib.LastInventoryAt = &lastInventory.Time } libraries = append(libraries, lib) } c.JSON(http.StatusOK, gin.H{"libraries": libraries}) } // GetLibrary retrieves a library by ID func (h *Handler) GetLibrary(c *gin.Context) { libraryID := c.Param("id") query := ` SELECT id, name, serial_number, vendor, model, changer_device_path, changer_stable_path, slot_count, drive_count, is_active, discovered_at, last_inventory_at, created_at, updated_at FROM physical_tape_libraries WHERE id = $1 ` var lib TapeLibrary var lastInventory sql.NullTime err := h.db.QueryRowContext(c.Request.Context(), query, libraryID).Scan( &lib.ID, &lib.Name, &lib.SerialNumber, &lib.Vendor, &lib.Model, &lib.ChangerDevicePath, &lib.ChangerStablePath, &lib.SlotCount, &lib.DriveCount, &lib.IsActive, &lib.DiscoveredAt, &lastInventory, &lib.CreatedAt, &lib.UpdatedAt, ) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "library not found"}) return } h.logger.Error("Failed to get library", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get library"}) return } if lastInventory.Valid { lib.LastInventoryAt = &lastInventory.Time } // Get drives drives, _ := h.GetLibraryDrives(c, libraryID) // Get slots slots, _ := h.GetLibrarySlots(c, libraryID) c.JSON(http.StatusOK, gin.H{ "library": lib, "drives": drives, "slots": slots, }) } // DiscoverLibraries discovers physical tape libraries (async) func (h *Handler) DiscoverLibraries(c *gin.Context) { userID, _ := c.Get("user_id") // Create async task taskID, err := h.taskEngine.CreateTask(c.Request.Context(), tasks.TaskTypeRescan, userID.(string), map[string]interface{}{ "operation": "discover_tape_libraries", }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create task"}) return } // Run discovery in background go func() { ctx := c.Request.Context() h.taskEngine.StartTask(ctx, taskID) h.taskEngine.UpdateProgress(ctx, taskID, 30, "Discovering tape libraries...") libraries, err := h.service.DiscoverLibraries(ctx) if err != nil { h.taskEngine.FailTask(ctx, taskID, err.Error()) return } h.taskEngine.UpdateProgress(ctx, taskID, 60, "Syncing libraries to database...") // Sync each library to database for _, lib := range libraries { if err := h.service.SyncLibraryToDatabase(ctx, &lib); err != nil { h.logger.Warn("Failed to sync library", "library", lib.Name, "error", err) continue } // Discover drives for this library if lib.ChangerDevicePath != "" { drives, err := h.service.DiscoverDrives(ctx, lib.ID, lib.ChangerDevicePath) if err == nil { // Sync drives to database for _, drive := range drives { h.syncDriveToDatabase(ctx, &drive) } } } } h.taskEngine.UpdateProgress(ctx, taskID, 100, "Discovery completed") h.taskEngine.CompleteTask(ctx, taskID, "Tape libraries discovered successfully") }() c.JSON(http.StatusAccepted, gin.H{"task_id": taskID}) } // GetLibraryDrives lists drives for a library func (h *Handler) GetLibraryDrives(c *gin.Context, libraryID string) ([]TapeDrive, error) { query := ` SELECT id, library_id, drive_number, device_path, stable_path, vendor, model, serial_number, drive_type, status, current_tape_barcode, is_active, created_at, updated_at FROM physical_tape_drives WHERE library_id = $1 ORDER BY drive_number ` rows, err := h.db.QueryContext(c.Request.Context(), query, libraryID) if err != nil { return nil, err } defer rows.Close() var drives []TapeDrive for rows.Next() { var drive TapeDrive var barcode sql.NullString err := rows.Scan( &drive.ID, &drive.LibraryID, &drive.DriveNumber, &drive.DevicePath, &drive.StablePath, &drive.Vendor, &drive.Model, &drive.SerialNumber, &drive.DriveType, &drive.Status, &barcode, &drive.IsActive, &drive.CreatedAt, &drive.UpdatedAt, ) if err != nil { h.logger.Error("Failed to scan drive", "error", err) continue } if barcode.Valid { drive.CurrentTapeBarcode = barcode.String } drives = append(drives, drive) } return drives, rows.Err() } // GetLibrarySlots lists slots for a library func (h *Handler) GetLibrarySlots(c *gin.Context, libraryID string) ([]TapeSlot, error) { query := ` SELECT id, library_id, slot_number, barcode, tape_present, tape_type, last_updated_at FROM physical_tape_slots WHERE library_id = $1 ORDER BY slot_number ` rows, err := h.db.QueryContext(c.Request.Context(), query, libraryID) if err != nil { return nil, err } defer rows.Close() var slots []TapeSlot for rows.Next() { var slot TapeSlot err := rows.Scan( &slot.ID, &slot.LibraryID, &slot.SlotNumber, &slot.Barcode, &slot.TapePresent, &slot.TapeType, &slot.LastUpdatedAt, ) if err != nil { h.logger.Error("Failed to scan slot", "error", err) continue } slots = append(slots, slot) } return slots, rows.Err() } // PerformInventory performs inventory of a library (async) func (h *Handler) PerformInventory(c *gin.Context) { libraryID := c.Param("id") // Get library var changerPath string err := h.db.QueryRowContext(c.Request.Context(), "SELECT changer_device_path FROM physical_tape_libraries WHERE id = $1", libraryID, ).Scan(&changerPath) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "library not found"}) return } userID, _ := c.Get("user_id") // Create async task taskID, err := h.taskEngine.CreateTask(c.Request.Context(), tasks.TaskTypeInventory, userID.(string), map[string]interface{}{ "operation": "inventory", "library_id": libraryID, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create task"}) return } // Run inventory in background go func() { ctx := c.Request.Context() h.taskEngine.StartTask(ctx, taskID) h.taskEngine.UpdateProgress(ctx, taskID, 50, "Performing inventory...") slots, err := h.service.PerformInventory(ctx, libraryID, changerPath) if err != nil { h.taskEngine.FailTask(ctx, taskID, err.Error()) return } // Sync slots to database for _, slot := range slots { h.syncSlotToDatabase(ctx, &slot) } // Update last inventory time h.db.ExecContext(ctx, "UPDATE physical_tape_libraries SET last_inventory_at = NOW() WHERE id = $1", libraryID, ) h.taskEngine.UpdateProgress(ctx, taskID, 100, "Inventory completed") h.taskEngine.CompleteTask(ctx, taskID, fmt.Sprintf("Inventory completed: %d slots", len(slots))) }() c.JSON(http.StatusAccepted, gin.H{"task_id": taskID}) } // LoadTapeRequest represents a load tape request type LoadTapeRequest struct { SlotNumber int `json:"slot_number" binding:"required"` DriveNumber int `json:"drive_number" binding:"required"` } // LoadTape loads a tape from slot to drive (async) func (h *Handler) LoadTape(c *gin.Context) { libraryID := c.Param("id") var req LoadTapeRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Get library var changerPath string err := h.db.QueryRowContext(c.Request.Context(), "SELECT changer_device_path FROM physical_tape_libraries WHERE id = $1", libraryID, ).Scan(&changerPath) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "library not found"}) return } userID, _ := c.Get("user_id") // Create async task taskID, err := h.taskEngine.CreateTask(c.Request.Context(), tasks.TaskTypeLoadUnload, userID.(string), map[string]interface{}{ "operation": "load_tape", "library_id": libraryID, "slot_number": req.SlotNumber, "drive_number": req.DriveNumber, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create task"}) return } // Run load in background go func() { ctx := c.Request.Context() h.taskEngine.StartTask(ctx, taskID) h.taskEngine.UpdateProgress(ctx, taskID, 50, "Loading tape...") if err := h.service.LoadTape(ctx, libraryID, changerPath, req.SlotNumber, req.DriveNumber); err != nil { h.taskEngine.FailTask(ctx, taskID, err.Error()) return } // Update drive status h.db.ExecContext(ctx, "UPDATE physical_tape_drives SET status = 'ready', updated_at = NOW() WHERE library_id = $1 AND drive_number = $2", libraryID, req.DriveNumber, ) h.taskEngine.UpdateProgress(ctx, taskID, 100, "Tape loaded") h.taskEngine.CompleteTask(ctx, taskID, "Tape loaded successfully") }() c.JSON(http.StatusAccepted, gin.H{"task_id": taskID}) } // UnloadTapeRequest represents an unload tape request type UnloadTapeRequest struct { DriveNumber int `json:"drive_number" binding:"required"` SlotNumber int `json:"slot_number" binding:"required"` } // UnloadTape unloads a tape from drive to slot (async) func (h *Handler) UnloadTape(c *gin.Context) { libraryID := c.Param("id") var req UnloadTapeRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Get library var changerPath string err := h.db.QueryRowContext(c.Request.Context(), "SELECT changer_device_path FROM physical_tape_libraries WHERE id = $1", libraryID, ).Scan(&changerPath) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "library not found"}) return } userID, _ := c.Get("user_id") // Create async task taskID, err := h.taskEngine.CreateTask(c.Request.Context(), tasks.TaskTypeLoadUnload, userID.(string), map[string]interface{}{ "operation": "unload_tape", "library_id": libraryID, "slot_number": req.SlotNumber, "drive_number": req.DriveNumber, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create task"}) return } // Run unload in background go func() { ctx := c.Request.Context() h.taskEngine.StartTask(ctx, taskID) h.taskEngine.UpdateProgress(ctx, taskID, 50, "Unloading tape...") if err := h.service.UnloadTape(ctx, libraryID, changerPath, req.DriveNumber, req.SlotNumber); err != nil { h.taskEngine.FailTask(ctx, taskID, err.Error()) return } // Update drive status h.db.ExecContext(ctx, "UPDATE physical_tape_drives SET status = 'idle', current_tape_barcode = NULL, updated_at = NOW() WHERE library_id = $1 AND drive_number = $2", libraryID, req.DriveNumber, ) h.taskEngine.UpdateProgress(ctx, taskID, 100, "Tape unloaded") h.taskEngine.CompleteTask(ctx, taskID, "Tape unloaded successfully") }() c.JSON(http.StatusAccepted, gin.H{"task_id": taskID}) } // syncDriveToDatabase syncs a drive to the database func (h *Handler) syncDriveToDatabase(ctx context.Context, drive *TapeDrive) { query := ` INSERT INTO physical_tape_drives ( library_id, drive_number, device_path, stable_path, vendor, model, serial_number, drive_type, status, is_active ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (library_id, drive_number) DO UPDATE SET device_path = EXCLUDED.device_path, stable_path = EXCLUDED.stable_path, vendor = EXCLUDED.vendor, model = EXCLUDED.model, serial_number = EXCLUDED.serial_number, drive_type = EXCLUDED.drive_type, updated_at = NOW() ` h.db.ExecContext(ctx, query, drive.LibraryID, drive.DriveNumber, drive.DevicePath, drive.StablePath, drive.Vendor, drive.Model, drive.SerialNumber, drive.DriveType, drive.Status, drive.IsActive, ) } // syncSlotToDatabase syncs a slot to the database func (h *Handler) syncSlotToDatabase(ctx context.Context, slot *TapeSlot) { query := ` INSERT INTO physical_tape_slots ( library_id, slot_number, barcode, tape_present, tape_type, last_updated_at ) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (library_id, slot_number) DO UPDATE SET barcode = EXCLUDED.barcode, tape_present = EXCLUDED.tape_present, tape_type = EXCLUDED.tape_type, last_updated_at = EXCLUDED.last_updated_at ` h.db.ExecContext(ctx, query, slot.LibraryID, slot.SlotNumber, slot.Barcode, slot.TapePresent, slot.TapeType, slot.LastUpdatedAt, ) }