7 changed files with 796 additions and 215 deletions
-
69weed/admin/dash/admin_server.go
-
325weed/admin/dash/bucket_handlers.go
-
120weed/admin/dash/handler_admin.go
-
2weed/admin/handlers/handlers.go
-
147weed/admin/static/js/admin.js
-
139weed/admin/view/app/s3_buckets.templ
-
193weed/admin/view/app/s3_buckets_templ.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 |
||||
|
}) |
||||
|
} |
193
weed/admin/view/app/s3_buckets_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue