From 6b706f9ccdf46046133c867c4240c4e8594da5b3 Mon Sep 17 00:00:00 2001 From: chrislu Date: Wed, 2 Jul 2025 00:04:46 -0700 Subject: rename files *_server.go - main server files *_management.go - business logic *_data.go - data structures and types *_middleware.go - middleware logic --- weed/admin/dash/admin_data.go | 280 +++++++++++++++++++++++++++ weed/admin/dash/auth_middleware.go | 53 +++++ weed/admin/dash/bucket_handlers.go | 325 ------------------------------- weed/admin/dash/bucket_management.go | 325 +++++++++++++++++++++++++++++++ weed/admin/dash/file_browser.go | 350 ---------------------------------- weed/admin/dash/file_browser_data.go | 350 ++++++++++++++++++++++++++++++++++ weed/admin/dash/handler_admin.go | 280 --------------------------- weed/admin/dash/handler_auth.go | 53 ----- weed/admin/handlers/admin_handlers.go | 316 ++++++++++++++++++++++++++++++ weed/admin/handlers/auth.go | 45 ----- weed/admin/handlers/auth_handlers.go | 45 +++++ weed/admin/handlers/handlers.go | 316 ------------------------------ 12 files changed, 1369 insertions(+), 1369 deletions(-) create mode 100644 weed/admin/dash/admin_data.go create mode 100644 weed/admin/dash/auth_middleware.go delete mode 100644 weed/admin/dash/bucket_handlers.go create mode 100644 weed/admin/dash/bucket_management.go delete mode 100644 weed/admin/dash/file_browser.go create mode 100644 weed/admin/dash/file_browser_data.go delete mode 100644 weed/admin/dash/handler_admin.go delete mode 100644 weed/admin/dash/handler_auth.go create mode 100644 weed/admin/handlers/admin_handlers.go delete mode 100644 weed/admin/handlers/auth.go create mode 100644 weed/admin/handlers/auth_handlers.go delete mode 100644 weed/admin/handlers/handlers.go diff --git a/weed/admin/dash/admin_data.go b/weed/admin/dash/admin_data.go new file mode 100644 index 000000000..ae218de75 --- /dev/null +++ b/weed/admin/dash/admin_data.go @@ -0,0 +1,280 @@ +package dash + +import ( + "context" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/seaweedfs/seaweedfs/weed/cluster" + "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" +) + +type AdminData struct { + Username string `json:"username"` + ClusterStatus string `json:"cluster_status"` + TotalVolumes int `json:"total_volumes"` + TotalFiles int64 `json:"total_files"` + TotalSize int64 `json:"total_size"` + MasterNodes []MasterNode `json:"master_nodes"` + VolumeServers []VolumeServer `json:"volume_servers"` + FilerNodes []FilerNode `json:"filer_nodes"` + DataCenters []DataCenter `json:"datacenters"` + LastUpdated time.Time `json:"last_updated"` + SystemHealth string `json:"system_health"` +} + +// Object Store Users management structures +type ObjectStoreUser struct { + Username string `json:"username"` + Email string `json:"email"` + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` + Permissions []string `json:"permissions"` +} + +type ObjectStoreUsersData struct { + Username string `json:"username"` + Users []ObjectStoreUser `json:"users"` + TotalUsers int `json:"total_users"` + LastUpdated time.Time `json:"last_updated"` +} + +// User management request structures +type CreateUserRequest struct { + Username string `json:"username" binding:"required"` + Email string `json:"email"` + Actions []string `json:"actions"` + GenerateKey bool `json:"generate_key"` +} + +type UpdateUserRequest struct { + Email string `json:"email"` + Actions []string `json:"actions"` +} + +type UpdateUserPoliciesRequest struct { + Actions []string `json:"actions" binding:"required"` +} + +type AccessKeyInfo struct { + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` + CreatedAt time.Time `json:"created_at"` +} + +type UserDetails struct { + Username string `json:"username"` + Email string `json:"email"` + Actions []string `json:"actions"` + AccessKeys []AccessKeyInfo `json:"access_keys"` +} + +type FilerNode struct { + Address string `json:"address"` + DataCenter string `json:"datacenter"` + Rack string `json:"rack"` + Status string `json:"status"` + LastUpdated time.Time `json:"last_updated"` +} + +// GetAdminData retrieves admin data as a struct (for reuse by both JSON and HTML handlers) +func (s *AdminServer) GetAdminData(username string) (AdminData, error) { + if username == "" { + username = "admin" + } + + // Get cluster topology + topology, err := s.GetClusterTopology() + if err != nil { + glog.Errorf("Failed to get cluster topology: %v", err) + return AdminData{}, err + } + + // Get master nodes status + masterNodes := s.getMasterNodesStatus() + + // Get filer nodes status + filerNodes := s.getFilerNodesStatus() + + // Prepare admin data + adminData := AdminData{ + Username: username, + ClusterStatus: s.determineClusterStatus(topology, masterNodes), + TotalVolumes: topology.TotalVolumes, + TotalFiles: topology.TotalFiles, + TotalSize: topology.TotalSize, + MasterNodes: masterNodes, + VolumeServers: topology.VolumeServers, + FilerNodes: filerNodes, + DataCenters: topology.DataCenters, + LastUpdated: topology.UpdatedAt, + SystemHealth: s.determineSystemHealth(topology, masterNodes), + } + + return adminData, nil +} + +// ShowAdmin displays the main admin page (now uses GetAdminData) +func (s *AdminServer) ShowAdmin(c *gin.Context) { + username := c.GetString("username") + + adminData, err := s.GetAdminData(username) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get admin data: " + err.Error()}) + return + } + + // Return JSON for API calls + c.JSON(http.StatusOK, adminData) +} + +// ShowOverview displays cluster overview +func (s *AdminServer) ShowOverview(c *gin.Context) { + topology, err := s.GetClusterTopology() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, topology) +} + +// getMasterNodesStatus checks status of all master nodes +func (s *AdminServer) getMasterNodesStatus() []MasterNode { + var masterNodes []MasterNode + + // Since we have a single master address, create one entry + var isLeader bool = true // Assume leader since it's the only master we know about + var status string + + // Try to get leader info from this master + err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { + _, err := client.GetMasterConfiguration(context.Background(), &master_pb.GetMasterConfigurationRequest{}) + if err != nil { + return err + } + // For now, assume this master is the leader since we can connect to it + isLeader = true + return nil + }) + + if err != nil { + status = "unreachable" + isLeader = false + } else { + status = "active" + } + + masterNodes = append(masterNodes, MasterNode{ + Address: s.masterAddress, + IsLeader: isLeader, + Status: status, + }) + + return masterNodes +} + +// getFilerNodesStatus checks status of all filer nodes using master's ListClusterNodes +func (s *AdminServer) getFilerNodesStatus() []FilerNode { + var filerNodes []FilerNode + + // Get filer nodes from master using ListClusterNodes + err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { + resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{ + ClientType: cluster.FilerType, + }) + if err != nil { + return err + } + + // Process each filer node + for _, node := range resp.ClusterNodes { + filerNodes = append(filerNodes, FilerNode{ + Address: node.Address, + DataCenter: node.DataCenter, + Rack: node.Rack, + Status: "active", // If it's in the cluster list, it's considered active + LastUpdated: time.Now(), + }) + } + + return nil + }) + + if err != nil { + glog.Errorf("Failed to get filer nodes from master %s: %v", s.masterAddress, err) + // Return empty list if we can't get filer info from master + return []FilerNode{} + } + + return filerNodes +} + +// determineClusterStatus analyzes cluster health +func (s *AdminServer) determineClusterStatus(topology *ClusterTopology, masters []MasterNode) string { + // Check if we have an active leader + hasActiveLeader := false + for _, master := range masters { + if master.IsLeader && master.Status == "active" { + hasActiveLeader = true + break + } + } + + if !hasActiveLeader { + return "critical" + } + + // Check volume server health + activeServers := 0 + for _, vs := range topology.VolumeServers { + if vs.Status == "active" { + activeServers++ + } + } + + if activeServers == 0 { + return "critical" + } else if activeServers < len(topology.VolumeServers) { + return "warning" + } + + return "healthy" +} + +// determineSystemHealth provides overall system health assessment +func (s *AdminServer) determineSystemHealth(topology *ClusterTopology, masters []MasterNode) string { + // Simple health calculation based on active components + totalComponents := len(masters) + len(topology.VolumeServers) + activeComponents := 0 + + for _, master := range masters { + if master.Status == "active" { + activeComponents++ + } + } + + for _, vs := range topology.VolumeServers { + if vs.Status == "active" { + activeComponents++ + } + } + + if totalComponents == 0 { + return "unknown" + } + + healthPercent := float64(activeComponents) / float64(totalComponents) * 100 + + if healthPercent >= 95 { + return "excellent" + } else if healthPercent >= 80 { + return "good" + } else if healthPercent >= 60 { + return "fair" + } else { + return "poor" + } +} diff --git a/weed/admin/dash/auth_middleware.go b/weed/admin/dash/auth_middleware.go new file mode 100644 index 000000000..986a30290 --- /dev/null +++ b/weed/admin/dash/auth_middleware.go @@ -0,0 +1,53 @@ +package dash + +import ( + "net/http" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" +) + +// ShowLogin displays the login page +func (s *AdminServer) ShowLogin(c *gin.Context) { + // If authentication is not required, redirect to admin + session := sessions.Default(c) + if session.Get("authenticated") == true { + c.Redirect(http.StatusSeeOther, "/admin") + return + } + + // For now, return a simple login form as JSON + c.HTML(http.StatusOK, "login.html", gin.H{ + "title": "SeaweedFS Admin Login", + "error": c.Query("error"), + }) +} + +// HandleLogin handles login form submission +func (s *AdminServer) HandleLogin(username, password string) gin.HandlerFunc { + return func(c *gin.Context) { + loginUsername := c.PostForm("username") + loginPassword := c.PostForm("password") + + if loginUsername == username && loginPassword == password { + session := sessions.Default(c) + session.Set("authenticated", true) + session.Set("username", loginUsername) + session.Save() + + c.Redirect(http.StatusSeeOther, "/admin") + return + } + + // Authentication failed + c.Redirect(http.StatusSeeOther, "/login?error=Invalid credentials") + } +} + +// HandleLogout handles user logout +func (s *AdminServer) HandleLogout(c *gin.Context) { + session := sessions.Default(c) + session.Clear() + session.Save() + c.Redirect(http.StatusSeeOther, "/login") +} diff --git a/weed/admin/dash/bucket_handlers.go b/weed/admin/dash/bucket_handlers.go deleted file mode 100644 index 7fbd74a1e..000000000 --- a/weed/admin/dash/bucket_handlers.go +++ /dev/null @@ -1,325 +0,0 @@ -package dash - -import ( - "context" - "fmt" - "net/http" - "os" - "strings" - "time" - - "github.com/gin-gonic/gin" - "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" -) - -// S3 Bucket management data structures for templates -type S3BucketsData struct { - Username string `json:"username"` - Buckets []S3Bucket `json:"buckets"` - TotalBuckets int `json:"total_buckets"` - TotalSize int64 `json:"total_size"` - LastUpdated time.Time `json:"last_updated"` -} - -type CreateBucketRequest struct { - Name string `json:"name" binding:"required"` - Region string `json:"region"` - QuotaSize int64 `json:"quota_size"` // Quota size in bytes - QuotaUnit string `json:"quota_unit"` // Unit: MB, GB, TB - QuotaEnabled bool `json:"quota_enabled"` // Whether quota is enabled -} - -// S3 Bucket Management Handlers - -// ShowS3Buckets displays the Object Store buckets management page -func (s *AdminServer) ShowS3Buckets(c *gin.Context) { - username := c.GetString("username") - - buckets, err := s.GetS3Buckets() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get Object Store buckets: " + err.Error()}) - return - } - - // Calculate totals - var totalSize int64 - for _, bucket := range buckets { - totalSize += bucket.Size - } - - data := S3BucketsData{ - Username: username, - Buckets: buckets, - TotalBuckets: len(buckets), - TotalSize: totalSize, - LastUpdated: time.Now(), - } - - c.JSON(http.StatusOK, data) -} - -// ShowBucketDetails displays detailed information about a specific bucket -func (s *AdminServer) ShowBucketDetails(c *gin.Context) { - bucketName := c.Param("bucket") - if bucketName == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) - return - } - - details, err := s.GetBucketDetails(bucketName) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bucket details: " + err.Error()}) - return - } - - c.JSON(http.StatusOK, details) -} - -// CreateBucket creates a new S3 bucket -func (s *AdminServer) CreateBucket(c *gin.Context) { - var req CreateBucketRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) - return - } - - // Validate bucket name (basic validation) - if len(req.Name) < 3 || len(req.Name) > 63 { - c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name must be between 3 and 63 characters"}) - return - } - - // Convert quota to bytes - quotaBytes := convertQuotaToBytes(req.QuotaSize, req.QuotaUnit) - - err := s.CreateS3BucketWithQuota(req.Name, quotaBytes, req.QuotaEnabled) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bucket: " + err.Error()}) - return - } - - c.JSON(http.StatusCreated, gin.H{ - "message": "Bucket created successfully", - "bucket": req.Name, - "quota_size": req.QuotaSize, - "quota_unit": req.QuotaUnit, - "quota_enabled": req.QuotaEnabled, - }) -} - -// UpdateBucketQuota updates the quota settings for a bucket -func (s *AdminServer) UpdateBucketQuota(c *gin.Context) { - bucketName := c.Param("bucket") - if bucketName == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) - return - } - - var req struct { - QuotaSize int64 `json:"quota_size"` - QuotaUnit string `json:"quota_unit"` - QuotaEnabled bool `json:"quota_enabled"` - } - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) - return - } - - // Convert quota to bytes - quotaBytes := convertQuotaToBytes(req.QuotaSize, req.QuotaUnit) - - err := s.SetBucketQuota(bucketName, quotaBytes, req.QuotaEnabled) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update bucket quota: " + err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "message": "Bucket quota updated successfully", - "bucket": bucketName, - "quota_size": req.QuotaSize, - "quota_unit": req.QuotaUnit, - "quota_enabled": req.QuotaEnabled, - }) -} - -// DeleteBucket deletes an S3 bucket -func (s *AdminServer) DeleteBucket(c *gin.Context) { - bucketName := c.Param("bucket") - if bucketName == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) - return - } - - err := s.DeleteS3Bucket(bucketName) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete bucket: " + err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "message": "Bucket deleted successfully", - "bucket": bucketName, - }) -} - -// ListBucketsAPI returns the list of buckets as JSON -func (s *AdminServer) ListBucketsAPI(c *gin.Context) { - buckets, err := s.GetS3Buckets() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get buckets: " + err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "buckets": buckets, - "total": len(buckets), - }) -} - -// Helper function to convert quota size and unit to bytes -func convertQuotaToBytes(size int64, unit string) int64 { - if size <= 0 { - return 0 - } - - switch strings.ToUpper(unit) { - case "TB": - return size * 1024 * 1024 * 1024 * 1024 - case "GB": - return size * 1024 * 1024 * 1024 - case "MB": - return size * 1024 * 1024 - default: - // Default to MB if unit is not recognized - return size * 1024 * 1024 - } -} - -// Helper function to convert bytes to appropriate unit and size -func convertBytesToQuota(bytes int64) (int64, string) { - if bytes == 0 { - return 0, "MB" - } - - // Convert to TB if >= 1TB - if bytes >= 1024*1024*1024*1024 && bytes%(1024*1024*1024*1024) == 0 { - return bytes / (1024 * 1024 * 1024 * 1024), "TB" - } - - // Convert to GB if >= 1GB - if bytes >= 1024*1024*1024 && bytes%(1024*1024*1024) == 0 { - return bytes / (1024 * 1024 * 1024), "GB" - } - - // Convert to MB (default) - return bytes / (1024 * 1024), "MB" -} - -// SetBucketQuota sets the quota for a bucket -func (s *AdminServer) SetBucketQuota(bucketName string, quotaBytes int64, quotaEnabled bool) error { - return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { - // Get the current bucket entry - lookupResp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ - Directory: "/buckets", - Name: bucketName, - }) - if err != nil { - return fmt.Errorf("bucket not found: %v", err) - } - - bucketEntry := lookupResp.Entry - - // Determine quota value (negative if disabled) - var quota int64 - if quotaEnabled && quotaBytes > 0 { - quota = quotaBytes - } else if !quotaEnabled && quotaBytes > 0 { - quota = -quotaBytes - } else { - quota = 0 - } - - // Update the quota - bucketEntry.Quota = quota - - // Update the entry - _, err = client.UpdateEntry(context.Background(), &filer_pb.UpdateEntryRequest{ - Directory: "/buckets", - Entry: bucketEntry, - }) - if err != nil { - return fmt.Errorf("failed to update bucket quota: %v", err) - } - - return nil - }) -} - -// CreateS3BucketWithQuota creates a new S3 bucket with quota settings -func (s *AdminServer) CreateS3BucketWithQuota(bucketName string, quotaBytes int64, quotaEnabled bool) error { - return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { - // First ensure /buckets directory exists - _, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ - Directory: "/", - Entry: &filer_pb.Entry{ - Name: "buckets", - IsDirectory: true, - Attributes: &filer_pb.FuseAttributes{ - FileMode: uint32(0755 | os.ModeDir), // Directory mode - Uid: uint32(1000), - Gid: uint32(1000), - Crtime: time.Now().Unix(), - Mtime: time.Now().Unix(), - TtlSec: 0, - }, - }, - }) - // Ignore error if directory already exists - if err != nil && !strings.Contains(err.Error(), "already exists") && !strings.Contains(err.Error(), "existing entry") { - return fmt.Errorf("failed to create /buckets directory: %v", err) - } - - // Check if bucket already exists - _, err = client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ - Directory: "/buckets", - Name: bucketName, - }) - if err == nil { - return fmt.Errorf("bucket %s already exists", bucketName) - } - - // Determine quota value (negative if disabled) - var quota int64 - if quotaEnabled && quotaBytes > 0 { - quota = quotaBytes - } else if !quotaEnabled && quotaBytes > 0 { - quota = -quotaBytes - } else { - quota = 0 - } - - // Create bucket directory under /buckets - _, err = client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ - Directory: "/buckets", - Entry: &filer_pb.Entry{ - Name: bucketName, - IsDirectory: true, - Attributes: &filer_pb.FuseAttributes{ - FileMode: uint32(0755 | os.ModeDir), // Directory mode - Uid: filer_pb.OS_UID, - Gid: filer_pb.OS_GID, - Crtime: time.Now().Unix(), - Mtime: time.Now().Unix(), - TtlSec: 0, - }, - Quota: quota, - }, - }) - if err != nil { - return fmt.Errorf("failed to create bucket directory: %v", err) - } - - return nil - }) -} diff --git a/weed/admin/dash/bucket_management.go b/weed/admin/dash/bucket_management.go new file mode 100644 index 000000000..7fbd74a1e --- /dev/null +++ b/weed/admin/dash/bucket_management.go @@ -0,0 +1,325 @@ +package dash + +import ( + "context" + "fmt" + "net/http" + "os" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" +) + +// S3 Bucket management data structures for templates +type S3BucketsData struct { + Username string `json:"username"` + Buckets []S3Bucket `json:"buckets"` + TotalBuckets int `json:"total_buckets"` + TotalSize int64 `json:"total_size"` + LastUpdated time.Time `json:"last_updated"` +} + +type CreateBucketRequest struct { + Name string `json:"name" binding:"required"` + Region string `json:"region"` + QuotaSize int64 `json:"quota_size"` // Quota size in bytes + QuotaUnit string `json:"quota_unit"` // Unit: MB, GB, TB + QuotaEnabled bool `json:"quota_enabled"` // Whether quota is enabled +} + +// S3 Bucket Management Handlers + +// ShowS3Buckets displays the Object Store buckets management page +func (s *AdminServer) ShowS3Buckets(c *gin.Context) { + username := c.GetString("username") + + buckets, err := s.GetS3Buckets() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get Object Store buckets: " + err.Error()}) + return + } + + // Calculate totals + var totalSize int64 + for _, bucket := range buckets { + totalSize += bucket.Size + } + + data := S3BucketsData{ + Username: username, + Buckets: buckets, + TotalBuckets: len(buckets), + TotalSize: totalSize, + LastUpdated: time.Now(), + } + + c.JSON(http.StatusOK, data) +} + +// ShowBucketDetails displays detailed information about a specific bucket +func (s *AdminServer) ShowBucketDetails(c *gin.Context) { + bucketName := c.Param("bucket") + if bucketName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) + return + } + + details, err := s.GetBucketDetails(bucketName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bucket details: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, details) +} + +// CreateBucket creates a new S3 bucket +func (s *AdminServer) CreateBucket(c *gin.Context) { + var req CreateBucketRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + + // Validate bucket name (basic validation) + if len(req.Name) < 3 || len(req.Name) > 63 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name must be between 3 and 63 characters"}) + return + } + + // Convert quota to bytes + quotaBytes := convertQuotaToBytes(req.QuotaSize, req.QuotaUnit) + + err := s.CreateS3BucketWithQuota(req.Name, quotaBytes, req.QuotaEnabled) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bucket: " + err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "message": "Bucket created successfully", + "bucket": req.Name, + "quota_size": req.QuotaSize, + "quota_unit": req.QuotaUnit, + "quota_enabled": req.QuotaEnabled, + }) +} + +// UpdateBucketQuota updates the quota settings for a bucket +func (s *AdminServer) UpdateBucketQuota(c *gin.Context) { + bucketName := c.Param("bucket") + if bucketName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) + return + } + + var req struct { + QuotaSize int64 `json:"quota_size"` + QuotaUnit string `json:"quota_unit"` + QuotaEnabled bool `json:"quota_enabled"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + + // Convert quota to bytes + quotaBytes := convertQuotaToBytes(req.QuotaSize, req.QuotaUnit) + + err := s.SetBucketQuota(bucketName, quotaBytes, req.QuotaEnabled) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update bucket quota: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Bucket quota updated successfully", + "bucket": bucketName, + "quota_size": req.QuotaSize, + "quota_unit": req.QuotaUnit, + "quota_enabled": req.QuotaEnabled, + }) +} + +// DeleteBucket deletes an S3 bucket +func (s *AdminServer) DeleteBucket(c *gin.Context) { + bucketName := c.Param("bucket") + if bucketName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) + return + } + + err := s.DeleteS3Bucket(bucketName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete bucket: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Bucket deleted successfully", + "bucket": bucketName, + }) +} + +// ListBucketsAPI returns the list of buckets as JSON +func (s *AdminServer) ListBucketsAPI(c *gin.Context) { + buckets, err := s.GetS3Buckets() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get buckets: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "buckets": buckets, + "total": len(buckets), + }) +} + +// Helper function to convert quota size and unit to bytes +func convertQuotaToBytes(size int64, unit string) int64 { + if size <= 0 { + return 0 + } + + switch strings.ToUpper(unit) { + case "TB": + return size * 1024 * 1024 * 1024 * 1024 + case "GB": + return size * 1024 * 1024 * 1024 + case "MB": + return size * 1024 * 1024 + default: + // Default to MB if unit is not recognized + return size * 1024 * 1024 + } +} + +// Helper function to convert bytes to appropriate unit and size +func convertBytesToQuota(bytes int64) (int64, string) { + if bytes == 0 { + return 0, "MB" + } + + // Convert to TB if >= 1TB + if bytes >= 1024*1024*1024*1024 && bytes%(1024*1024*1024*1024) == 0 { + return bytes / (1024 * 1024 * 1024 * 1024), "TB" + } + + // Convert to GB if >= 1GB + if bytes >= 1024*1024*1024 && bytes%(1024*1024*1024) == 0 { + return bytes / (1024 * 1024 * 1024), "GB" + } + + // Convert to MB (default) + return bytes / (1024 * 1024), "MB" +} + +// SetBucketQuota sets the quota for a bucket +func (s *AdminServer) SetBucketQuota(bucketName string, quotaBytes int64, quotaEnabled bool) error { + return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + // Get the current bucket entry + lookupResp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ + Directory: "/buckets", + Name: bucketName, + }) + if err != nil { + return fmt.Errorf("bucket not found: %v", err) + } + + bucketEntry := lookupResp.Entry + + // Determine quota value (negative if disabled) + var quota int64 + if quotaEnabled && quotaBytes > 0 { + quota = quotaBytes + } else if !quotaEnabled && quotaBytes > 0 { + quota = -quotaBytes + } else { + quota = 0 + } + + // Update the quota + bucketEntry.Quota = quota + + // Update the entry + _, err = client.UpdateEntry(context.Background(), &filer_pb.UpdateEntryRequest{ + Directory: "/buckets", + Entry: bucketEntry, + }) + if err != nil { + return fmt.Errorf("failed to update bucket quota: %v", err) + } + + return nil + }) +} + +// CreateS3BucketWithQuota creates a new S3 bucket with quota settings +func (s *AdminServer) CreateS3BucketWithQuota(bucketName string, quotaBytes int64, quotaEnabled bool) error { + return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + // First ensure /buckets directory exists + _, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ + Directory: "/", + Entry: &filer_pb.Entry{ + Name: "buckets", + IsDirectory: true, + Attributes: &filer_pb.FuseAttributes{ + FileMode: uint32(0755 | os.ModeDir), // Directory mode + Uid: uint32(1000), + Gid: uint32(1000), + Crtime: time.Now().Unix(), + Mtime: time.Now().Unix(), + TtlSec: 0, + }, + }, + }) + // Ignore error if directory already exists + if err != nil && !strings.Contains(err.Error(), "already exists") && !strings.Contains(err.Error(), "existing entry") { + return fmt.Errorf("failed to create /buckets directory: %v", err) + } + + // Check if bucket already exists + _, err = client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ + Directory: "/buckets", + Name: bucketName, + }) + if err == nil { + return fmt.Errorf("bucket %s already exists", bucketName) + } + + // Determine quota value (negative if disabled) + var quota int64 + if quotaEnabled && quotaBytes > 0 { + quota = quotaBytes + } else if !quotaEnabled && quotaBytes > 0 { + quota = -quotaBytes + } else { + quota = 0 + } + + // Create bucket directory under /buckets + _, err = client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ + Directory: "/buckets", + Entry: &filer_pb.Entry{ + Name: bucketName, + IsDirectory: true, + Attributes: &filer_pb.FuseAttributes{ + FileMode: uint32(0755 | os.ModeDir), // Directory mode + Uid: filer_pb.OS_UID, + Gid: filer_pb.OS_GID, + Crtime: time.Now().Unix(), + Mtime: time.Now().Unix(), + TtlSec: 0, + }, + Quota: quota, + }, + }) + if err != nil { + return fmt.Errorf("failed to create bucket directory: %v", err) + } + + return nil + }) +} diff --git a/weed/admin/dash/file_browser.go b/weed/admin/dash/file_browser.go deleted file mode 100644 index 3cb878718..000000000 --- a/weed/admin/dash/file_browser.go +++ /dev/null @@ -1,350 +0,0 @@ -package dash - -import ( - "context" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" -) - -// FileEntry represents a file or directory entry in the file browser -type FileEntry struct { - Name string `json:"name"` - FullPath string `json:"full_path"` - IsDirectory bool `json:"is_directory"` - Size int64 `json:"size"` - ModTime time.Time `json:"mod_time"` - Mode string `json:"mode"` - Uid uint32 `json:"uid"` - Gid uint32 `json:"gid"` - Mime string `json:"mime"` - Replication string `json:"replication"` - Collection string `json:"collection"` - TtlSec int32 `json:"ttl_sec"` -} - -// BreadcrumbItem represents a single breadcrumb in the navigation -type BreadcrumbItem struct { - Name string `json:"name"` - Path string `json:"path"` -} - -// FileBrowserData contains all data needed for the file browser view -type FileBrowserData struct { - Username string `json:"username"` - CurrentPath string `json:"current_path"` - ParentPath string `json:"parent_path"` - Breadcrumbs []BreadcrumbItem `json:"breadcrumbs"` - Entries []FileEntry `json:"entries"` - TotalEntries int `json:"total_entries"` - TotalSize int64 `json:"total_size"` - LastUpdated time.Time `json:"last_updated"` - IsBucketPath bool `json:"is_bucket_path"` - BucketName string `json:"bucket_name"` -} - -// GetFileBrowser retrieves file browser data for a given path -func (s *AdminServer) GetFileBrowser(path string) (*FileBrowserData, error) { - if path == "" { - path = "/" - } - - var entries []FileEntry - var totalSize int64 - - // Get directory listing from filer - err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { - stream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{ - Directory: path, - Prefix: "", - Limit: 1000, - InclusiveStartFrom: false, - }) - if err != nil { - return err - } - - for { - resp, err := stream.Recv() - if err != nil { - if err.Error() == "EOF" { - break - } - return err - } - - entry := resp.Entry - if entry == nil { - continue - } - - fullPath := path - if !strings.HasSuffix(fullPath, "/") { - fullPath += "/" - } - fullPath += entry.Name - - var modTime time.Time - if entry.Attributes != nil && entry.Attributes.Mtime > 0 { - modTime = time.Unix(entry.Attributes.Mtime, 0) - } - - var mode string - var uid, gid uint32 - var size int64 - var replication, collection string - var ttlSec int32 - - if entry.Attributes != nil { - mode = formatFileMode(entry.Attributes.FileMode) - uid = entry.Attributes.Uid - gid = entry.Attributes.Gid - size = int64(entry.Attributes.FileSize) - ttlSec = entry.Attributes.TtlSec - } - - // Get replication and collection from entry extended attributes or chunks - if entry.Extended != nil { - if repl, ok := entry.Extended["replication"]; ok { - replication = string(repl) - } - if coll, ok := entry.Extended["collection"]; ok { - collection = string(coll) - } - } - - // Determine MIME type based on file extension - mime := "application/octet-stream" - if entry.IsDirectory { - mime = "inode/directory" - } else { - ext := strings.ToLower(filepath.Ext(entry.Name)) - switch ext { - case ".txt", ".log": - mime = "text/plain" - case ".html", ".htm": - mime = "text/html" - case ".css": - mime = "text/css" - case ".js": - mime = "application/javascript" - case ".json": - mime = "application/json" - case ".xml": - mime = "application/xml" - case ".pdf": - mime = "application/pdf" - case ".jpg", ".jpeg": - mime = "image/jpeg" - case ".png": - mime = "image/png" - case ".gif": - mime = "image/gif" - case ".svg": - mime = "image/svg+xml" - case ".mp4": - mime = "video/mp4" - case ".mp3": - mime = "audio/mpeg" - case ".zip": - mime = "application/zip" - case ".tar": - mime = "application/x-tar" - case ".gz": - mime = "application/gzip" - } - } - - fileEntry := FileEntry{ - Name: entry.Name, - FullPath: fullPath, - IsDirectory: entry.IsDirectory, - Size: size, - ModTime: modTime, - Mode: mode, - Uid: uid, - Gid: gid, - Mime: mime, - Replication: replication, - Collection: collection, - TtlSec: ttlSec, - } - - entries = append(entries, fileEntry) - if !entry.IsDirectory { - totalSize += size - } - } - - return nil - }) - - if err != nil { - return nil, err - } - - // Sort entries: directories first, then files, both alphabetically - sort.Slice(entries, func(i, j int) bool { - if entries[i].IsDirectory != entries[j].IsDirectory { - return entries[i].IsDirectory - } - return strings.ToLower(entries[i].Name) < strings.ToLower(entries[j].Name) - }) - - // Generate breadcrumbs - breadcrumbs := s.generateBreadcrumbs(path) - - // Calculate parent path - parentPath := "/" - if path != "/" { - parentPath = filepath.Dir(path) - if parentPath == "." { - parentPath = "/" - } - } - - // Check if this is a bucket path - isBucketPath := false - bucketName := "" - if strings.HasPrefix(path, "/buckets/") { - isBucketPath = true - pathParts := strings.Split(strings.Trim(path, "/"), "/") - if len(pathParts) >= 2 { - bucketName = pathParts[1] - } - } - - return &FileBrowserData{ - CurrentPath: path, - ParentPath: parentPath, - Breadcrumbs: breadcrumbs, - Entries: entries, - TotalEntries: len(entries), - TotalSize: totalSize, - LastUpdated: time.Now(), - IsBucketPath: isBucketPath, - BucketName: bucketName, - }, nil -} - -// generateBreadcrumbs creates breadcrumb navigation for the current path -func (s *AdminServer) generateBreadcrumbs(path string) []BreadcrumbItem { - var breadcrumbs []BreadcrumbItem - - // Always start with root - breadcrumbs = append(breadcrumbs, BreadcrumbItem{ - Name: "Root", - Path: "/", - }) - - if path == "/" { - return breadcrumbs - } - - // Split path and build breadcrumbs - parts := strings.Split(strings.Trim(path, "/"), "/") - currentPath := "" - - for _, part := range parts { - if part == "" { - continue - } - currentPath += "/" + part - - // Special handling for bucket paths - displayName := part - if len(breadcrumbs) == 1 && part == "buckets" { - displayName = "Object Store Buckets" - } else if len(breadcrumbs) == 2 && strings.HasPrefix(path, "/buckets/") { - displayName = "📦 " + part // Add bucket icon to bucket name - } - - breadcrumbs = append(breadcrumbs, BreadcrumbItem{ - Name: displayName, - Path: currentPath, - }) - } - - return breadcrumbs -} - -// formatFileMode converts file mode to Unix-style string representation (e.g., "drwxr-xr-x") -func formatFileMode(mode uint32) string { - var result []byte = make([]byte, 10) - - // File type - switch mode & 0170000 { // S_IFMT mask - case 0040000: // S_IFDIR - result[0] = 'd' - case 0100000: // S_IFREG - result[0] = '-' - case 0120000: // S_IFLNK - result[0] = 'l' - case 0020000: // S_IFCHR - result[0] = 'c' - case 0060000: // S_IFBLK - result[0] = 'b' - case 0010000: // S_IFIFO - result[0] = 'p' - case 0140000: // S_IFSOCK - result[0] = 's' - default: - result[0] = '-' // S_IFREG is default - } - - // Owner permissions - if mode&0400 != 0 { // S_IRUSR - result[1] = 'r' - } else { - result[1] = '-' - } - if mode&0200 != 0 { // S_IWUSR - result[2] = 'w' - } else { - result[2] = '-' - } - if mode&0100 != 0 { // S_IXUSR - result[3] = 'x' - } else { - result[3] = '-' - } - - // Group permissions - if mode&0040 != 0 { // S_IRGRP - result[4] = 'r' - } else { - result[4] = '-' - } - if mode&0020 != 0 { // S_IWGRP - result[5] = 'w' - } else { - result[5] = '-' - } - if mode&0010 != 0 { // S_IXGRP - result[6] = 'x' - } else { - result[6] = '-' - } - - // Other permissions - if mode&0004 != 0 { // S_IROTH - result[7] = 'r' - } else { - result[7] = '-' - } - if mode&0002 != 0 { // S_IWOTH - result[8] = 'w' - } else { - result[8] = '-' - } - if mode&0001 != 0 { // S_IXOTH - result[9] = 'x' - } else { - result[9] = '-' - } - - return string(result) -} diff --git a/weed/admin/dash/file_browser_data.go b/weed/admin/dash/file_browser_data.go new file mode 100644 index 000000000..3cb878718 --- /dev/null +++ b/weed/admin/dash/file_browser_data.go @@ -0,0 +1,350 @@ +package dash + +import ( + "context" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" +) + +// FileEntry represents a file or directory entry in the file browser +type FileEntry struct { + Name string `json:"name"` + FullPath string `json:"full_path"` + IsDirectory bool `json:"is_directory"` + Size int64 `json:"size"` + ModTime time.Time `json:"mod_time"` + Mode string `json:"mode"` + Uid uint32 `json:"uid"` + Gid uint32 `json:"gid"` + Mime string `json:"mime"` + Replication string `json:"replication"` + Collection string `json:"collection"` + TtlSec int32 `json:"ttl_sec"` +} + +// BreadcrumbItem represents a single breadcrumb in the navigation +type BreadcrumbItem struct { + Name string `json:"name"` + Path string `json:"path"` +} + +// FileBrowserData contains all data needed for the file browser view +type FileBrowserData struct { + Username string `json:"username"` + CurrentPath string `json:"current_path"` + ParentPath string `json:"parent_path"` + Breadcrumbs []BreadcrumbItem `json:"breadcrumbs"` + Entries []FileEntry `json:"entries"` + TotalEntries int `json:"total_entries"` + TotalSize int64 `json:"total_size"` + LastUpdated time.Time `json:"last_updated"` + IsBucketPath bool `json:"is_bucket_path"` + BucketName string `json:"bucket_name"` +} + +// GetFileBrowser retrieves file browser data for a given path +func (s *AdminServer) GetFileBrowser(path string) (*FileBrowserData, error) { + if path == "" { + path = "/" + } + + var entries []FileEntry + var totalSize int64 + + // Get directory listing from filer + err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + stream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{ + Directory: path, + Prefix: "", + Limit: 1000, + InclusiveStartFrom: false, + }) + if err != nil { + return err + } + + for { + resp, err := stream.Recv() + if err != nil { + if err.Error() == "EOF" { + break + } + return err + } + + entry := resp.Entry + if entry == nil { + continue + } + + fullPath := path + if !strings.HasSuffix(fullPath, "/") { + fullPath += "/" + } + fullPath += entry.Name + + var modTime time.Time + if entry.Attributes != nil && entry.Attributes.Mtime > 0 { + modTime = time.Unix(entry.Attributes.Mtime, 0) + } + + var mode string + var uid, gid uint32 + var size int64 + var replication, collection string + var ttlSec int32 + + if entry.Attributes != nil { + mode = formatFileMode(entry.Attributes.FileMode) + uid = entry.Attributes.Uid + gid = entry.Attributes.Gid + size = int64(entry.Attributes.FileSize) + ttlSec = entry.Attributes.TtlSec + } + + // Get replication and collection from entry extended attributes or chunks + if entry.Extended != nil { + if repl, ok := entry.Extended["replication"]; ok { + replication = string(repl) + } + if coll, ok := entry.Extended["collection"]; ok { + collection = string(coll) + } + } + + // Determine MIME type based on file extension + mime := "application/octet-stream" + if entry.IsDirectory { + mime = "inode/directory" + } else { + ext := strings.ToLower(filepath.Ext(entry.Name)) + switch ext { + case ".txt", ".log": + mime = "text/plain" + case ".html", ".htm": + mime = "text/html" + case ".css": + mime = "text/css" + case ".js": + mime = "application/javascript" + case ".json": + mime = "application/json" + case ".xml": + mime = "application/xml" + case ".pdf": + mime = "application/pdf" + case ".jpg", ".jpeg": + mime = "image/jpeg" + case ".png": + mime = "image/png" + case ".gif": + mime = "image/gif" + case ".svg": + mime = "image/svg+xml" + case ".mp4": + mime = "video/mp4" + case ".mp3": + mime = "audio/mpeg" + case ".zip": + mime = "application/zip" + case ".tar": + mime = "application/x-tar" + case ".gz": + mime = "application/gzip" + } + } + + fileEntry := FileEntry{ + Name: entry.Name, + FullPath: fullPath, + IsDirectory: entry.IsDirectory, + Size: size, + ModTime: modTime, + Mode: mode, + Uid: uid, + Gid: gid, + Mime: mime, + Replication: replication, + Collection: collection, + TtlSec: ttlSec, + } + + entries = append(entries, fileEntry) + if !entry.IsDirectory { + totalSize += size + } + } + + return nil + }) + + if err != nil { + return nil, err + } + + // Sort entries: directories first, then files, both alphabetically + sort.Slice(entries, func(i, j int) bool { + if entries[i].IsDirectory != entries[j].IsDirectory { + return entries[i].IsDirectory + } + return strings.ToLower(entries[i].Name) < strings.ToLower(entries[j].Name) + }) + + // Generate breadcrumbs + breadcrumbs := s.generateBreadcrumbs(path) + + // Calculate parent path + parentPath := "/" + if path != "/" { + parentPath = filepath.Dir(path) + if parentPath == "." { + parentPath = "/" + } + } + + // Check if this is a bucket path + isBucketPath := false + bucketName := "" + if strings.HasPrefix(path, "/buckets/") { + isBucketPath = true + pathParts := strings.Split(strings.Trim(path, "/"), "/") + if len(pathParts) >= 2 { + bucketName = pathParts[1] + } + } + + return &FileBrowserData{ + CurrentPath: path, + ParentPath: parentPath, + Breadcrumbs: breadcrumbs, + Entries: entries, + TotalEntries: len(entries), + TotalSize: totalSize, + LastUpdated: time.Now(), + IsBucketPath: isBucketPath, + BucketName: bucketName, + }, nil +} + +// generateBreadcrumbs creates breadcrumb navigation for the current path +func (s *AdminServer) generateBreadcrumbs(path string) []BreadcrumbItem { + var breadcrumbs []BreadcrumbItem + + // Always start with root + breadcrumbs = append(breadcrumbs, BreadcrumbItem{ + Name: "Root", + Path: "/", + }) + + if path == "/" { + return breadcrumbs + } + + // Split path and build breadcrumbs + parts := strings.Split(strings.Trim(path, "/"), "/") + currentPath := "" + + for _, part := range parts { + if part == "" { + continue + } + currentPath += "/" + part + + // Special handling for bucket paths + displayName := part + if len(breadcrumbs) == 1 && part == "buckets" { + displayName = "Object Store Buckets" + } else if len(breadcrumbs) == 2 && strings.HasPrefix(path, "/buckets/") { + displayName = "📦 " + part // Add bucket icon to bucket name + } + + breadcrumbs = append(breadcrumbs, BreadcrumbItem{ + Name: displayName, + Path: currentPath, + }) + } + + return breadcrumbs +} + +// formatFileMode converts file mode to Unix-style string representation (e.g., "drwxr-xr-x") +func formatFileMode(mode uint32) string { + var result []byte = make([]byte, 10) + + // File type + switch mode & 0170000 { // S_IFMT mask + case 0040000: // S_IFDIR + result[0] = 'd' + case 0100000: // S_IFREG + result[0] = '-' + case 0120000: // S_IFLNK + result[0] = 'l' + case 0020000: // S_IFCHR + result[0] = 'c' + case 0060000: // S_IFBLK + result[0] = 'b' + case 0010000: // S_IFIFO + result[0] = 'p' + case 0140000: // S_IFSOCK + result[0] = 's' + default: + result[0] = '-' // S_IFREG is default + } + + // Owner permissions + if mode&0400 != 0 { // S_IRUSR + result[1] = 'r' + } else { + result[1] = '-' + } + if mode&0200 != 0 { // S_IWUSR + result[2] = 'w' + } else { + result[2] = '-' + } + if mode&0100 != 0 { // S_IXUSR + result[3] = 'x' + } else { + result[3] = '-' + } + + // Group permissions + if mode&0040 != 0 { // S_IRGRP + result[4] = 'r' + } else { + result[4] = '-' + } + if mode&0020 != 0 { // S_IWGRP + result[5] = 'w' + } else { + result[5] = '-' + } + if mode&0010 != 0 { // S_IXGRP + result[6] = 'x' + } else { + result[6] = '-' + } + + // Other permissions + if mode&0004 != 0 { // S_IROTH + result[7] = 'r' + } else { + result[7] = '-' + } + if mode&0002 != 0 { // S_IWOTH + result[8] = 'w' + } else { + result[8] = '-' + } + if mode&0001 != 0 { // S_IXOTH + result[9] = 'x' + } else { + result[9] = '-' + } + + return string(result) +} diff --git a/weed/admin/dash/handler_admin.go b/weed/admin/dash/handler_admin.go deleted file mode 100644 index ae218de75..000000000 --- a/weed/admin/dash/handler_admin.go +++ /dev/null @@ -1,280 +0,0 @@ -package dash - -import ( - "context" - "net/http" - "time" - - "github.com/gin-gonic/gin" - "github.com/seaweedfs/seaweedfs/weed/cluster" - "github.com/seaweedfs/seaweedfs/weed/glog" - "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" -) - -type AdminData struct { - Username string `json:"username"` - ClusterStatus string `json:"cluster_status"` - TotalVolumes int `json:"total_volumes"` - TotalFiles int64 `json:"total_files"` - TotalSize int64 `json:"total_size"` - MasterNodes []MasterNode `json:"master_nodes"` - VolumeServers []VolumeServer `json:"volume_servers"` - FilerNodes []FilerNode `json:"filer_nodes"` - DataCenters []DataCenter `json:"datacenters"` - LastUpdated time.Time `json:"last_updated"` - SystemHealth string `json:"system_health"` -} - -// Object Store Users management structures -type ObjectStoreUser struct { - Username string `json:"username"` - Email string `json:"email"` - AccessKey string `json:"access_key"` - SecretKey string `json:"secret_key"` - Permissions []string `json:"permissions"` -} - -type ObjectStoreUsersData struct { - Username string `json:"username"` - Users []ObjectStoreUser `json:"users"` - TotalUsers int `json:"total_users"` - LastUpdated time.Time `json:"last_updated"` -} - -// User management request structures -type CreateUserRequest struct { - Username string `json:"username" binding:"required"` - Email string `json:"email"` - Actions []string `json:"actions"` - GenerateKey bool `json:"generate_key"` -} - -type UpdateUserRequest struct { - Email string `json:"email"` - Actions []string `json:"actions"` -} - -type UpdateUserPoliciesRequest struct { - Actions []string `json:"actions" binding:"required"` -} - -type AccessKeyInfo struct { - AccessKey string `json:"access_key"` - SecretKey string `json:"secret_key"` - CreatedAt time.Time `json:"created_at"` -} - -type UserDetails struct { - Username string `json:"username"` - Email string `json:"email"` - Actions []string `json:"actions"` - AccessKeys []AccessKeyInfo `json:"access_keys"` -} - -type FilerNode struct { - Address string `json:"address"` - DataCenter string `json:"datacenter"` - Rack string `json:"rack"` - Status string `json:"status"` - LastUpdated time.Time `json:"last_updated"` -} - -// GetAdminData retrieves admin data as a struct (for reuse by both JSON and HTML handlers) -func (s *AdminServer) GetAdminData(username string) (AdminData, error) { - if username == "" { - username = "admin" - } - - // Get cluster topology - topology, err := s.GetClusterTopology() - if err != nil { - glog.Errorf("Failed to get cluster topology: %v", err) - return AdminData{}, err - } - - // Get master nodes status - masterNodes := s.getMasterNodesStatus() - - // Get filer nodes status - filerNodes := s.getFilerNodesStatus() - - // Prepare admin data - adminData := AdminData{ - Username: username, - ClusterStatus: s.determineClusterStatus(topology, masterNodes), - TotalVolumes: topology.TotalVolumes, - TotalFiles: topology.TotalFiles, - TotalSize: topology.TotalSize, - MasterNodes: masterNodes, - VolumeServers: topology.VolumeServers, - FilerNodes: filerNodes, - DataCenters: topology.DataCenters, - LastUpdated: topology.UpdatedAt, - SystemHealth: s.determineSystemHealth(topology, masterNodes), - } - - return adminData, nil -} - -// ShowAdmin displays the main admin page (now uses GetAdminData) -func (s *AdminServer) ShowAdmin(c *gin.Context) { - username := c.GetString("username") - - adminData, err := s.GetAdminData(username) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get admin data: " + err.Error()}) - return - } - - // Return JSON for API calls - c.JSON(http.StatusOK, adminData) -} - -// ShowOverview displays cluster overview -func (s *AdminServer) ShowOverview(c *gin.Context) { - topology, err := s.GetClusterTopology() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, topology) -} - -// getMasterNodesStatus checks status of all master nodes -func (s *AdminServer) getMasterNodesStatus() []MasterNode { - var masterNodes []MasterNode - - // Since we have a single master address, create one entry - var isLeader bool = true // Assume leader since it's the only master we know about - var status string - - // Try to get leader info from this master - err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { - _, err := client.GetMasterConfiguration(context.Background(), &master_pb.GetMasterConfigurationRequest{}) - if err != nil { - return err - } - // For now, assume this master is the leader since we can connect to it - isLeader = true - return nil - }) - - if err != nil { - status = "unreachable" - isLeader = false - } else { - status = "active" - } - - masterNodes = append(masterNodes, MasterNode{ - Address: s.masterAddress, - IsLeader: isLeader, - Status: status, - }) - - return masterNodes -} - -// getFilerNodesStatus checks status of all filer nodes using master's ListClusterNodes -func (s *AdminServer) getFilerNodesStatus() []FilerNode { - var filerNodes []FilerNode - - // Get filer nodes from master using ListClusterNodes - err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { - resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{ - ClientType: cluster.FilerType, - }) - if err != nil { - return err - } - - // Process each filer node - for _, node := range resp.ClusterNodes { - filerNodes = append(filerNodes, FilerNode{ - Address: node.Address, - DataCenter: node.DataCenter, - Rack: node.Rack, - Status: "active", // If it's in the cluster list, it's considered active - LastUpdated: time.Now(), - }) - } - - return nil - }) - - if err != nil { - glog.Errorf("Failed to get filer nodes from master %s: %v", s.masterAddress, err) - // Return empty list if we can't get filer info from master - return []FilerNode{} - } - - return filerNodes -} - -// determineClusterStatus analyzes cluster health -func (s *AdminServer) determineClusterStatus(topology *ClusterTopology, masters []MasterNode) string { - // Check if we have an active leader - hasActiveLeader := false - for _, master := range masters { - if master.IsLeader && master.Status == "active" { - hasActiveLeader = true - break - } - } - - if !hasActiveLeader { - return "critical" - } - - // Check volume server health - activeServers := 0 - for _, vs := range topology.VolumeServers { - if vs.Status == "active" { - activeServers++ - } - } - - if activeServers == 0 { - return "critical" - } else if activeServers < len(topology.VolumeServers) { - return "warning" - } - - return "healthy" -} - -// determineSystemHealth provides overall system health assessment -func (s *AdminServer) determineSystemHealth(topology *ClusterTopology, masters []MasterNode) string { - // Simple health calculation based on active components - totalComponents := len(masters) + len(topology.VolumeServers) - activeComponents := 0 - - for _, master := range masters { - if master.Status == "active" { - activeComponents++ - } - } - - for _, vs := range topology.VolumeServers { - if vs.Status == "active" { - activeComponents++ - } - } - - if totalComponents == 0 { - return "unknown" - } - - healthPercent := float64(activeComponents) / float64(totalComponents) * 100 - - if healthPercent >= 95 { - return "excellent" - } else if healthPercent >= 80 { - return "good" - } else if healthPercent >= 60 { - return "fair" - } else { - return "poor" - } -} diff --git a/weed/admin/dash/handler_auth.go b/weed/admin/dash/handler_auth.go deleted file mode 100644 index 986a30290..000000000 --- a/weed/admin/dash/handler_auth.go +++ /dev/null @@ -1,53 +0,0 @@ -package dash - -import ( - "net/http" - - "github.com/gin-contrib/sessions" - "github.com/gin-gonic/gin" -) - -// ShowLogin displays the login page -func (s *AdminServer) ShowLogin(c *gin.Context) { - // If authentication is not required, redirect to admin - session := sessions.Default(c) - if session.Get("authenticated") == true { - c.Redirect(http.StatusSeeOther, "/admin") - return - } - - // For now, return a simple login form as JSON - c.HTML(http.StatusOK, "login.html", gin.H{ - "title": "SeaweedFS Admin Login", - "error": c.Query("error"), - }) -} - -// HandleLogin handles login form submission -func (s *AdminServer) HandleLogin(username, password string) gin.HandlerFunc { - return func(c *gin.Context) { - loginUsername := c.PostForm("username") - loginPassword := c.PostForm("password") - - if loginUsername == username && loginPassword == password { - session := sessions.Default(c) - session.Set("authenticated", true) - session.Set("username", loginUsername) - session.Save() - - c.Redirect(http.StatusSeeOther, "/admin") - return - } - - // Authentication failed - c.Redirect(http.StatusSeeOther, "/login?error=Invalid credentials") - } -} - -// HandleLogout handles user logout -func (s *AdminServer) HandleLogout(c *gin.Context) { - session := sessions.Default(c) - session.Clear() - session.Save() - c.Redirect(http.StatusSeeOther, "/login") -} diff --git a/weed/admin/handlers/admin_handlers.go b/weed/admin/handlers/admin_handlers.go new file mode 100644 index 000000000..a0140a25d --- /dev/null +++ b/weed/admin/handlers/admin_handlers.go @@ -0,0 +1,316 @@ +package handlers + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/seaweedfs/seaweedfs/weed/admin/dash" + "github.com/seaweedfs/seaweedfs/weed/admin/view/app" + "github.com/seaweedfs/seaweedfs/weed/admin/view/layout" +) + +// AdminHandlers contains all the HTTP handlers for the admin interface +type AdminHandlers struct { + adminServer *dash.AdminServer + authHandlers *AuthHandlers + clusterHandlers *ClusterHandlers + fileBrowserHandlers *FileBrowserHandlers + userHandlers *UserHandlers +} + +// NewAdminHandlers creates a new instance of AdminHandlers +func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers { + authHandlers := NewAuthHandlers(adminServer) + clusterHandlers := NewClusterHandlers(adminServer) + fileBrowserHandlers := NewFileBrowserHandlers(adminServer) + userHandlers := NewUserHandlers(adminServer) + return &AdminHandlers{ + adminServer: adminServer, + authHandlers: authHandlers, + clusterHandlers: clusterHandlers, + fileBrowserHandlers: fileBrowserHandlers, + userHandlers: userHandlers, + } +} + +// SetupRoutes configures all the routes for the admin interface +func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, password string) { + // Health check (no auth required) + r.GET("/health", h.HealthCheck) + + if authRequired { + // Authentication routes (no auth required) + r.GET("/login", h.authHandlers.ShowLogin) + r.POST("/login", h.authHandlers.HandleLogin(username, password)) + r.GET("/logout", h.authHandlers.HandleLogout) + + // Protected routes group + protected := r.Group("/") + protected.Use(dash.RequireAuth()) + + // Main admin interface routes + protected.GET("/", h.ShowDashboard) + protected.GET("/admin", h.ShowDashboard) + + // Object Store management routes + protected.GET("/object-store/buckets", h.ShowS3Buckets) + protected.GET("/object-store/buckets/:bucket", h.ShowBucketDetails) + protected.GET("/object-store/users", h.userHandlers.ShowObjectStoreUsers) + + // File browser routes + protected.GET("/files", h.fileBrowserHandlers.ShowFileBrowser) + + // Cluster management routes + protected.GET("/cluster/masters", h.clusterHandlers.ShowClusterMasters) + protected.GET("/cluster/filers", h.clusterHandlers.ShowClusterFilers) + protected.GET("/cluster/volume-servers", h.clusterHandlers.ShowClusterVolumeServers) + protected.GET("/cluster/volumes", h.clusterHandlers.ShowClusterVolumes) + protected.GET("/cluster/collections", h.clusterHandlers.ShowClusterCollections) + + // API routes for AJAX calls + api := protected.Group("/api") + { + api.GET("/cluster/topology", h.clusterHandlers.GetClusterTopology) + api.GET("/cluster/masters", h.clusterHandlers.GetMasters) + api.GET("/cluster/volumes", h.clusterHandlers.GetVolumeServers) + api.GET("/admin", h.adminServer.ShowAdmin) // JSON API for admin data + + // S3 API routes + s3Api := api.Group("/s3") + { + s3Api.GET("/buckets", h.adminServer.ListBucketsAPI) + s3Api.POST("/buckets", h.adminServer.CreateBucket) + s3Api.DELETE("/buckets/:bucket", h.adminServer.DeleteBucket) + s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails) + s3Api.PUT("/buckets/:bucket/quota", h.adminServer.UpdateBucketQuota) + } + + // User management API routes + usersApi := api.Group("/users") + { + usersApi.GET("", h.userHandlers.GetUsers) + usersApi.POST("", h.userHandlers.CreateUser) + usersApi.GET("/:username", h.userHandlers.GetUserDetails) + usersApi.PUT("/:username", h.userHandlers.UpdateUser) + usersApi.DELETE("/:username", h.userHandlers.DeleteUser) + usersApi.POST("/:username/access-keys", h.userHandlers.CreateAccessKey) + usersApi.DELETE("/:username/access-keys/:accessKeyId", h.userHandlers.DeleteAccessKey) + usersApi.GET("/:username/policies", h.userHandlers.GetUserPolicies) + usersApi.PUT("/:username/policies", h.userHandlers.UpdateUserPolicies) + } + + // File management API routes + filesApi := api.Group("/files") + { + filesApi.DELETE("/delete", h.fileBrowserHandlers.DeleteFile) + filesApi.DELETE("/delete-multiple", h.fileBrowserHandlers.DeleteMultipleFiles) + filesApi.POST("/create-folder", h.fileBrowserHandlers.CreateFolder) + filesApi.POST("/upload", h.fileBrowserHandlers.UploadFile) + filesApi.GET("/download", h.fileBrowserHandlers.DownloadFile) + filesApi.GET("/view", h.fileBrowserHandlers.ViewFile) + filesApi.GET("/properties", h.fileBrowserHandlers.GetFileProperties) + } + } + } else { + // No authentication required - all routes are public + r.GET("/", h.ShowDashboard) + r.GET("/admin", h.ShowDashboard) + + // Object Store management routes + r.GET("/object-store/buckets", h.ShowS3Buckets) + r.GET("/object-store/buckets/:bucket", h.ShowBucketDetails) + r.GET("/object-store/users", h.userHandlers.ShowObjectStoreUsers) + + // File browser routes + r.GET("/files", h.fileBrowserHandlers.ShowFileBrowser) + + // Cluster management routes + r.GET("/cluster/masters", h.clusterHandlers.ShowClusterMasters) + r.GET("/cluster/filers", h.clusterHandlers.ShowClusterFilers) + r.GET("/cluster/volume-servers", h.clusterHandlers.ShowClusterVolumeServers) + r.GET("/cluster/volumes", h.clusterHandlers.ShowClusterVolumes) + r.GET("/cluster/collections", h.clusterHandlers.ShowClusterCollections) + + // API routes for AJAX calls + api := r.Group("/api") + { + api.GET("/cluster/topology", h.clusterHandlers.GetClusterTopology) + api.GET("/cluster/masters", h.clusterHandlers.GetMasters) + api.GET("/cluster/volumes", h.clusterHandlers.GetVolumeServers) + api.GET("/admin", h.adminServer.ShowAdmin) // JSON API for admin data + + // S3 API routes + s3Api := api.Group("/s3") + { + s3Api.GET("/buckets", h.adminServer.ListBucketsAPI) + s3Api.POST("/buckets", h.adminServer.CreateBucket) + s3Api.DELETE("/buckets/:bucket", h.adminServer.DeleteBucket) + s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails) + s3Api.PUT("/buckets/:bucket/quota", h.adminServer.UpdateBucketQuota) + } + + // User management API routes + usersApi := api.Group("/users") + { + usersApi.GET("", h.userHandlers.GetUsers) + usersApi.POST("", h.userHandlers.CreateUser) + usersApi.GET("/:username", h.userHandlers.GetUserDetails) + usersApi.PUT("/:username", h.userHandlers.UpdateUser) + usersApi.DELETE("/:username", h.userHandlers.DeleteUser) + usersApi.POST("/:username/access-keys", h.userHandlers.CreateAccessKey) + usersApi.DELETE("/:username/access-keys/:accessKeyId", h.userHandlers.DeleteAccessKey) + usersApi.GET("/:username/policies", h.userHandlers.GetUserPolicies) + usersApi.PUT("/:username/policies", h.userHandlers.UpdateUserPolicies) + } + + // File management API routes + filesApi := api.Group("/files") + { + filesApi.DELETE("/delete", h.fileBrowserHandlers.DeleteFile) + filesApi.DELETE("/delete-multiple", h.fileBrowserHandlers.DeleteMultipleFiles) + filesApi.POST("/create-folder", h.fileBrowserHandlers.CreateFolder) + filesApi.POST("/upload", h.fileBrowserHandlers.UploadFile) + filesApi.GET("/download", h.fileBrowserHandlers.DownloadFile) + filesApi.GET("/view", h.fileBrowserHandlers.ViewFile) + filesApi.GET("/properties", h.fileBrowserHandlers.GetFileProperties) + } + } + } +} + +// HealthCheck returns the health status of the admin interface +func (h *AdminHandlers) HealthCheck(c *gin.Context) { + c.JSON(200, gin.H{"status": "ok"}) +} + +// ShowDashboard renders the main admin dashboard +func (h *AdminHandlers) ShowDashboard(c *gin.Context) { + // Get admin data from the server + adminData := h.getAdminData(c) + + // Render HTML template + c.Header("Content-Type", "text/html") + adminComponent := app.Admin(adminData) + layoutComponent := layout.Layout(c, adminComponent) + err := layoutComponent.Render(c.Request.Context(), c.Writer) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) + return + } +} + +// ShowS3Buckets renders the Object Store buckets management page +func (h *AdminHandlers) ShowS3Buckets(c *gin.Context) { + // Get Object Store buckets data from the server + s3Data := h.getS3BucketsData(c) + + // Render HTML template + c.Header("Content-Type", "text/html") + s3Component := app.S3Buckets(s3Data) + layoutComponent := layout.Layout(c, s3Component) + err := layoutComponent.Render(c.Request.Context(), c.Writer) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) + return + } +} + +// ShowBucketDetails returns detailed information about a specific bucket +func (h *AdminHandlers) ShowBucketDetails(c *gin.Context) { + bucketName := c.Param("bucket") + details, err := h.adminServer.GetBucketDetails(bucketName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bucket details: " + err.Error()}) + return + } + c.JSON(http.StatusOK, details) +} + +// getS3BucketsData retrieves Object Store buckets data from the server +func (h *AdminHandlers) getS3BucketsData(c *gin.Context) dash.S3BucketsData { + username := c.GetString("username") + if username == "" { + username = "admin" + } + + // Get Object Store buckets + buckets, err := h.adminServer.GetS3Buckets() + if err != nil { + // Return empty data on error + return dash.S3BucketsData{ + Username: username, + Buckets: []dash.S3Bucket{}, + TotalBuckets: 0, + TotalSize: 0, + LastUpdated: time.Now(), + } + } + + // Calculate totals + var totalSize int64 + for _, bucket := range buckets { + totalSize += bucket.Size + } + + return dash.S3BucketsData{ + Username: username, + Buckets: buckets, + TotalBuckets: len(buckets), + TotalSize: totalSize, + LastUpdated: time.Now(), + } +} + +// getAdminData retrieves admin data from the server (now uses consolidated method) +func (h *AdminHandlers) getAdminData(c *gin.Context) dash.AdminData { + username := c.GetString("username") + + // Use the consolidated GetAdminData method from AdminServer + adminData, err := h.adminServer.GetAdminData(username) + if err != nil { + // Return default data when services are not available + if username == "" { + username = "admin" + } + + masterNodes := []dash.MasterNode{ + { + Address: "localhost:9333", + IsLeader: true, + Status: "unreachable", + }, + } + + return dash.AdminData{ + Username: username, + ClusterStatus: "warning", + TotalVolumes: 0, + TotalFiles: 0, + TotalSize: 0, + MasterNodes: masterNodes, + VolumeServers: []dash.VolumeServer{}, + FilerNodes: []dash.FilerNode{}, + DataCenters: []dash.DataCenter{}, + LastUpdated: time.Now(), + SystemHealth: "poor", + } + } + + return adminData +} + +// Helper functions +func (h *AdminHandlers) determineClusterStatus(topology *dash.ClusterTopology, masters []dash.MasterNode) string { + if len(topology.VolumeServers) == 0 { + return "warning" + } + return "healthy" +} + +func (h *AdminHandlers) determineSystemHealth(topology *dash.ClusterTopology, masters []dash.MasterNode) string { + if len(topology.VolumeServers) > 0 && len(masters) > 0 { + return "good" + } + return "fair" +} diff --git a/weed/admin/handlers/auth.go b/weed/admin/handlers/auth.go deleted file mode 100644 index 07596b8e4..000000000 --- a/weed/admin/handlers/auth.go +++ /dev/null @@ -1,45 +0,0 @@ -package handlers - -import ( - "net/http" - - "github.com/gin-gonic/gin" - "github.com/seaweedfs/seaweedfs/weed/admin/dash" - "github.com/seaweedfs/seaweedfs/weed/admin/view/layout" -) - -// AuthHandlers contains authentication-related HTTP handlers -type AuthHandlers struct { - adminServer *dash.AdminServer -} - -// NewAuthHandlers creates a new instance of AuthHandlers -func NewAuthHandlers(adminServer *dash.AdminServer) *AuthHandlers { - return &AuthHandlers{ - adminServer: adminServer, - } -} - -// ShowLogin displays the login page -func (a *AuthHandlers) ShowLogin(c *gin.Context) { - errorMessage := c.Query("error") - - // Render login template - c.Header("Content-Type", "text/html") - loginComponent := layout.LoginForm(c, "SeaweedFS Admin", errorMessage) - err := loginComponent.Render(c.Request.Context(), c.Writer) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render login template: " + err.Error()}) - return - } -} - -// HandleLogin handles login form submission -func (a *AuthHandlers) HandleLogin(username, password string) gin.HandlerFunc { - return a.adminServer.HandleLogin(username, password) -} - -// HandleLogout handles user logout -func (a *AuthHandlers) HandleLogout(c *gin.Context) { - a.adminServer.HandleLogout(c) -} diff --git a/weed/admin/handlers/auth_handlers.go b/weed/admin/handlers/auth_handlers.go new file mode 100644 index 000000000..07596b8e4 --- /dev/null +++ b/weed/admin/handlers/auth_handlers.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/seaweedfs/seaweedfs/weed/admin/dash" + "github.com/seaweedfs/seaweedfs/weed/admin/view/layout" +) + +// AuthHandlers contains authentication-related HTTP handlers +type AuthHandlers struct { + adminServer *dash.AdminServer +} + +// NewAuthHandlers creates a new instance of AuthHandlers +func NewAuthHandlers(adminServer *dash.AdminServer) *AuthHandlers { + return &AuthHandlers{ + adminServer: adminServer, + } +} + +// ShowLogin displays the login page +func (a *AuthHandlers) ShowLogin(c *gin.Context) { + errorMessage := c.Query("error") + + // Render login template + c.Header("Content-Type", "text/html") + loginComponent := layout.LoginForm(c, "SeaweedFS Admin", errorMessage) + err := loginComponent.Render(c.Request.Context(), c.Writer) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render login template: " + err.Error()}) + return + } +} + +// HandleLogin handles login form submission +func (a *AuthHandlers) HandleLogin(username, password string) gin.HandlerFunc { + return a.adminServer.HandleLogin(username, password) +} + +// HandleLogout handles user logout +func (a *AuthHandlers) HandleLogout(c *gin.Context) { + a.adminServer.HandleLogout(c) +} diff --git a/weed/admin/handlers/handlers.go b/weed/admin/handlers/handlers.go deleted file mode 100644 index a0140a25d..000000000 --- a/weed/admin/handlers/handlers.go +++ /dev/null @@ -1,316 +0,0 @@ -package handlers - -import ( - "net/http" - "time" - - "github.com/gin-gonic/gin" - "github.com/seaweedfs/seaweedfs/weed/admin/dash" - "github.com/seaweedfs/seaweedfs/weed/admin/view/app" - "github.com/seaweedfs/seaweedfs/weed/admin/view/layout" -) - -// AdminHandlers contains all the HTTP handlers for the admin interface -type AdminHandlers struct { - adminServer *dash.AdminServer - authHandlers *AuthHandlers - clusterHandlers *ClusterHandlers - fileBrowserHandlers *FileBrowserHandlers - userHandlers *UserHandlers -} - -// NewAdminHandlers creates a new instance of AdminHandlers -func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers { - authHandlers := NewAuthHandlers(adminServer) - clusterHandlers := NewClusterHandlers(adminServer) - fileBrowserHandlers := NewFileBrowserHandlers(adminServer) - userHandlers := NewUserHandlers(adminServer) - return &AdminHandlers{ - adminServer: adminServer, - authHandlers: authHandlers, - clusterHandlers: clusterHandlers, - fileBrowserHandlers: fileBrowserHandlers, - userHandlers: userHandlers, - } -} - -// SetupRoutes configures all the routes for the admin interface -func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, password string) { - // Health check (no auth required) - r.GET("/health", h.HealthCheck) - - if authRequired { - // Authentication routes (no auth required) - r.GET("/login", h.authHandlers.ShowLogin) - r.POST("/login", h.authHandlers.HandleLogin(username, password)) - r.GET("/logout", h.authHandlers.HandleLogout) - - // Protected routes group - protected := r.Group("/") - protected.Use(dash.RequireAuth()) - - // Main admin interface routes - protected.GET("/", h.ShowDashboard) - protected.GET("/admin", h.ShowDashboard) - - // Object Store management routes - protected.GET("/object-store/buckets", h.ShowS3Buckets) - protected.GET("/object-store/buckets/:bucket", h.ShowBucketDetails) - protected.GET("/object-store/users", h.userHandlers.ShowObjectStoreUsers) - - // File browser routes - protected.GET("/files", h.fileBrowserHandlers.ShowFileBrowser) - - // Cluster management routes - protected.GET("/cluster/masters", h.clusterHandlers.ShowClusterMasters) - protected.GET("/cluster/filers", h.clusterHandlers.ShowClusterFilers) - protected.GET("/cluster/volume-servers", h.clusterHandlers.ShowClusterVolumeServers) - protected.GET("/cluster/volumes", h.clusterHandlers.ShowClusterVolumes) - protected.GET("/cluster/collections", h.clusterHandlers.ShowClusterCollections) - - // API routes for AJAX calls - api := protected.Group("/api") - { - api.GET("/cluster/topology", h.clusterHandlers.GetClusterTopology) - api.GET("/cluster/masters", h.clusterHandlers.GetMasters) - api.GET("/cluster/volumes", h.clusterHandlers.GetVolumeServers) - api.GET("/admin", h.adminServer.ShowAdmin) // JSON API for admin data - - // S3 API routes - s3Api := api.Group("/s3") - { - s3Api.GET("/buckets", h.adminServer.ListBucketsAPI) - s3Api.POST("/buckets", h.adminServer.CreateBucket) - s3Api.DELETE("/buckets/:bucket", h.adminServer.DeleteBucket) - s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails) - s3Api.PUT("/buckets/:bucket/quota", h.adminServer.UpdateBucketQuota) - } - - // User management API routes - usersApi := api.Group("/users") - { - usersApi.GET("", h.userHandlers.GetUsers) - usersApi.POST("", h.userHandlers.CreateUser) - usersApi.GET("/:username", h.userHandlers.GetUserDetails) - usersApi.PUT("/:username", h.userHandlers.UpdateUser) - usersApi.DELETE("/:username", h.userHandlers.DeleteUser) - usersApi.POST("/:username/access-keys", h.userHandlers.CreateAccessKey) - usersApi.DELETE("/:username/access-keys/:accessKeyId", h.userHandlers.DeleteAccessKey) - usersApi.GET("/:username/policies", h.userHandlers.GetUserPolicies) - usersApi.PUT("/:username/policies", h.userHandlers.UpdateUserPolicies) - } - - // File management API routes - filesApi := api.Group("/files") - { - filesApi.DELETE("/delete", h.fileBrowserHandlers.DeleteFile) - filesApi.DELETE("/delete-multiple", h.fileBrowserHandlers.DeleteMultipleFiles) - filesApi.POST("/create-folder", h.fileBrowserHandlers.CreateFolder) - filesApi.POST("/upload", h.fileBrowserHandlers.UploadFile) - filesApi.GET("/download", h.fileBrowserHandlers.DownloadFile) - filesApi.GET("/view", h.fileBrowserHandlers.ViewFile) - filesApi.GET("/properties", h.fileBrowserHandlers.GetFileProperties) - } - } - } else { - // No authentication required - all routes are public - r.GET("/", h.ShowDashboard) - r.GET("/admin", h.ShowDashboard) - - // Object Store management routes - r.GET("/object-store/buckets", h.ShowS3Buckets) - r.GET("/object-store/buckets/:bucket", h.ShowBucketDetails) - r.GET("/object-store/users", h.userHandlers.ShowObjectStoreUsers) - - // File browser routes - r.GET("/files", h.fileBrowserHandlers.ShowFileBrowser) - - // Cluster management routes - r.GET("/cluster/masters", h.clusterHandlers.ShowClusterMasters) - r.GET("/cluster/filers", h.clusterHandlers.ShowClusterFilers) - r.GET("/cluster/volume-servers", h.clusterHandlers.ShowClusterVolumeServers) - r.GET("/cluster/volumes", h.clusterHandlers.ShowClusterVolumes) - r.GET("/cluster/collections", h.clusterHandlers.ShowClusterCollections) - - // API routes for AJAX calls - api := r.Group("/api") - { - api.GET("/cluster/topology", h.clusterHandlers.GetClusterTopology) - api.GET("/cluster/masters", h.clusterHandlers.GetMasters) - api.GET("/cluster/volumes", h.clusterHandlers.GetVolumeServers) - api.GET("/admin", h.adminServer.ShowAdmin) // JSON API for admin data - - // S3 API routes - s3Api := api.Group("/s3") - { - s3Api.GET("/buckets", h.adminServer.ListBucketsAPI) - s3Api.POST("/buckets", h.adminServer.CreateBucket) - s3Api.DELETE("/buckets/:bucket", h.adminServer.DeleteBucket) - s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails) - s3Api.PUT("/buckets/:bucket/quota", h.adminServer.UpdateBucketQuota) - } - - // User management API routes - usersApi := api.Group("/users") - { - usersApi.GET("", h.userHandlers.GetUsers) - usersApi.POST("", h.userHandlers.CreateUser) - usersApi.GET("/:username", h.userHandlers.GetUserDetails) - usersApi.PUT("/:username", h.userHandlers.UpdateUser) - usersApi.DELETE("/:username", h.userHandlers.DeleteUser) - usersApi.POST("/:username/access-keys", h.userHandlers.CreateAccessKey) - usersApi.DELETE("/:username/access-keys/:accessKeyId", h.userHandlers.DeleteAccessKey) - usersApi.GET("/:username/policies", h.userHandlers.GetUserPolicies) - usersApi.PUT("/:username/policies", h.userHandlers.UpdateUserPolicies) - } - - // File management API routes - filesApi := api.Group("/files") - { - filesApi.DELETE("/delete", h.fileBrowserHandlers.DeleteFile) - filesApi.DELETE("/delete-multiple", h.fileBrowserHandlers.DeleteMultipleFiles) - filesApi.POST("/create-folder", h.fileBrowserHandlers.CreateFolder) - filesApi.POST("/upload", h.fileBrowserHandlers.UploadFile) - filesApi.GET("/download", h.fileBrowserHandlers.DownloadFile) - filesApi.GET("/view", h.fileBrowserHandlers.ViewFile) - filesApi.GET("/properties", h.fileBrowserHandlers.GetFileProperties) - } - } - } -} - -// HealthCheck returns the health status of the admin interface -func (h *AdminHandlers) HealthCheck(c *gin.Context) { - c.JSON(200, gin.H{"status": "ok"}) -} - -// ShowDashboard renders the main admin dashboard -func (h *AdminHandlers) ShowDashboard(c *gin.Context) { - // Get admin data from the server - adminData := h.getAdminData(c) - - // Render HTML template - c.Header("Content-Type", "text/html") - adminComponent := app.Admin(adminData) - layoutComponent := layout.Layout(c, adminComponent) - err := layoutComponent.Render(c.Request.Context(), c.Writer) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) - return - } -} - -// ShowS3Buckets renders the Object Store buckets management page -func (h *AdminHandlers) ShowS3Buckets(c *gin.Context) { - // Get Object Store buckets data from the server - s3Data := h.getS3BucketsData(c) - - // Render HTML template - c.Header("Content-Type", "text/html") - s3Component := app.S3Buckets(s3Data) - layoutComponent := layout.Layout(c, s3Component) - err := layoutComponent.Render(c.Request.Context(), c.Writer) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) - return - } -} - -// ShowBucketDetails returns detailed information about a specific bucket -func (h *AdminHandlers) ShowBucketDetails(c *gin.Context) { - bucketName := c.Param("bucket") - details, err := h.adminServer.GetBucketDetails(bucketName) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bucket details: " + err.Error()}) - return - } - c.JSON(http.StatusOK, details) -} - -// getS3BucketsData retrieves Object Store buckets data from the server -func (h *AdminHandlers) getS3BucketsData(c *gin.Context) dash.S3BucketsData { - username := c.GetString("username") - if username == "" { - username = "admin" - } - - // Get Object Store buckets - buckets, err := h.adminServer.GetS3Buckets() - if err != nil { - // Return empty data on error - return dash.S3BucketsData{ - Username: username, - Buckets: []dash.S3Bucket{}, - TotalBuckets: 0, - TotalSize: 0, - LastUpdated: time.Now(), - } - } - - // Calculate totals - var totalSize int64 - for _, bucket := range buckets { - totalSize += bucket.Size - } - - return dash.S3BucketsData{ - Username: username, - Buckets: buckets, - TotalBuckets: len(buckets), - TotalSize: totalSize, - LastUpdated: time.Now(), - } -} - -// getAdminData retrieves admin data from the server (now uses consolidated method) -func (h *AdminHandlers) getAdminData(c *gin.Context) dash.AdminData { - username := c.GetString("username") - - // Use the consolidated GetAdminData method from AdminServer - adminData, err := h.adminServer.GetAdminData(username) - if err != nil { - // Return default data when services are not available - if username == "" { - username = "admin" - } - - masterNodes := []dash.MasterNode{ - { - Address: "localhost:9333", - IsLeader: true, - Status: "unreachable", - }, - } - - return dash.AdminData{ - Username: username, - ClusterStatus: "warning", - TotalVolumes: 0, - TotalFiles: 0, - TotalSize: 0, - MasterNodes: masterNodes, - VolumeServers: []dash.VolumeServer{}, - FilerNodes: []dash.FilerNode{}, - DataCenters: []dash.DataCenter{}, - LastUpdated: time.Now(), - SystemHealth: "poor", - } - } - - return adminData -} - -// Helper functions -func (h *AdminHandlers) determineClusterStatus(topology *dash.ClusterTopology, masters []dash.MasterNode) string { - if len(topology.VolumeServers) == 0 { - return "warning" - } - return "healthy" -} - -func (h *AdminHandlers) determineSystemHealth(topology *dash.ClusterTopology, masters []dash.MasterNode) string { - if len(topology.VolumeServers) > 0 && len(masters) > 0 { - return "good" - } - return "fair" -} -- cgit v1.2.3