Files
atlas/internal/httpapp/router_helpers.go
2025-12-23 07:50:08 +00:00

425 lines
14 KiB
Go

package httpapp
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/errors"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/models"
)
// methodHandler routes requests based on HTTP method
func methodHandler(get, post, put, delete, patch http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
if get != nil {
get(w, r)
} else {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
case http.MethodPost:
if post != nil {
post(w, r)
} else {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
case http.MethodPut:
if put != nil {
put(w, r)
} else {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
case http.MethodDelete:
if delete != nil {
delete(w, r)
} else {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
case http.MethodPatch:
if patch != nil {
patch(w, r)
} else {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
}
// pathParam extracts the last segment from a path
func pathParam(r *http.Request, prefix string) string {
path := strings.TrimPrefix(r.URL.Path, prefix)
path = strings.Trim(path, "/")
parts := strings.Split(path, "/")
if len(parts) > 0 {
return parts[len(parts)-1]
}
return ""
}
// pathParamFull extracts the full path after prefix (for dataset/zvol names that may contain slashes)
func pathParamFull(r *http.Request, prefix string) string {
path := strings.TrimPrefix(r.URL.Path, prefix)
path = strings.Trim(path, "/")
if path == "" {
return ""
}
// URL decode the path
decoded, err := url.PathUnescape(path)
if err != nil {
// If decoding fails, return original path
return path
}
return decoded
}
// handlePoolOps routes pool operations by method
func (a *App) handlePoolOps(w http.ResponseWriter, r *http.Request) {
// Extract pool name from path like /api/v1/pools/tank
name := pathParam(r, "/api/v1/pools/")
if name == "" {
writeError(w, errors.ErrBadRequest("pool name required"))
return
}
if strings.HasSuffix(r.URL.Path, "/scrub") {
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
if r.Method == http.MethodPost {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleScrubPool)).ServeHTTP(w, r)
} else if r.Method == http.MethodGet {
a.handleGetScrubStatus(w, r)
} else {
writeError(w, errors.NewAPIError(errors.ErrCodeBadRequest, "method not allowed", http.StatusMethodNotAllowed))
}
return
}
if strings.HasSuffix(r.URL.Path, "/export") {
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
if r.Method == http.MethodPost {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleExportPool)).ServeHTTP(w, r)
} else {
writeError(w, errors.NewAPIError(errors.ErrCodeBadRequest, "method not allowed", http.StatusMethodNotAllowed))
}
return
}
if strings.HasSuffix(r.URL.Path, "/spare") {
if r.Method == http.MethodPost {
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleAddSpareDisk)).ServeHTTP(w, r)
} else {
writeError(w, errors.NewAPIError(errors.ErrCodeBadRequest, "method not allowed", http.StatusMethodNotAllowed))
}
return
}
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetPool(w, r) },
nil,
nil,
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeletePool)).ServeHTTP(w, r)
},
nil,
)(w, r)
}
// handleDatasetOps routes dataset operations by method
func (a *App) handleDatasetOps(w http.ResponseWriter, r *http.Request) {
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetDataset(w, r) },
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateDataset)).ServeHTTP(w, r)
},
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleUpdateDataset)).ServeHTTP(w, r)
},
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteDataset)).ServeHTTP(w, r)
},
nil,
)(w, r)
}
// handleZVOLOps routes ZVOL operations by method
func (a *App) handleZVOLOps(w http.ResponseWriter, r *http.Request) {
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetZVOL(w, r) },
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateZVOL)).ServeHTTP(w, r)
},
nil,
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteZVOL)).ServeHTTP(w, r)
},
nil,
)(w, r)
}
// handleSnapshotOps routes snapshot operations by method
func (a *App) handleSnapshotOps(w http.ResponseWriter, r *http.Request) {
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
// Check if it's a restore operation
if strings.HasSuffix(r.URL.Path, "/restore") {
if r.Method == http.MethodPost {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleRestoreSnapshot)).ServeHTTP(w, r)
} else {
writeError(w, errors.ErrBadRequest("method not allowed"))
}
return
}
methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetSnapshot(w, r) },
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateSnapshot)).ServeHTTP(w, r)
},
nil,
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteSnapshot)).ServeHTTP(w, r)
},
nil,
)(w, r)
}
// handleSnapshotPolicyOps routes snapshot policy operations by method
func (a *App) handleSnapshotPolicyOps(w http.ResponseWriter, r *http.Request) {
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetSnapshotPolicy(w, r) },
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateSnapshotPolicy)).ServeHTTP(w, r)
},
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleUpdateSnapshotPolicy)).ServeHTTP(w, r)
},
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteSnapshotPolicy)).ServeHTTP(w, r)
},
nil,
)(w, r)
}
// handleSMBShareOps routes SMB share operations by method
func (a *App) handleSMBShareOps(w http.ResponseWriter, r *http.Request) {
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetSMBShare(w, r) },
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateSMBShare)).ServeHTTP(w, r)
},
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleUpdateSMBShare)).ServeHTTP(w, r)
},
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteSMBShare)).ServeHTTP(w, r)
},
nil,
)(w, r)
}
// handleNFSExportOps routes NFS export operations by method
func (a *App) handleNFSExportOps(w http.ResponseWriter, r *http.Request) {
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetNFSExport(w, r) },
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateNFSExport)).ServeHTTP(w, r)
},
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleUpdateNFSExport)).ServeHTTP(w, r)
},
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteNFSExport)).ServeHTTP(w, r)
},
nil,
)(w, r)
}
// handleISCSITargetOps routes iSCSI target operations by method
func (a *App) handleBackupOps(w http.ResponseWriter, r *http.Request) {
backupID := pathParam(r, "/api/v1/backups/")
if backupID == "" {
writeError(w, errors.ErrBadRequest("backup id required"))
return
}
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
switch r.Method {
case http.MethodGet:
// Check if it's a verify request
if r.URL.Query().Get("verify") == "true" {
a.handleVerifyBackup(w, r)
} else {
a.handleGetBackup(w, r)
}
case http.MethodPost:
// Restore backup (POST /api/v1/backups/{id}/restore)
if strings.HasSuffix(r.URL.Path, "/restore") {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleRestoreBackup)).ServeHTTP(w, r)
} else {
writeError(w, errors.ErrBadRequest("invalid backup operation"))
}
case http.MethodDelete:
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteBackup)).ServeHTTP(w, r)
default:
writeError(w, errors.ErrBadRequest("method not allowed"))
}
}
func (a *App) handleISCSITargetOps(w http.ResponseWriter, r *http.Request) {
id := pathParam(r, "/api/v1/iscsi/targets/")
if id == "" {
writeError(w, errors.ErrBadRequest("target id required"))
return
}
if strings.HasSuffix(r.URL.Path, "/connection") {
if r.Method == http.MethodGet {
a.handleGetISCSIConnectionInstructions(w, r)
} else {
writeError(w, errors.NewAPIError(errors.ErrCodeBadRequest, "method not allowed", http.StatusMethodNotAllowed))
}
return
}
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
if strings.HasSuffix(r.URL.Path, "/luns") {
if r.Method == http.MethodPost {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleAddLUN)).ServeHTTP(w, r)
return
}
writeError(w, errors.NewAPIError(errors.ErrCodeBadRequest, "method not allowed", http.StatusMethodNotAllowed))
return
}
if strings.HasSuffix(r.URL.Path, "/luns/remove") {
if r.Method == http.MethodPost {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleRemoveLUN)).ServeHTTP(w, r)
return
}
writeError(w, errors.NewAPIError(errors.ErrCodeBadRequest, "method not allowed", http.StatusMethodNotAllowed))
return
}
methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetISCSITarget(w, r) },
nil,
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleUpdateISCSITarget)).ServeHTTP(w, r)
},
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteISCSITarget)).ServeHTTP(w, r)
},
nil,
)(w, r)
}
// handleJobOps routes job operations by method
func (a *App) handleJobOps(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/cancel") {
if r.Method == http.MethodPost {
a.handleCancelJob(w, r)
return
}
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetJob(w, r) },
nil,
nil,
nil,
nil,
)(w, r)
}
// handleUserOps routes user operations by method
func (a *App) handleUserOps(w http.ResponseWriter, r *http.Request) {
methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetUser(w, r) },
func(w http.ResponseWriter, r *http.Request) { a.handleCreateUser(w, r) },
func(w http.ResponseWriter, r *http.Request) { a.handleUpdateUser(w, r) },
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteUser(w, r) },
nil,
)(w, r)
}
// handleVTLDriveOps routes VTL drive operations by method
func (a *App) handleVTLDriveOps(w http.ResponseWriter, r *http.Request) {
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetVTLDrive(w, r) },
nil,
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleUpdateVTLDrive)).ServeHTTP(w, r)
},
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteVTLDrive)).ServeHTTP(w, r)
},
nil,
)(w, r)
}
// handleVTLTapeOps routes VTL tape operations by method
func (a *App) handleVTLTapeOps(w http.ResponseWriter, r *http.Request) {
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetVTLTape(w, r) },
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateVTLTape)).ServeHTTP(w, r)
},
nil,
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteVTLTape)).ServeHTTP(w, r)
},
nil,
)(w, r)
}
// handleMediaChangerOps routes media changer operations by method
func (a *App) handleMediaChangerOps(w http.ResponseWriter, r *http.Request) {
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
methodHandler(
func(w http.ResponseWriter, r *http.Request) {
// Get single changer by ID
libraryIDStr := pathParam(r, "/api/v1/vtl/changers/")
libraryID, err := strconv.Atoi(libraryIDStr)
if err != nil || libraryID <= 0 {
writeError(w, errors.ErrValidation("invalid library_id"))
return
}
changers, err := a.vtlService.ListMediaChangers()
if err != nil {
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to list changers: %v", err)))
return
}
for _, changer := range changers {
if changer.LibraryID == libraryID {
writeJSON(w, http.StatusOK, changer)
return
}
}
writeError(w, errors.ErrNotFound(fmt.Sprintf("media changer %d not found", libraryID)))
},
nil,
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleUpdateMediaChanger)).ServeHTTP(w, r)
},
func(w http.ResponseWriter, r *http.Request) {
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteMediaChanger)).ServeHTTP(w, r)
},
nil,
)(w, r)
}