diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go
index 9f97677e3..75b038a26 100644
--- a/weed/admin/dash/admin_server.go
+++ b/weed/admin/dash/admin_server.go
@@ -5,7 +5,6 @@ import (
"context"
"fmt"
"net/http"
- "strconv"
"time"
"github.com/gin-gonic/gin"
@@ -24,6 +23,8 @@ import (
"github.com/seaweedfs/seaweedfs/weed/util"
"github.com/seaweedfs/seaweedfs/weed/wdclient"
"google.golang.org/grpc"
+
+ "github.com/seaweedfs/seaweedfs/weed/s3api"
)
type AdminServer struct {
@@ -293,20 +294,11 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) {
var objectLockDuration int32 = 0
if resp.Entry.Extended != nil {
- if versioningBytes, exists := resp.Entry.Extended["s3.versioning"]; exists {
- versioningEnabled = string(versioningBytes) == "Enabled"
- }
- if objectLockBytes, exists := resp.Entry.Extended["s3.objectlock"]; exists {
- objectLockEnabled = string(objectLockBytes) == "Enabled"
- }
- if objectLockModeBytes, exists := resp.Entry.Extended["s3.objectlock.mode"]; exists {
- objectLockMode = string(objectLockModeBytes)
- }
- if objectLockDurationBytes, exists := resp.Entry.Extended["s3.objectlock.duration"]; exists {
- if duration, err := strconv.ParseInt(string(objectLockDurationBytes), 10, 32); err == nil {
- objectLockDuration = int32(duration)
- }
- }
+ // Use shared utility to extract versioning information
+ versioningEnabled = extractVersioningFromEntry(resp.Entry)
+
+ // Use shared utility to extract Object Lock information
+ objectLockEnabled, objectLockMode, objectLockDuration = extractObjectLockInfoFromEntry(resp.Entry)
}
bucket := S3Bucket{
@@ -379,20 +371,11 @@ func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error
var objectLockDuration int32 = 0
if bucketResp.Entry.Extended != nil {
- if versioningBytes, exists := bucketResp.Entry.Extended["s3.versioning"]; exists {
- versioningEnabled = string(versioningBytes) == "Enabled"
- }
- if objectLockBytes, exists := bucketResp.Entry.Extended["s3.objectlock"]; exists {
- objectLockEnabled = string(objectLockBytes) == "Enabled"
- }
- if objectLockModeBytes, exists := bucketResp.Entry.Extended["s3.objectlock.mode"]; exists {
- objectLockMode = string(objectLockModeBytes)
- }
- if objectLockDurationBytes, exists := bucketResp.Entry.Extended["s3.objectlock.duration"]; exists {
- if duration, err := strconv.ParseInt(string(objectLockDurationBytes), 10, 32); err == nil {
- objectLockDuration = int32(duration)
- }
- }
+ // Use shared utility to extract versioning information
+ versioningEnabled = extractVersioningFromEntry(bucketResp.Entry)
+
+ // Use shared utility to extract Object Lock information
+ objectLockEnabled, objectLockMode, objectLockDuration = extractObjectLockInfoFromEntry(bucketResp.Entry)
}
details.Bucket.VersioningEnabled = versioningEnabled
@@ -1502,3 +1485,19 @@ func (s *AdminServer) Shutdown() {
glog.V(1).Infof("Admin server shutdown complete")
}
+
+// Function to extract Object Lock information from bucket entry using shared utilities
+func extractObjectLockInfoFromEntry(entry *filer_pb.Entry) (bool, string, int32) {
+ // Try to load Object Lock configuration using shared utility
+ if config, found := s3api.LoadObjectLockConfigurationFromExtended(entry); found {
+ return s3api.ExtractObjectLockInfoFromConfig(config)
+ }
+
+ return false, "", 0
+}
+
+// Function to extract versioning information from bucket entry using shared utilities
+func extractVersioningFromEntry(entry *filer_pb.Entry) bool {
+ enabled, _ := s3api.LoadVersioningFromExtended(entry)
+ return enabled
+}
diff --git a/weed/admin/dash/bucket_management.go b/weed/admin/dash/bucket_management.go
index faa19ec99..bd488dc90 100644
--- a/weed/admin/dash/bucket_management.go
+++ b/weed/admin/dash/bucket_management.go
@@ -10,6 +10,7 @@ import (
"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
@@ -340,32 +341,43 @@ func (s *AdminServer) CreateS3BucketWithObjectLock(bucketName string, quotaBytes
TtlSec: 0,
}
- // Create extended attributes map for versioning and object lock
+ // Create extended attributes map for versioning
extended := make(map[string][]byte)
- if versioningEnabled {
- extended["s3.versioning"] = []byte("Enabled")
- } else {
- extended["s3.versioning"] = []byte("Suspended")
+
+ // 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 {
- extended["s3.objectlock"] = []byte("Enabled")
- extended["s3.objectlock.mode"] = []byte(objectLockMode)
- extended["s3.objectlock.duration"] = []byte(fmt.Sprintf("%d", objectLockDuration))
- } else {
- extended["s3.objectlock"] = []byte("Disabled")
+ // Validate Object Lock parameters
+ if err := s3api.ValidateObjectLockParameters(objectLockEnabled, objectLockMode, objectLockDuration); err != nil {
+ return fmt.Errorf("invalid Object Lock parameters: %w", err)
+ }
+
+ // Create Object Lock configuration using shared utility
+ objectLockConfig := s3api.CreateObjectLockConfigurationFromParams(objectLockEnabled, objectLockMode, objectLockDuration)
+
+ // 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: &filer_pb.Entry{
- Name: bucketName,
- IsDirectory: true,
- Attributes: attributes,
- Extended: extended,
- Quota: quota,
- },
+ Entry: bucketEntry,
})
if err != nil {
return fmt.Errorf("failed to create bucket directory: %w", err)
diff --git a/weed/s3api/object_lock_utils.go b/weed/s3api/object_lock_utils.go
new file mode 100644
index 000000000..6d2df7854
--- /dev/null
+++ b/weed/s3api/object_lock_utils.go
@@ -0,0 +1,228 @@
+package s3api
+
+import (
+ "encoding/xml"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
+)
+
+// ObjectLockUtils provides shared utilities for Object Lock configuration
+// These functions are used by both Admin UI and S3 API handlers to ensure consistency
+
+// VersioningUtils provides shared utilities for bucket versioning configuration
+// These functions ensure Admin UI and S3 API use the same versioning keys
+
+// StoreVersioningInExtended stores versioning configuration in entry extended attributes
+func StoreVersioningInExtended(entry *filer_pb.Entry, enabled bool) error {
+ if entry.Extended == nil {
+ entry.Extended = make(map[string][]byte)
+ }
+
+ if enabled {
+ entry.Extended[s3_constants.ExtVersioningKey] = []byte(s3_constants.VersioningEnabled)
+ } else {
+ entry.Extended[s3_constants.ExtVersioningKey] = []byte(s3_constants.VersioningSuspended)
+ }
+
+ return nil
+}
+
+// LoadVersioningFromExtended loads versioning configuration from entry extended attributes
+func LoadVersioningFromExtended(entry *filer_pb.Entry) (bool, bool) {
+ if entry == nil || entry.Extended == nil {
+ return false, false // not found, default to suspended
+ }
+
+ // Check for S3 API compatible key
+ if versioningBytes, exists := entry.Extended[s3_constants.ExtVersioningKey]; exists {
+ enabled := string(versioningBytes) == s3_constants.VersioningEnabled
+ return enabled, true
+ }
+
+ return false, false // not found
+}
+
+// CreateObjectLockConfiguration creates a new ObjectLockConfiguration with the specified parameters
+func CreateObjectLockConfiguration(enabled bool, mode string, days int, years int) *ObjectLockConfiguration {
+ if !enabled {
+ return nil
+ }
+
+ config := &ObjectLockConfiguration{
+ ObjectLockEnabled: s3_constants.ObjectLockEnabled,
+ }
+
+ // Add default retention rule if mode and period are specified
+ if mode != "" && (days > 0 || years > 0) {
+ config.Rule = &ObjectLockRule{
+ DefaultRetention: &DefaultRetention{
+ Mode: mode,
+ Days: days,
+ Years: years,
+ },
+ }
+ }
+
+ return config
+}
+
+// ObjectLockConfigurationToXML converts ObjectLockConfiguration to XML bytes
+func ObjectLockConfigurationToXML(config *ObjectLockConfiguration) ([]byte, error) {
+ if config == nil {
+ return nil, fmt.Errorf("object lock configuration is nil")
+ }
+
+ return xml.Marshal(config)
+}
+
+// XMLToObjectLockConfiguration parses XML bytes to ObjectLockConfiguration
+func XMLToObjectLockConfiguration(xmlData []byte) (*ObjectLockConfiguration, error) {
+ if len(xmlData) == 0 {
+ return nil, fmt.Errorf("XML data is empty")
+ }
+
+ var config ObjectLockConfiguration
+ if err := xml.Unmarshal(xmlData, &config); err != nil {
+ return nil, fmt.Errorf("failed to parse Object Lock configuration XML: %w", err)
+ }
+
+ return &config, nil
+}
+
+// StoreObjectLockConfigurationInExtended stores Object Lock configuration in entry extended attributes
+func StoreObjectLockConfigurationInExtended(entry *filer_pb.Entry, config *ObjectLockConfiguration) error {
+ if entry.Extended == nil {
+ entry.Extended = make(map[string][]byte)
+ }
+
+ if config == nil {
+ // Remove Object Lock configuration
+ delete(entry.Extended, s3_constants.ExtObjectLockEnabledKey)
+ delete(entry.Extended, s3_constants.ExtObjectLockConfigKey)
+ return nil
+ }
+
+ // Store the enabled flag
+ entry.Extended[s3_constants.ExtObjectLockEnabledKey] = []byte(config.ObjectLockEnabled)
+
+ // Store the full XML configuration
+ configXML, err := ObjectLockConfigurationToXML(config)
+ if err != nil {
+ return fmt.Errorf("failed to marshal Object Lock configuration: %w", err)
+ }
+ entry.Extended[s3_constants.ExtObjectLockConfigKey] = configXML
+
+ return nil
+}
+
+// LoadObjectLockConfigurationFromExtended loads Object Lock configuration from entry extended attributes
+func LoadObjectLockConfigurationFromExtended(entry *filer_pb.Entry) (*ObjectLockConfiguration, bool) {
+ if entry == nil || entry.Extended == nil {
+ return nil, false
+ }
+
+ // Check if Object Lock is enabled
+ enabledBytes, exists := entry.Extended[s3_constants.ExtObjectLockEnabledKey]
+ if !exists {
+ return nil, false
+ }
+
+ enabled := string(enabledBytes)
+ if enabled != s3_constants.ObjectLockEnabled && enabled != "true" {
+ return nil, false
+ }
+
+ // Try to load full XML configuration
+ if configXML, exists := entry.Extended[s3_constants.ExtObjectLockConfigKey]; exists {
+ if config, err := XMLToObjectLockConfiguration(configXML); err == nil {
+ return config, true
+ }
+ }
+
+ // Fallback: create minimal configuration for enabled Object Lock
+ return &ObjectLockConfiguration{
+ ObjectLockEnabled: s3_constants.ObjectLockEnabled,
+ }, true
+}
+
+// ExtractObjectLockInfoFromConfig extracts basic Object Lock information from configuration
+// Returns: enabled, mode, duration (for UI display)
+func ExtractObjectLockInfoFromConfig(config *ObjectLockConfiguration) (bool, string, int32) {
+ if config == nil || config.ObjectLockEnabled != s3_constants.ObjectLockEnabled {
+ return false, "", 0
+ }
+
+ if config.Rule == nil || config.Rule.DefaultRetention == nil {
+ return true, "", 0
+ }
+
+ defaultRetention := config.Rule.DefaultRetention
+
+ // Convert years to days for consistent representation
+ days := defaultRetention.Days
+ if defaultRetention.Years > 0 {
+ days += defaultRetention.Years * 365
+ }
+
+ return true, defaultRetention.Mode, int32(days)
+}
+
+// CreateObjectLockConfigurationFromParams creates ObjectLockConfiguration from individual parameters
+// This is a convenience function for Admin UI usage
+func CreateObjectLockConfigurationFromParams(enabled bool, mode string, duration int32) *ObjectLockConfiguration {
+ if !enabled {
+ return nil
+ }
+
+ return CreateObjectLockConfiguration(enabled, mode, int(duration), 0)
+}
+
+// ValidateObjectLockParameters validates Object Lock parameters before creating configuration
+func ValidateObjectLockParameters(enabled bool, mode string, duration int32) error {
+ if !enabled {
+ return nil
+ }
+
+ if mode != s3_constants.RetentionModeGovernance && mode != s3_constants.RetentionModeCompliance {
+ return fmt.Errorf("invalid object lock mode: %s, must be GOVERNANCE or COMPLIANCE", mode)
+ }
+
+ if duration <= 0 {
+ return fmt.Errorf("object lock duration must be greater than 0 days")
+ }
+
+ if duration > MaxRetentionDays {
+ return fmt.Errorf("object lock duration exceeds maximum allowed days: %d", MaxRetentionDays)
+ }
+
+ return nil
+}
+
+// SimpleXMLParseObjectLockMode extracts mode from XML string using simple string parsing
+// This is used as a fallback when full XML parsing is not needed
+func SimpleXMLParseObjectLockMode(xmlStr string) string {
+ if strings.Contains(xmlStr, "GOVERNANCE") {
+ return "GOVERNANCE"
+ } else if strings.Contains(xmlStr, "COMPLIANCE") {
+ return "COMPLIANCE"
+ }
+ return ""
+}
+
+// SimpleXMLParseObjectLockDays extracts days from XML string using simple string parsing
+// This is used as a fallback when full XML parsing is not needed
+func SimpleXMLParseObjectLockDays(xmlStr string) int32 {
+ if daysStart := strings.Index(xmlStr, ""); daysStart != -1 {
+ daysStart += 6 // length of ""
+ if daysEnd := strings.Index(xmlStr[daysStart:], ""); daysEnd != -1 {
+ if duration, err := strconv.ParseInt(xmlStr[daysStart:daysStart+daysEnd], 10, 32); err == nil {
+ return int32(duration)
+ }
+ }
+ }
+ return 0
+}