package dash import ( "context" "errors" "fmt" "net/http" "os" "strings" "time" "github.com/gorilla/mux" "github.com/seaweedfs/seaweedfs/weed/filer" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/s3api" "github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3lifecycle" ) // MaxBucketLifecycleRules mirrors AWS S3's limit of 1000 lifecycle rules per bucket. const MaxBucketLifecycleRules = 1000 // MaxBucketLifecycleRuleIDLength mirrors the S3 API's limit on a lifecycle rule's . const MaxBucketLifecycleRuleIDLength = 255 // MaxOwnerNameLength is the maximum allowed length for bucket owner identity names. // This is a reasonable limit to prevent abuse; AWS IAM user names are limited to 64 chars, // but we use 256 to allow for more complex identity formats (e.g., email addresses). const MaxOwnerNameLength = 256 // 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"` // Pagination CurrentPage int `json:"current_page"` TotalPages int `json:"total_pages"` PageSize int `json:"page_size"` // Sorting SortBy string `json:"sort_by"` SortOrder string `json:"sort_order"` } type CreateBucketRequest struct { Name string `json:"name"` // validated manually in CreateBucket 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 Owner string `json:"owner"` // Bucket owner identity (for S3 IAM authentication) } // S3 Bucket Management Handlers // ShowS3Buckets displays the Object Store buckets management page func (s *AdminServer) ShowS3Buckets(w http.ResponseWriter, r *http.Request) { username := UsernameFromContext(r.Context()) data, err := s.GetS3BucketsData(1, 100, "name", "asc") if err != nil { writeJSONError(w, http.StatusInternalServerError, "Failed to get Object Store buckets: "+err.Error()) return } data.Username = username writeJSON(w, http.StatusOK, data) } // ShowBucketDetails displays detailed information about a specific bucket func (s *AdminServer) ShowBucketDetails(w http.ResponseWriter, r *http.Request) { bucketName := mux.Vars(r)["bucket"] if bucketName == "" { writeJSONError(w, http.StatusBadRequest, "Bucket name is required") return } details, err := s.GetBucketDetails(bucketName) if err != nil { writeJSONError(w, http.StatusInternalServerError, "Failed to get bucket details: "+err.Error()) return } writeJSON(w, http.StatusOK, details) } // ShowBucketLifecycle returns the lifecycle configuration for a specific bucket func (s *AdminServer) ShowBucketLifecycle(w http.ResponseWriter, r *http.Request) { bucketName := mux.Vars(r)["bucket"] if bucketName == "" { writeJSONError(w, http.StatusBadRequest, "Bucket name is required") return } lifecycle, err := s.GetBucketLifecycle(bucketName) if err != nil { writeJSONError(w, http.StatusInternalServerError, "Failed to get bucket lifecycle: "+err.Error()) return } writeJSON(w, http.StatusOK, lifecycle) } // UpdateBucketLifecycle replaces the entire lifecycle configuration for a bucket. func (s *AdminServer) UpdateBucketLifecycle(w http.ResponseWriter, r *http.Request) { if !requireSessionCSRFToken(w, r) { return } bucketName := mux.Vars(r)["bucket"] if bucketName == "" { writeJSONError(w, http.StatusBadRequest, "Bucket name is required") return } var req struct { Rules []BucketLifecycleRule `json:"rules"` } if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil { writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error()) return } if err := validateBucketLifecycleRules(req.Rules); err != nil { writeJSONError(w, http.StatusBadRequest, err.Error()) return } if err := s.SetBucketLifecycle(bucketName, req.Rules); err != nil { writeJSONError(w, bucketLifecycleErrorStatus(err), "Failed to update bucket lifecycle: "+err.Error()) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "message": "Bucket lifecycle updated successfully", "bucket": bucketName, }) } // DeleteBucketLifecycle clears the entire lifecycle configuration for a bucket. func (s *AdminServer) DeleteBucketLifecycle(w http.ResponseWriter, r *http.Request) { if !requireSessionCSRFToken(w, r) { return } bucketName := mux.Vars(r)["bucket"] if bucketName == "" { writeJSONError(w, http.StatusBadRequest, "Bucket name is required") return } if err := s.SetBucketLifecycle(bucketName, nil); err != nil { writeJSONError(w, bucketLifecycleErrorStatus(err), "Failed to delete bucket lifecycle: "+err.Error()) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "message": "Bucket lifecycle deleted successfully", "bucket": bucketName, }) } // bucketLifecycleErrorStatus keeps a request for a bucket that does not exist // out of the 5xx bucket, where a client would retry it. func bucketLifecycleErrorStatus(err error) int { if errors.Is(err, ErrBucketNotFound) { return http.StatusNotFound } return http.StatusInternalServerError } // validateBucketLifecycleRules rejects a rule set before any write is // attempted, so a malformed PUT can't half-apply. Mirrors the constraints // AWS enforces on PutBucketLifecycleConfiguration plus the local rule cap. func validateBucketLifecycleRules(rules []BucketLifecycleRule) error { if len(rules) > MaxBucketLifecycleRules { return fmt.Errorf("a bucket may have at most %d lifecycle rules, got %d", MaxBucketLifecycleRules, len(rules)) } seenIDs := make(map[string]bool, len(rules)) for i, rule := range rules { label := fmt.Sprintf("rule %d", i+1) if rule.ID != "" { label = fmt.Sprintf("rule %q", rule.ID) if len(rule.ID) > MaxBucketLifecycleRuleIDLength { return fmt.Errorf("%s: ID must be %d characters or less", label, MaxBucketLifecycleRuleIDLength) } if seenIDs[rule.ID] { return fmt.Errorf("duplicate rule ID %q", rule.ID) } seenIDs[rule.ID] = true } switch rule.Status { case s3lifecycle.StatusEnabled, s3lifecycle.StatusDisabled: default: return fmt.Errorf("%s: status must be %q or %q, got %q", label, s3lifecycle.StatusEnabled, s3lifecycle.StatusDisabled, rule.Status) } if rule.ExpirationDays > 0 && rule.ExpirationDate != "" { return fmt.Errorf("%s: expiration_days and expiration_date are mutually exclusive", label) } if rule.ExpirationDays < 0 { return fmt.Errorf("%s: expiration_days must be positive", label) } if rule.ExpirationDate != "" { if _, err := time.Parse(time.DateOnly, rule.ExpirationDate); err != nil { return fmt.Errorf("%s: invalid expiration_date %q, expected YYYY-MM-DD", label, rule.ExpirationDate) } } if rule.ExpiredObjectDeleteMarker && (rule.ExpirationDays > 0 || rule.ExpirationDate != "") { return fmt.Errorf("%s: expired_object_delete_marker cannot be combined with expiration_days or expiration_date", label) } if rule.NoncurrentVersionExpirationDays < 0 { return fmt.Errorf("%s: noncurrent_version_expiration_days must be positive", label) } if rule.NewerNoncurrentVersions < 0 { return fmt.Errorf("%s: newer_noncurrent_versions must be positive", label) } if rule.AbortMultipartDays < 0 { return fmt.Errorf("%s: abort_multipart_days must be positive", label) } if rule.SizeGreaterThan < 0 || rule.SizeLessThan < 0 { return fmt.Errorf("%s: size_greater_than and size_less_than must be positive", label) } if rule.SizeGreaterThan > 0 && rule.SizeLessThan > 0 && rule.SizeGreaterThan >= rule.SizeLessThan { return fmt.Errorf("%s: size_greater_than must be less than size_less_than", label) } for k, v := range rule.Tags { if strings.TrimSpace(k) == "" { return fmt.Errorf("%s: tag keys must not be empty", label) } if strings.TrimSpace(v) == "" { return fmt.Errorf("%s: tag value for key %q must not be empty", label, k) } } hasAction := rule.ExpirationDays > 0 || rule.ExpirationDate != "" || rule.ExpiredObjectDeleteMarker || rule.NoncurrentVersionExpirationDays > 0 || rule.NewerNoncurrentVersions > 0 || rule.AbortMultipartDays > 0 if !hasAction { return fmt.Errorf("%s: must specify at least one action (expiration, noncurrent version expiration, or abort incomplete multipart upload)", label) } } return nil } // ShowBucketPolicy returns the policy document for a specific bucket, or // {"bucket": ..., "policy": null} if the bucket has none. func (s *AdminServer) ShowBucketPolicy(w http.ResponseWriter, r *http.Request) { bucketName := mux.Vars(r)["bucket"] if bucketName == "" { writeJSONError(w, http.StatusBadRequest, "Bucket name is required") return } policy, raw, err := s.GetBucketPolicy(bucketName) if err != nil { writeJSONError(w, bucketPolicyErrorStatus(err), "Failed to get bucket policy: "+err.Error()) return } resp := map[string]interface{}{ "bucket": bucketName, "policy": policy, } if policy == nil && len(raw) > 0 { // Stored bytes the decoder rejects: hand them to the JSON tab so // the operator can fix or delete the document. resp["policy_text"] = string(raw) } writeJSON(w, http.StatusOK, resp) } // UpdateBucketPolicy replaces the bucket policy for a bucket. func (s *AdminServer) UpdateBucketPolicy(w http.ResponseWriter, r *http.Request) { if !requireSessionCSRFToken(w, r) { return } bucketName := mux.Vars(r)["bucket"] if bucketName == "" { writeJSONError(w, http.StatusBadRequest, "Bucket name is required") return } var req struct { Policy *policy_engine.PolicyDocument `json:"policy"` } if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil { writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error()) return } if req.Policy == nil { writeJSONError(w, http.StatusBadRequest, "policy is required; use DELETE to clear a bucket policy") return } if err := s.SetBucketPolicy(bucketName, req.Policy); err != nil { writeJSONError(w, bucketPolicyErrorStatus(err), "Failed to update bucket policy: "+err.Error()) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "message": "Bucket policy updated successfully", "bucket": bucketName, }) } // RemoveBucketPolicy clears the bucket policy for a bucket. Named // "Remove", not "Delete", because (*AdminServer).DeleteBucketPolicy is the // data-layer method this handler calls. func (s *AdminServer) RemoveBucketPolicy(w http.ResponseWriter, r *http.Request) { if !requireSessionCSRFToken(w, r) { return } bucketName := mux.Vars(r)["bucket"] if bucketName == "" { writeJSONError(w, http.StatusBadRequest, "Bucket name is required") return } if err := s.DeleteBucketPolicy(bucketName); err != nil { writeJSONError(w, bucketPolicyErrorStatus(err), "Failed to delete bucket policy: "+err.Error()) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "message": "Bucket policy deleted successfully", "bucket": bucketName, }) } // bucketPolicyErrorStatus keeps a request for a bucket that does not exist, // or an invalid policy document, out of the 5xx bucket where a client // would retry it. Mirrors bucketLifecycleErrorStatus. func bucketPolicyErrorStatus(err error) int { if errors.Is(err, ErrBucketNotFound) { return http.StatusNotFound } if errors.Is(err, ErrInvalidBucketPolicy) { return http.StatusBadRequest } return http.StatusInternalServerError } // CreateBucket creates a new S3 bucket func (s *AdminServer) CreateBucket(w http.ResponseWriter, r *http.Request) { var req CreateBucketRequest if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil { writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error()) return } if strings.TrimSpace(req.Name) == "" { writeJSONError(w, http.StatusBadRequest, "Bucket name is required") return } // Validate bucket name (basic validation) if len(req.Name) < 3 || len(req.Name) > 63 { writeJSONError(w, http.StatusBadRequest, "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" { writeJSONError(w, http.StatusBadRequest, "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 { writeJSONError(w, http.StatusBadRequest, "Object lock duration must be greater than 0 days when default retention is enabled") return } } } normalizedUnit, err := normalizeQuotaUnit(req.QuotaUnit) if err != nil { writeJSONError(w, http.StatusBadRequest, err.Error()) return } req.QuotaUnit = normalizedUnit quotaBytes := convertQuotaToBytes(req.QuotaSize, normalizedUnit) // Validate quota: if enabled, size must be greater than 0 if req.QuotaEnabled && quotaBytes <= 0 { writeJSONError(w, http.StatusBadRequest, "Quota size must be greater than 0 when quota is enabled") return } // Sanitize owner: trim whitespace and enforce max length owner := strings.TrimSpace(req.Owner) if len(owner) > MaxOwnerNameLength { writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Owner name must be %d characters or less", MaxOwnerNameLength)) return } err = s.CreateS3BucketWithObjectLock(req.Name, quotaBytes, req.QuotaEnabled, req.VersioningEnabled, req.ObjectLockEnabled, req.ObjectLockMode, req.SetDefaultRetention, req.ObjectLockDuration, owner) if err != nil { writeJSONError(w, http.StatusInternalServerError, "Failed to create bucket: "+err.Error()) return } writeJSON(w, http.StatusCreated, map[string]interface{}{ "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, "owner": owner, }) } // UpdateBucketQuota updates the quota settings for a bucket func (s *AdminServer) UpdateBucketQuota(w http.ResponseWriter, r *http.Request) { bucketName := mux.Vars(r)["bucket"] if bucketName == "" { writeJSONError(w, http.StatusBadRequest, "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 := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil { writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error()) return } if req.QuotaEnabled && req.QuotaSize <= 0 { writeJSONError(w, http.StatusBadRequest, "quota_size must be > 0 when quota_enabled is true") return } normalizedUnit, err := normalizeQuotaUnit(req.QuotaUnit) if err != nil { writeJSONError(w, http.StatusBadRequest, err.Error()) return } req.QuotaUnit = normalizedUnit // Convert quota to bytes quotaBytes := convertQuotaToBytes(req.QuotaSize, normalizedUnit) err = s.SetBucketQuota(bucketName, quotaBytes, req.QuotaEnabled) if err != nil { writeJSONError(w, http.StatusInternalServerError, "Failed to update bucket quota: "+err.Error()) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "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(w http.ResponseWriter, r *http.Request) { bucketName := mux.Vars(r)["bucket"] if bucketName == "" { writeJSONError(w, http.StatusBadRequest, "Bucket name is required") return } err := s.DeleteS3Bucket(bucketName) if err != nil { writeJSONError(w, http.StatusInternalServerError, "Failed to delete bucket: "+err.Error()) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "message": "Bucket deleted successfully", "bucket": bucketName, }) } // UpdateBucketOwner updates the owner of an S3 bucket func (s *AdminServer) UpdateBucketOwner(w http.ResponseWriter, r *http.Request) { bucketName := mux.Vars(r)["bucket"] if bucketName == "" { writeJSONError(w, http.StatusBadRequest, "Bucket name is required") return } // Use pointer to detect if owner field was explicitly provided var req struct { Owner *string `json:"owner"` } if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil { writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error()) return } // Require owner field to be explicitly provided if req.Owner == nil { writeJSONError(w, http.StatusBadRequest, "Owner field is required (use empty string to clear owner)") return } // Trim and validate owner owner := strings.TrimSpace(*req.Owner) if len(owner) > MaxOwnerNameLength { writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Owner name must be %d characters or less", MaxOwnerNameLength)) return } err := s.SetBucketOwner(bucketName, owner) if err != nil { writeJSONError(w, http.StatusInternalServerError, "Failed to update bucket owner: "+err.Error()) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "message": "Bucket owner updated successfully", "bucket": bucketName, "owner": owner, }) } // SetBucketOwner sets the owner of a bucket func (s *AdminServer) SetBucketOwner(bucketName string, owner string) 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("lookup bucket %s: %w", bucketName, err) } bucketEntry := lookupResp.Entry // Initialize Extended map if nil if bucketEntry.Extended == nil { bucketEntry.Extended = make(map[string][]byte) } // Set or remove the owner. The account id recorded by the S3 API goes with // it: the S3 API derives it from the owning identity when it is absent, so // leaving the old one behind would keep the previous owner of the objects. delete(bucketEntry.Extended, s3_constants.ExtAmzOwnerKey) if owner == "" { delete(bucketEntry.Extended, s3_constants.AmzIdentityId) } else { bucketEntry.Extended[s3_constants.AmzIdentityId] = []byte(owner) } // 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 owner: %w", err) } return nil }) } // ListBucketsAPI returns the list of buckets as JSON func (s *AdminServer) ListBucketsAPI(w http.ResponseWriter, r *http.Request) { buckets, err := s.GetS3Buckets() if err != nil { writeJSONError(w, http.StatusInternalServerError, "Failed to get buckets: "+err.Error()) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "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 unit { case "TB": return size * 1024 * 1024 * 1024 * 1024 case "GB": return size * 1024 * 1024 * 1024 case "MB": return size * 1024 * 1024 case "KB": return size * 1024 case "B": return size default: return 0 } } func normalizeQuotaUnit(unit string) (string, error) { normalized := strings.ToUpper(strings.TrimSpace(unit)) if normalized == "" { return "MB", nil } switch normalized { case "B", "KB", "MB", "GB", "TB": return normalized, nil default: return "", fmt.Errorf("unsupported quota unit: %s", unit) } } // 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) } if quota <= 0 { // with no active quota, quota enforcement can no longer clear a // read-only flag it turned on, so lift it here if _, err := filer.ClearBucketReadOnly(context.Background(), client, "/buckets", bucketName); err != nil { return fmt.Errorf("failed to clear bucket read-only flag: %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, object lock settings, and owner func (s *AdminServer) CreateS3BucketWithObjectLock(bucketName string, quotaBytes int64, quotaEnabled, versioningEnabled, objectLockEnabled bool, objectLockMode string, setDefaultRetention bool, objectLockDuration int32, owner 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: %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 and owner extended := make(map[string][]byte) // Set bucket owner if specified if owner != "" { extended[s3_constants.AmzIdentityId] = []byte(owner) } // 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 var mode string = "" 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 mode = objectLockMode } // Create Object Lock configuration using shared utility objectLockConfig := s3api.CreateObjectLockConfigurationFromParams(objectLockEnabled, mode, 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 }) }