diff options
Diffstat (limited to 'weed/admin/dash')
| -rw-r--r-- | weed/admin/dash/admin_server.go | 69 | ||||
| -rw-r--r-- | weed/admin/dash/bucket_handlers.go | 325 | ||||
| -rw-r--r-- | weed/admin/dash/handler_admin.go | 120 |
3 files changed, 339 insertions, 175 deletions
diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index ccfcab6d6..79bce365b 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -4,9 +4,7 @@ import ( "context" "fmt" "net/http" - "os" "sort" - "strings" "time" "github.com/seaweedfs/seaweedfs/weed/cluster" @@ -83,6 +81,8 @@ type S3Bucket struct { ObjectCount int64 `json:"object_count"` LastModified time.Time `json:"last_modified"` Status string `json:"status"` + Quota int64 `json:"quota"` // Quota in bytes, 0 means no quota + QuotaEnabled bool `json:"quota_enabled"` // Whether quota is enabled } type S3Object struct { @@ -499,6 +499,15 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) { objectCount = collectionData.FileCount } + // Get quota information from entry + quota := resp.Entry.Quota + quotaEnabled := quota > 0 + if quota < 0 { + // Negative quota means disabled + quota = -quota + quotaEnabled = false + } + bucket := S3Bucket{ Name: bucketName, CreatedAt: time.Unix(resp.Entry.Attributes.Crtime, 0), @@ -506,6 +515,8 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) { ObjectCount: objectCount, LastModified: time.Unix(resp.Entry.Attributes.Mtime, 0), Status: "active", + Quota: quota, + QuotaEnabled: quotaEnabled, } buckets = append(buckets, bucket) } @@ -620,59 +631,7 @@ func (s *AdminServer) listBucketObjects(client filer_pb.SeaweedFilerClient, dire // CreateS3Bucket creates a new S3 bucket func (s *AdminServer) CreateS3Bucket(bucketName string) 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) - } - - // 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: uint32(1000), - Gid: uint32(1000), - Crtime: time.Now().Unix(), - Mtime: time.Now().Unix(), - TtlSec: 0, - }, - }, - }) - if err != nil { - return fmt.Errorf("failed to create bucket directory: %v", err) - } - - return nil - }) + return s.CreateS3BucketWithQuota(bucketName, 0, false) } // DeleteS3Bucket deletes an S3 bucket and all its contents diff --git a/weed/admin/dash/bucket_handlers.go b/weed/admin/dash/bucket_handlers.go new file mode 100644 index 000000000..e6edaa217 --- /dev/null +++ b/weed/admin/dash/bucket_handlers.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: uint32(1000), + Gid: uint32(1000), + 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/handler_admin.go b/weed/admin/dash/handler_admin.go index 72a59b2ff..a7a783aaf 100644 --- a/weed/admin/dash/handler_admin.go +++ b/weed/admin/dash/handler_admin.go @@ -25,20 +25,6 @@ type AdminData struct { SystemHealth string `json:"system_health"` } -// 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"` -} - // Object Store Users management structures type ObjectStoreUser struct { Username string `json:"username"` @@ -128,112 +114,6 @@ func (s *AdminServer) ShowOverview(c *gin.Context) { c.JSON(http.StatusOK, topology) } -// 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 - } - - err := s.CreateS3Bucket(req.Name) - 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, - }) -} - -// 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 buckets as JSON API -func (s *AdminServer) ListBucketsAPI(c *gin.Context) { - buckets, err := s.GetS3Buckets() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "buckets": buckets, - "count": len(buckets), - }) -} - // getMasterNodesStatus checks status of all master nodes func (s *AdminServer) getMasterNodesStatus() []MasterNode { var masterNodes []MasterNode |
