| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- 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
- })
- }
|