You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							395 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							395 lines
						
					
					
						
							12 KiB
						
					
					
				
								package dash
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"context"
							 | 
						|
									"fmt"
							 | 
						|
									"net/http"
							 | 
						|
									"os"
							 | 
						|
									"strings"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"github.com/gin-gonic/gin"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/s3api"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// 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
							 | 
						|
									VersioningEnabled   bool   `json:"versioning_enabled"`    // Whether versioning is enabled
							 | 
						|
									ObjectLockEnabled   bool   `json:"object_lock_enabled"`   // Whether object lock is enabled
							 | 
						|
									ObjectLockMode      string `json:"object_lock_mode"`      // Object lock mode: "GOVERNANCE" or "COMPLIANCE"
							 | 
						|
									SetDefaultRetention bool   `json:"set_default_retention"` // Whether to set default retention
							 | 
						|
									ObjectLockDuration  int32  `json:"object_lock_duration"`  // Default retention duration in days
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// 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
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate object lock settings
							 | 
						|
									if req.ObjectLockEnabled {
							 | 
						|
										// Object lock requires versioning to be enabled
							 | 
						|
										req.VersioningEnabled = true
							 | 
						|
								
							 | 
						|
										// Validate object lock mode
							 | 
						|
										if req.ObjectLockMode != "GOVERNANCE" && req.ObjectLockMode != "COMPLIANCE" {
							 | 
						|
											c.JSON(http.StatusBadRequest, gin.H{"error": "Object lock mode must be either GOVERNANCE or COMPLIANCE"})
							 | 
						|
											return
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Validate retention duration if default retention is enabled
							 | 
						|
										if req.SetDefaultRetention {
							 | 
						|
											if req.ObjectLockDuration <= 0 {
							 | 
						|
												c.JSON(http.StatusBadRequest, gin.H{"error": "Object lock duration must be greater than 0 days when default retention is enabled"})
							 | 
						|
												return
							 | 
						|
											}
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Convert quota to bytes
							 | 
						|
									quotaBytes := convertQuotaToBytes(req.QuotaSize, req.QuotaUnit)
							 | 
						|
								
							 | 
						|
									err := s.CreateS3BucketWithObjectLock(req.Name, quotaBytes, req.QuotaEnabled, req.VersioningEnabled, req.ObjectLockEnabled, req.ObjectLockMode, req.SetDefaultRetention, req.ObjectLockDuration)
							 | 
						|
									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,
							 | 
						|
										"versioning_enabled":   req.VersioningEnabled,
							 | 
						|
										"object_lock_enabled":  req.ObjectLockEnabled,
							 | 
						|
										"object_lock_mode":     req.ObjectLockMode,
							 | 
						|
										"object_lock_duration": req.ObjectLockDuration,
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// 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: %w", 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: %w", 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.CreateS3BucketWithObjectLock(bucketName, quotaBytes, quotaEnabled, false, false, "", false, 0)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// CreateS3BucketWithObjectLock creates a new S3 bucket with quota, versioning, and object lock settings
							 | 
						|
								func (s *AdminServer) CreateS3BucketWithObjectLock(bucketName string, quotaBytes int64, quotaEnabled, versioningEnabled, objectLockEnabled bool, objectLockMode string, setDefaultRetention bool, objectLockDuration int32) 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: %w", 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
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Prepare bucket attributes with versioning and object lock metadata
							 | 
						|
										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,
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Create extended attributes map for versioning
							 | 
						|
										extended := make(map[string][]byte)
							 | 
						|
								
							 | 
						|
										// Create bucket entry
							 | 
						|
										bucketEntry := &filer_pb.Entry{
							 | 
						|
											Name:        bucketName,
							 | 
						|
											IsDirectory: true,
							 | 
						|
											Attributes:  attributes,
							 | 
						|
											Extended:    extended,
							 | 
						|
											Quota:       quota,
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Handle versioning using shared utilities
							 | 
						|
										if err := s3api.StoreVersioningInExtended(bucketEntry, versioningEnabled); err != nil {
							 | 
						|
											return fmt.Errorf("failed to store versioning configuration: %w", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Handle Object Lock configuration using shared utilities
							 | 
						|
										if objectLockEnabled {
							 | 
						|
											var duration int32 = 0
							 | 
						|
											if setDefaultRetention {
							 | 
						|
												// Validate Object Lock parameters only when setting default retention
							 | 
						|
												if err := s3api.ValidateObjectLockParameters(objectLockEnabled, objectLockMode, objectLockDuration); err != nil {
							 | 
						|
													return fmt.Errorf("invalid Object Lock parameters: %w", err)
							 | 
						|
												}
							 | 
						|
												duration = objectLockDuration
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											// Create Object Lock configuration using shared utility
							 | 
						|
											objectLockConfig := s3api.CreateObjectLockConfigurationFromParams(objectLockEnabled, objectLockMode, duration)
							 | 
						|
								
							 | 
						|
											// Store Object Lock configuration in extended attributes using shared utility
							 | 
						|
											if err := s3api.StoreObjectLockConfigurationInExtended(bucketEntry, objectLockConfig); err != nil {
							 | 
						|
												return fmt.Errorf("failed to store Object Lock configuration: %w", err)
							 | 
						|
											}
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Create bucket directory under /buckets
							 | 
						|
										_, err = client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
							 | 
						|
											Directory: "/buckets",
							 | 
						|
											Entry:     bucketEntry,
							 | 
						|
										})
							 | 
						|
										if err != nil {
							 | 
						|
											return fmt.Errorf("failed to create bucket directory: %w", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										return nil
							 | 
						|
									})
							 | 
						|
								}
							 |