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 +}