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