aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--test/s3/retention/s3_retention_test.go4
-rw-r--r--weed/admin/dash/admin_server.go57
-rw-r--r--weed/admin/dash/bucket_management.go46
-rw-r--r--weed/s3api/auth_credentials_subscribe.go70
-rw-r--r--weed/s3api/object_lock_utils.go232
-rw-r--r--weed/s3api/s3_constants/extend_key.go6
-rw-r--r--weed/s3api/s3api_bucket_config.go37
-rw-r--r--weed/s3api/s3api_bucket_handlers.go16
-rw-r--r--weed/s3api/s3api_bucket_handlers_object_lock_config.go139
-rw-r--r--weed/s3api/s3api_object_handlers_legal_hold.go126
-rw-r--r--weed/s3api/s3api_object_handlers_put.go103
-rw-r--r--weed/s3api/s3api_object_handlers_retention.go222
-rw-r--r--weed/s3api/s3api_object_lock_fix_test.go90
-rw-r--r--weed/s3api/s3api_server.go6
14 files changed, 842 insertions, 312 deletions
diff --git a/test/s3/retention/s3_retention_test.go b/test/s3/retention/s3_retention_test.go
index 54eb12848..fd85921b7 100644
--- a/test/s3/retention/s3_retention_test.go
+++ b/test/s3/retention/s3_retention_test.go
@@ -437,8 +437,10 @@ func TestObjectLockConfiguration(t *testing.T) {
})
require.NoError(t, err)
assert.Equal(t, types.ObjectLockEnabledEnabled, configResp.ObjectLockConfiguration.ObjectLockEnabled)
+ require.NotNil(t, configResp.ObjectLockConfiguration.Rule.DefaultRetention, "DefaultRetention should not be nil")
+ require.NotNil(t, configResp.ObjectLockConfiguration.Rule.DefaultRetention.Days, "Days should not be nil")
assert.Equal(t, types.ObjectLockRetentionModeGovernance, configResp.ObjectLockConfiguration.Rule.DefaultRetention.Mode)
- assert.Equal(t, int32(30), configResp.ObjectLockConfiguration.Rule.DefaultRetention.Days)
+ assert.Equal(t, int32(30), *configResp.ObjectLockConfiguration.Rule.DefaultRetention.Days)
}
// TestRetentionWithVersions tests retention with specific object versions
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/auth_credentials_subscribe.go b/weed/s3api/auth_credentials_subscribe.go
index 1f6b30312..4d6b0fd19 100644
--- a/weed/s3api/auth_credentials_subscribe.go
+++ b/weed/s3api/auth_credentials_subscribe.go
@@ -1,6 +1,8 @@
package s3api
import (
+ "time"
+
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
@@ -80,12 +82,74 @@ func (s3a *S3ApiServer) onCircuitBreakerConfigUpdate(dir, filename string, conte
func (s3a *S3ApiServer) onBucketMetadataChange(dir string, oldEntry *filer_pb.Entry, newEntry *filer_pb.Entry) error {
if dir == s3a.option.BucketsPath {
if newEntry != nil {
+ // Update bucket registry (existing functionality)
s3a.bucketRegistry.LoadBucketMetadata(newEntry)
- glog.V(0).Infof("updated bucketMetadata %s/%s", dir, newEntry)
- } else {
+ glog.V(0).Infof("updated bucketMetadata %s/%s", dir, newEntry.Name)
+
+ // Update bucket configuration cache with new entry
+ s3a.updateBucketConfigCacheFromEntry(newEntry)
+ } else if oldEntry != nil {
+ // Remove from bucket registry (existing functionality)
s3a.bucketRegistry.RemoveBucketMetadata(oldEntry)
- glog.V(0).Infof("remove bucketMetadata %s/%s", dir, newEntry)
+ glog.V(0).Infof("remove bucketMetadata %s/%s", dir, oldEntry.Name)
+
+ // Remove from bucket configuration cache
+ s3a.invalidateBucketConfigCache(oldEntry.Name)
}
}
return nil
}
+
+// updateBucketConfigCacheFromEntry updates the bucket config cache when a bucket entry changes
+func (s3a *S3ApiServer) updateBucketConfigCacheFromEntry(entry *filer_pb.Entry) {
+ if s3a.bucketConfigCache == nil {
+ return
+ }
+
+ bucket := entry.Name
+ glog.V(2).Infof("updateBucketConfigCacheFromEntry: updating cache for bucket %s", bucket)
+
+ // Create new bucket config from the entry
+ config := &BucketConfig{
+ Name: bucket,
+ Entry: entry,
+ }
+
+ // Extract configuration from extended attributes
+ if entry.Extended != nil {
+ if versioning, exists := entry.Extended[s3_constants.ExtVersioningKey]; exists {
+ config.Versioning = string(versioning)
+ }
+ if ownership, exists := entry.Extended[s3_constants.ExtOwnershipKey]; exists {
+ config.Ownership = string(ownership)
+ }
+ if acl, exists := entry.Extended[s3_constants.ExtAmzAclKey]; exists {
+ config.ACL = acl
+ }
+ if owner, exists := entry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
+ config.Owner = string(owner)
+ }
+ // Parse Object Lock configuration if present
+ if objectLockConfig, found := LoadObjectLockConfigurationFromExtended(entry); found {
+ config.ObjectLockConfig = objectLockConfig
+ glog.V(2).Infof("updateBucketConfigCacheFromEntry: cached Object Lock configuration for bucket %s", bucket)
+ }
+ }
+
+ // Update timestamp
+ config.LastModified = time.Now()
+
+ // Update cache
+ s3a.bucketConfigCache.Set(bucket, config)
+ glog.V(2).Infof("updateBucketConfigCacheFromEntry: updated bucket config cache for %s", bucket)
+}
+
+// invalidateBucketConfigCache removes a bucket from the configuration cache
+func (s3a *S3ApiServer) invalidateBucketConfigCache(bucket string) {
+ if s3a.bucketConfigCache == nil {
+ return
+ }
+
+ s3a.bucketConfigCache.Remove(bucket)
+ glog.V(2).Infof("invalidateBucketConfigCache: removed bucket %s from cache", bucket)
+}
diff --git a/weed/s3api/object_lock_utils.go b/weed/s3api/object_lock_utils.go
new file mode 100644
index 000000000..ffde5bd36
--- /dev/null
+++ b/weed/s3api/object_lock_utils.go
@@ -0,0 +1,232 @@
+package s3api
+
+import (
+ "encoding/xml"
+ "fmt"
+ "strconv"
+
+ "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)
+}
+
+// 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.ExtObjectLockDefaultModeKey)
+ delete(entry.Extended, s3_constants.ExtObjectLockDefaultDaysKey)
+ delete(entry.Extended, s3_constants.ExtObjectLockDefaultYearsKey)
+ return nil
+ }
+
+ // Store the enabled flag
+ entry.Extended[s3_constants.ExtObjectLockEnabledKey] = []byte(config.ObjectLockEnabled)
+
+ // Store default retention configuration if present
+ if config.Rule != nil && config.Rule.DefaultRetention != nil {
+ defaultRetention := config.Rule.DefaultRetention
+
+ // Store mode
+ if defaultRetention.Mode != "" {
+ entry.Extended[s3_constants.ExtObjectLockDefaultModeKey] = []byte(defaultRetention.Mode)
+ }
+
+ // Store days
+ if defaultRetention.Days > 0 {
+ entry.Extended[s3_constants.ExtObjectLockDefaultDaysKey] = []byte(strconv.Itoa(defaultRetention.Days))
+ }
+
+ // Store years
+ if defaultRetention.Years > 0 {
+ entry.Extended[s3_constants.ExtObjectLockDefaultYearsKey] = []byte(strconv.Itoa(defaultRetention.Years))
+ }
+ } else {
+ // Remove default retention if not present
+ delete(entry.Extended, s3_constants.ExtObjectLockDefaultModeKey)
+ delete(entry.Extended, s3_constants.ExtObjectLockDefaultDaysKey)
+ delete(entry.Extended, s3_constants.ExtObjectLockDefaultYearsKey)
+ }
+
+ 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
+ }
+
+ // Create basic configuration
+ config := &ObjectLockConfiguration{
+ ObjectLockEnabled: s3_constants.ObjectLockEnabled,
+ }
+
+ // Load default retention configuration if present
+ if modeBytes, exists := entry.Extended[s3_constants.ExtObjectLockDefaultModeKey]; exists {
+ mode := string(modeBytes)
+
+ // Parse days and years
+ var days, years int
+ if daysBytes, exists := entry.Extended[s3_constants.ExtObjectLockDefaultDaysKey]; exists {
+ if parsed, err := strconv.Atoi(string(daysBytes)); err == nil {
+ days = parsed
+ }
+ }
+ if yearsBytes, exists := entry.Extended[s3_constants.ExtObjectLockDefaultYearsKey]; exists {
+ if parsed, err := strconv.Atoi(string(yearsBytes)); err == nil {
+ years = parsed
+ }
+ }
+
+ // Create rule if we have a mode and at least days or years
+ if mode != "" && (days > 0 || years > 0) {
+ config.Rule = &ObjectLockRule{
+ DefaultRetention: &DefaultRetention{
+ Mode: mode,
+ Days: days,
+ Years: years,
+ },
+ }
+ }
+ }
+
+ return config, 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 ErrInvalidObjectLockMode
+ }
+
+ if duration <= 0 {
+ return ErrInvalidObjectLockDuration
+ }
+
+ if duration > MaxRetentionDays {
+ return ErrObjectLockDurationExceeded
+ }
+
+ return nil
+}
diff --git a/weed/s3api/s3_constants/extend_key.go b/weed/s3api/s3_constants/extend_key.go
index edfa4fe1d..f0f223a45 100644
--- a/weed/s3api/s3_constants/extend_key.go
+++ b/weed/s3api/s3_constants/extend_key.go
@@ -20,7 +20,11 @@ const (
ExtRetentionUntilDateKey = "Seaweed-X-Amz-Retention-Until-Date"
ExtLegalHoldKey = "Seaweed-X-Amz-Legal-Hold"
ExtObjectLockEnabledKey = "Seaweed-X-Amz-Object-Lock-Enabled"
- ExtObjectLockConfigKey = "Seaweed-X-Amz-Object-Lock-Config"
+
+ // Object Lock Bucket Configuration (individual components, not XML)
+ ExtObjectLockDefaultModeKey = "Lock-Default-Mode"
+ ExtObjectLockDefaultDaysKey = "Lock-Default-Days"
+ ExtObjectLockDefaultYearsKey = "Lock-Default-Years"
)
// Object Lock and Retention Constants
diff --git a/weed/s3api/s3api_bucket_config.go b/weed/s3api/s3api_bucket_config.go
index 43c056973..725ee3596 100644
--- a/weed/s3api/s3api_bucket_config.go
+++ b/weed/s3api/s3api_bucket_config.go
@@ -17,24 +17,29 @@ import (
// BucketConfig represents cached bucket configuration
type BucketConfig struct {
- Name string
- Versioning string // "Enabled", "Suspended", or ""
- Ownership string
- ACL []byte
- Owner string
- CORS *cors.CORSConfiguration
- LastModified time.Time
- Entry *filer_pb.Entry
+ Name string
+ Versioning string // "Enabled", "Suspended", or ""
+ Ownership string
+ ACL []byte
+ Owner string
+ CORS *cors.CORSConfiguration
+ ObjectLockConfig *ObjectLockConfiguration // Cached parsed Object Lock configuration
+ LastModified time.Time
+ Entry *filer_pb.Entry
}
// BucketConfigCache provides caching for bucket configurations
+// Cache entries are automatically updated/invalidated through metadata subscription events,
+// so TTL serves as a safety fallback rather than the primary consistency mechanism
type BucketConfigCache struct {
cache map[string]*BucketConfig
mutex sync.RWMutex
- ttl time.Duration
+ ttl time.Duration // Safety fallback TTL; real-time consistency maintained via events
}
// NewBucketConfigCache creates a new bucket configuration cache
+// TTL can be set to a longer duration since cache consistency is maintained
+// through real-time metadata subscription events rather than TTL expiration
func NewBucketConfigCache(ttl time.Duration) *BucketConfigCache {
return &BucketConfigCache{
cache: make(map[string]*BucketConfig),
@@ -52,7 +57,7 @@ func (bcc *BucketConfigCache) Get(bucket string) (*BucketConfig, bool) {
return nil, false
}
- // Check if cache entry is expired
+ // Check if cache entry is expired (safety fallback; entries are normally updated via events)
if time.Since(config.LastModified) > bcc.ttl {
return nil, false
}
@@ -121,6 +126,11 @@ func (s3a *S3ApiServer) getBucketConfig(bucket string) (*BucketConfig, s3err.Err
if owner, exists := bucketEntry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
config.Owner = string(owner)
}
+ // Parse Object Lock configuration if present
+ if objectLockConfig, found := LoadObjectLockConfigurationFromExtended(bucketEntry); found {
+ config.ObjectLockConfig = objectLockConfig
+ glog.V(2).Infof("getBucketConfig: cached Object Lock configuration for bucket %s", bucket)
+ }
}
// Load CORS configuration from .s3metadata
@@ -173,6 +183,13 @@ func (s3a *S3ApiServer) updateBucketConfig(bucket string, updateFn func(*BucketC
if config.Owner != "" {
config.Entry.Extended[s3_constants.ExtAmzOwnerKey] = []byte(config.Owner)
}
+ // Update Object Lock configuration
+ if config.ObjectLockConfig != nil {
+ if err := StoreObjectLockConfigurationInExtended(config.Entry, config.ObjectLockConfig); err != nil {
+ glog.Errorf("updateBucketConfig: failed to store Object Lock configuration for bucket %s: %v", bucket, err)
+ return s3err.ErrInternalError
+ }
+ }
// Save to filer
err := s3a.updateEntry(s3a.option.BucketsPath, config.Entry)
diff --git a/weed/s3api/s3api_bucket_handlers.go b/weed/s3api/s3api_bucket_handlers.go
index 0bc4a7b10..e30f172a7 100644
--- a/weed/s3api/s3api_bucket_handlers.go
+++ b/weed/s3api/s3api_bucket_handlers.go
@@ -147,25 +147,13 @@ func (s3a *S3ApiServer) PutBucketHandler(w http.ResponseWriter, r *http.Request)
// Enable versioning (required for Object Lock)
bucketConfig.Versioning = s3_constants.VersioningEnabled
- // Enable Object Lock configuration
- if bucketConfig.Entry.Extended == nil {
- bucketConfig.Entry.Extended = make(map[string][]byte)
- }
-
// Create basic Object Lock configuration (enabled without default retention)
- // The ObjectLockConfiguration struct is defined below in this file.
objectLockConfig := &ObjectLockConfiguration{
ObjectLockEnabled: s3_constants.ObjectLockEnabled,
}
- // Store the configuration as XML in extended attributes
- configXML, err := xml.Marshal(objectLockConfig)
- if err != nil {
- return fmt.Errorf("failed to marshal Object Lock configuration to XML: %w", err)
- }
-
- bucketConfig.Entry.Extended[s3_constants.ExtObjectLockConfigKey] = configXML
- bucketConfig.Entry.Extended[s3_constants.ExtObjectLockEnabledKey] = []byte(s3_constants.ObjectLockEnabled)
+ // Set the cached Object Lock configuration
+ bucketConfig.ObjectLockConfig = objectLockConfig
return nil
})
diff --git a/weed/s3api/s3api_bucket_handlers_object_lock_config.go b/weed/s3api/s3api_bucket_handlers_object_lock_config.go
new file mode 100644
index 000000000..494f203a4
--- /dev/null
+++ b/weed/s3api/s3api_bucket_handlers_object_lock_config.go
@@ -0,0 +1,139 @@
+package s3api
+
+import (
+ "encoding/xml"
+ "net/http"
+
+ "github.com/seaweedfs/seaweedfs/weed/glog"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
+ stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
+)
+
+// PutObjectLockConfigurationHandler Put object Lock configuration
+// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLockConfiguration.html
+func (s3a *S3ApiServer) PutObjectLockConfigurationHandler(w http.ResponseWriter, r *http.Request) {
+ bucket, _ := s3_constants.GetBucketAndObject(r)
+ glog.V(3).Infof("PutObjectLockConfigurationHandler %s", bucket)
+
+ // Check if Object Lock is available for this bucket (requires versioning)
+ if !s3a.handleObjectLockAvailabilityCheck(w, r, bucket, "PutObjectLockConfigurationHandler") {
+ return
+ }
+
+ // Parse object lock configuration from request body
+ config, err := parseObjectLockConfiguration(r)
+ if err != nil {
+ glog.Errorf("PutObjectLockConfigurationHandler: failed to parse object lock config: %v", err)
+ s3err.WriteErrorResponse(w, r, s3err.ErrMalformedXML)
+ return
+ }
+
+ // Validate object lock configuration
+ if err := validateObjectLockConfiguration(config); err != nil {
+ glog.Errorf("PutObjectLockConfigurationHandler: invalid object lock config: %v", err)
+ s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
+ return
+ }
+
+ // Set object lock configuration on the bucket
+ errCode := s3a.updateBucketConfig(bucket, func(bucketConfig *BucketConfig) error {
+ // Set the cached Object Lock configuration
+ bucketConfig.ObjectLockConfig = config
+ return nil
+ })
+
+ if errCode != s3err.ErrNone {
+ glog.Errorf("PutObjectLockConfigurationHandler: failed to set object lock config: %v", errCode)
+ s3err.WriteErrorResponse(w, r, errCode)
+ return
+ }
+
+ // Record metrics
+ stats_collect.RecordBucketActiveTime(bucket)
+
+ // Return success (HTTP 200 with no body)
+ w.WriteHeader(http.StatusOK)
+ glog.V(3).Infof("PutObjectLockConfigurationHandler: successfully set object lock config for %s", bucket)
+}
+
+// GetObjectLockConfigurationHandler Get object Lock configuration
+// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectLockConfiguration.html
+func (s3a *S3ApiServer) GetObjectLockConfigurationHandler(w http.ResponseWriter, r *http.Request) {
+ bucket, _ := s3_constants.GetBucketAndObject(r)
+ glog.V(3).Infof("GetObjectLockConfigurationHandler %s", bucket)
+
+ // Get bucket configuration
+ bucketConfig, errCode := s3a.getBucketConfig(bucket)
+ if errCode != s3err.ErrNone {
+ glog.Errorf("GetObjectLockConfigurationHandler: failed to get bucket config: %v", errCode)
+ s3err.WriteErrorResponse(w, r, errCode)
+ return
+ }
+
+ var configXML []byte
+
+ // Check if we have cached Object Lock configuration
+ if bucketConfig.ObjectLockConfig != nil {
+ // Use cached configuration and marshal it to XML for response
+ marshaledXML, err := xml.Marshal(bucketConfig.ObjectLockConfig)
+ if err != nil {
+ glog.Errorf("GetObjectLockConfigurationHandler: failed to marshal cached Object Lock config: %v", err)
+ s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ return
+ }
+
+ // Write XML response
+ w.Header().Set("Content-Type", "application/xml")
+ w.WriteHeader(http.StatusOK)
+ if _, err := w.Write([]byte(xml.Header)); err != nil {
+ glog.Errorf("GetObjectLockConfigurationHandler: failed to write XML header: %v", err)
+ return
+ }
+ if _, err := w.Write(marshaledXML); err != nil {
+ glog.Errorf("GetObjectLockConfigurationHandler: failed to write config XML: %v", err)
+ return
+ }
+ glog.V(3).Infof("GetObjectLockConfigurationHandler: successfully retrieved cached object lock config for %s", bucket)
+ return
+ }
+
+ // Fallback: check for legacy storage in extended attributes
+ if bucketConfig.Entry.Extended != nil {
+ // Check if Object Lock is enabled via boolean flag
+ if enabledBytes, exists := bucketConfig.Entry.Extended[s3_constants.ExtObjectLockEnabledKey]; exists {
+ enabled := string(enabledBytes)
+ if enabled == s3_constants.ObjectLockEnabled || enabled == "true" {
+ // Generate minimal XML configuration for enabled Object Lock without retention policies
+ minimalConfig := `<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled></ObjectLockConfiguration>`
+ configXML = []byte(minimalConfig)
+ }
+ }
+ }
+
+ // If no Object Lock configuration found, return error
+ if len(configXML) == 0 {
+ s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchObjectLockConfiguration)
+ return
+ }
+
+ // Set response headers
+ w.Header().Set("Content-Type", "application/xml")
+ w.WriteHeader(http.StatusOK)
+
+ // Write XML response
+ if _, err := w.Write([]byte(xml.Header)); err != nil {
+ glog.Errorf("GetObjectLockConfigurationHandler: failed to write XML header: %v", err)
+ return
+ }
+
+ if _, err := w.Write(configXML); err != nil {
+ glog.Errorf("GetObjectLockConfigurationHandler: failed to write config XML: %v", err)
+ return
+ }
+
+ // Record metrics
+ stats_collect.RecordBucketActiveTime(bucket)
+
+ glog.V(3).Infof("GetObjectLockConfigurationHandler: successfully retrieved object lock config for %s", bucket)
+}
diff --git a/weed/s3api/s3api_object_handlers_legal_hold.go b/weed/s3api/s3api_object_handlers_legal_hold.go
new file mode 100644
index 000000000..9cf523477
--- /dev/null
+++ b/weed/s3api/s3api_object_handlers_legal_hold.go
@@ -0,0 +1,126 @@
+package s3api
+
+import (
+ "encoding/xml"
+ "errors"
+ "net/http"
+
+ "github.com/seaweedfs/seaweedfs/weed/glog"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
+ stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
+)
+
+// PutObjectLegalHoldHandler Put object Legal Hold
+// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLegalHold.html
+func (s3a *S3ApiServer) PutObjectLegalHoldHandler(w http.ResponseWriter, r *http.Request) {
+ bucket, object := s3_constants.GetBucketAndObject(r)
+ glog.V(3).Infof("PutObjectLegalHoldHandler %s %s", bucket, object)
+
+ // Check if Object Lock is available for this bucket (requires versioning)
+ if !s3a.handleObjectLockAvailabilityCheck(w, r, bucket, "PutObjectLegalHoldHandler") {
+ return
+ }
+
+ // Get version ID from query parameters
+ versionId := r.URL.Query().Get("versionId")
+
+ // Parse legal hold configuration from request body
+ legalHold, err := parseObjectLegalHold(r)
+ if err != nil {
+ glog.Errorf("PutObjectLegalHoldHandler: failed to parse legal hold config: %v", err)
+ s3err.WriteErrorResponse(w, r, s3err.ErrMalformedXML)
+ return
+ }
+
+ // Validate legal hold configuration
+ if err := validateLegalHold(legalHold); err != nil {
+ glog.Errorf("PutObjectLegalHoldHandler: invalid legal hold config: %v", err)
+ s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
+ return
+ }
+
+ // Set legal hold on the object
+ if err := s3a.setObjectLegalHold(bucket, object, versionId, legalHold); err != nil {
+ glog.Errorf("PutObjectLegalHoldHandler: failed to set legal hold: %v", err)
+
+ // Handle specific error cases
+ if errors.Is(err, ErrObjectNotFound) || errors.Is(err, ErrVersionNotFound) {
+ s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
+ return
+ }
+
+ s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ return
+ }
+
+ // Record metrics
+ stats_collect.RecordBucketActiveTime(bucket)
+
+ // Return success (HTTP 200 with no body)
+ w.WriteHeader(http.StatusOK)
+ glog.V(3).Infof("PutObjectLegalHoldHandler: successfully set legal hold for %s/%s", bucket, object)
+}
+
+// GetObjectLegalHoldHandler Get object Legal Hold
+// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectLegalHold.html
+func (s3a *S3ApiServer) GetObjectLegalHoldHandler(w http.ResponseWriter, r *http.Request) {
+ bucket, object := s3_constants.GetBucketAndObject(r)
+ glog.V(3).Infof("GetObjectLegalHoldHandler %s %s", bucket, object)
+
+ // Check if Object Lock is available for this bucket (requires versioning)
+ if !s3a.handleObjectLockAvailabilityCheck(w, r, bucket, "GetObjectLegalHoldHandler") {
+ return
+ }
+
+ // Get version ID from query parameters
+ versionId := r.URL.Query().Get("versionId")
+
+ // Get legal hold configuration for the object
+ legalHold, err := s3a.getObjectLegalHold(bucket, object, versionId)
+ if err != nil {
+ glog.Errorf("GetObjectLegalHoldHandler: failed to get legal hold: %v", err)
+
+ // Handle specific error cases
+ if errors.Is(err, ErrObjectNotFound) || errors.Is(err, ErrVersionNotFound) {
+ s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
+ return
+ }
+
+ if errors.Is(err, ErrNoLegalHoldConfiguration) {
+ s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchObjectLegalHold)
+ return
+ }
+
+ s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ return
+ }
+
+ // Marshal legal hold configuration to XML
+ legalHoldXML, err := xml.Marshal(legalHold)
+ if err != nil {
+ glog.Errorf("GetObjectLegalHoldHandler: failed to marshal legal hold: %v", err)
+ s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ return
+ }
+
+ // Set response headers
+ w.Header().Set("Content-Type", "application/xml")
+ w.WriteHeader(http.StatusOK)
+
+ // Write XML response
+ if _, err := w.Write([]byte(xml.Header)); err != nil {
+ glog.Errorf("GetObjectLegalHoldHandler: failed to write XML header: %v", err)
+ return
+ }
+
+ if _, err := w.Write(legalHoldXML); err != nil {
+ glog.Errorf("GetObjectLegalHoldHandler: failed to write legal hold XML: %v", err)
+ return
+ }
+
+ // Record metrics
+ stats_collect.RecordBucketActiveTime(bucket)
+
+ glog.V(3).Infof("GetObjectLegalHoldHandler: successfully retrieved legal hold for %s/%s", bucket, object)
+}
diff --git a/weed/s3api/s3api_object_handlers_put.go b/weed/s3api/s3api_object_handlers_put.go
index 1d9fe9f92..50d308566 100644
--- a/weed/s3api/s3api_object_handlers_put.go
+++ b/weed/s3api/s3api_object_handlers_put.go
@@ -12,12 +12,11 @@ import (
"time"
"github.com/pquerna/cachecontrol/cacheobject"
+ "github.com/seaweedfs/seaweedfs/weed/glog"
+ "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
"github.com/seaweedfs/seaweedfs/weed/security"
-
- "github.com/seaweedfs/seaweedfs/weed/glog"
- "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
weed_server "github.com/seaweedfs/seaweedfs/weed/server"
stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
)
@@ -32,6 +31,8 @@ var (
ErrObjectLockModeRequiresDate = errors.New("object lock mode requires retention until date")
ErrRetentionDateRequiresMode = errors.New("retention until date requires object lock mode")
ErrGovernanceBypassVersioningRequired = errors.New("governance bypass header can only be used on versioned buckets")
+ ErrInvalidObjectLockDuration = errors.New("object lock duration must be greater than 0 days")
+ ErrObjectLockDurationExceeded = errors.New("object lock duration exceeds maximum allowed days")
)
func (s3a *S3ApiServer) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
@@ -374,28 +375,30 @@ func (s3a *S3ApiServer) updateLatestVersionInDirectory(bucket, object, versionId
}
// extractObjectLockMetadataFromRequest extracts object lock headers from PUT requests
-// and stores them in the entry's Extended attributes
+// and applies bucket default retention if no explicit retention is provided
func (s3a *S3ApiServer) extractObjectLockMetadataFromRequest(r *http.Request, entry *filer_pb.Entry) error {
if entry.Extended == nil {
entry.Extended = make(map[string][]byte)
}
- // Extract object lock mode (GOVERNANCE or COMPLIANCE)
- if mode := r.Header.Get(s3_constants.AmzObjectLockMode); mode != "" {
- entry.Extended[s3_constants.ExtObjectLockModeKey] = []byte(mode)
- glog.V(2).Infof("extractObjectLockMetadataFromRequest: storing object lock mode: %s", mode)
+ // Extract explicit object lock mode (GOVERNANCE or COMPLIANCE)
+ explicitMode := r.Header.Get(s3_constants.AmzObjectLockMode)
+ if explicitMode != "" {
+ entry.Extended[s3_constants.ExtObjectLockModeKey] = []byte(explicitMode)
+ glog.V(2).Infof("extractObjectLockMetadataFromRequest: storing explicit object lock mode: %s", explicitMode)
}
- // Extract retention until date
- if retainUntilDate := r.Header.Get(s3_constants.AmzObjectLockRetainUntilDate); retainUntilDate != "" {
+ // Extract explicit retention until date
+ explicitRetainUntilDate := r.Header.Get(s3_constants.AmzObjectLockRetainUntilDate)
+ if explicitRetainUntilDate != "" {
// Parse the ISO8601 date and convert to Unix timestamp for storage
- parsedTime, err := time.Parse(time.RFC3339, retainUntilDate)
+ parsedTime, err := time.Parse(time.RFC3339, explicitRetainUntilDate)
if err != nil {
glog.Errorf("extractObjectLockMetadataFromRequest: failed to parse retention until date, expected format: %s, error: %v", time.RFC3339, err)
return ErrInvalidRetentionDateFormat
}
entry.Extended[s3_constants.ExtRetentionUntilDateKey] = []byte(strconv.FormatInt(parsedTime.Unix(), 10))
- glog.V(2).Infof("extractObjectLockMetadataFromRequest: storing retention until date (timestamp: %d)", parsedTime.Unix())
+ glog.V(2).Infof("extractObjectLockMetadataFromRequest: storing explicit retention until date (timestamp: %d)", parsedTime.Unix())
}
// Extract legal hold status
@@ -410,6 +413,78 @@ func (s3a *S3ApiServer) extractObjectLockMetadataFromRequest(r *http.Request, en
}
}
+ // Apply bucket default retention if no explicit retention was provided
+ // This implements AWS S3 behavior where bucket default retention automatically applies to new objects
+ if explicitMode == "" && explicitRetainUntilDate == "" {
+ bucket, _ := s3_constants.GetBucketAndObject(r)
+ if err := s3a.applyBucketDefaultRetention(bucket, entry); err != nil {
+ glog.V(2).Infof("extractObjectLockMetadataFromRequest: skipping bucket default retention for %s: %v", bucket, err)
+ // Don't fail the upload if default retention can't be applied - this matches AWS behavior
+ }
+ }
+
+ return nil
+}
+
+// applyBucketDefaultRetention applies bucket default retention settings to a new object
+// This implements AWS S3 behavior where bucket default retention automatically applies to new objects
+// when no explicit retention headers are provided in the upload request
+func (s3a *S3ApiServer) applyBucketDefaultRetention(bucket string, entry *filer_pb.Entry) error {
+ // Safety check - if bucket config cache is not available, skip default retention
+ if s3a.bucketConfigCache == nil {
+ return nil
+ }
+
+ // Get bucket configuration (getBucketConfig handles caching internally)
+ bucketConfig, errCode := s3a.getBucketConfig(bucket)
+ if errCode != s3err.ErrNone {
+ return fmt.Errorf("failed to get bucket config: %v", errCode)
+ }
+
+ // Check if bucket has cached Object Lock configuration
+ if bucketConfig.ObjectLockConfig == nil {
+ return nil // No Object Lock configuration
+ }
+
+ objectLockConfig := bucketConfig.ObjectLockConfig
+
+ // Check if there's a default retention rule
+ if objectLockConfig.Rule == nil || objectLockConfig.Rule.DefaultRetention == nil {
+ return nil // No default retention configured
+ }
+
+ defaultRetention := objectLockConfig.Rule.DefaultRetention
+
+ // Validate default retention has required fields
+ if defaultRetention.Mode == "" {
+ return fmt.Errorf("default retention missing mode")
+ }
+
+ if defaultRetention.Days == 0 && defaultRetention.Years == 0 {
+ return fmt.Errorf("default retention missing period")
+ }
+
+ // Calculate retention until date based on default retention period
+ var retainUntilDate time.Time
+ now := time.Now()
+
+ if defaultRetention.Days > 0 {
+ retainUntilDate = now.AddDate(0, 0, defaultRetention.Days)
+ } else if defaultRetention.Years > 0 {
+ retainUntilDate = now.AddDate(defaultRetention.Years, 0, 0)
+ }
+
+ // Apply default retention to the object
+ if entry.Extended == nil {
+ entry.Extended = make(map[string][]byte)
+ }
+
+ entry.Extended[s3_constants.ExtObjectLockModeKey] = []byte(defaultRetention.Mode)
+ entry.Extended[s3_constants.ExtRetentionUntilDateKey] = []byte(strconv.FormatInt(retainUntilDate.Unix(), 10))
+
+ glog.V(2).Infof("applyBucketDefaultRetention: applied default retention %s until %s for bucket %s",
+ defaultRetention.Mode, retainUntilDate.Format(time.RFC3339), bucket)
+
return nil
}
@@ -493,6 +568,10 @@ func mapValidationErrorToS3Error(err error) s3err.ErrorCode {
return s3err.ErrInvalidRequest
case errors.Is(err, ErrGovernanceBypassVersioningRequired):
return s3err.ErrInvalidRequest
+ case errors.Is(err, ErrInvalidObjectLockDuration):
+ return s3err.ErrInvalidRequest
+ case errors.Is(err, ErrObjectLockDurationExceeded):
+ return s3err.ErrInvalidRequest
default:
return s3err.ErrInvalidRequest
}
diff --git a/weed/s3api/s3api_object_handlers_retention.go b/weed/s3api/s3api_object_handlers_retention.go
index e92e821c8..a419b469e 100644
--- a/weed/s3api/s3api_object_handlers_retention.go
+++ b/weed/s3api/s3api_object_handlers_retention.go
@@ -132,225 +132,3 @@ func (s3a *S3ApiServer) GetObjectRetentionHandler(w http.ResponseWriter, r *http
glog.V(3).Infof("GetObjectRetentionHandler: successfully retrieved retention for %s/%s", bucket, object)
}
-
-// PutObjectLegalHoldHandler Put object Legal Hold
-// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLegalHold.html
-func (s3a *S3ApiServer) PutObjectLegalHoldHandler(w http.ResponseWriter, r *http.Request) {
- bucket, object := s3_constants.GetBucketAndObject(r)
- glog.V(3).Infof("PutObjectLegalHoldHandler %s %s", bucket, object)
-
- // Check if Object Lock is available for this bucket (requires versioning)
- if !s3a.handleObjectLockAvailabilityCheck(w, r, bucket, "PutObjectLegalHoldHandler") {
- return
- }
-
- // Get version ID from query parameters
- versionId := r.URL.Query().Get("versionId")
-
- // Parse legal hold configuration from request body
- legalHold, err := parseObjectLegalHold(r)
- if err != nil {
- glog.Errorf("PutObjectLegalHoldHandler: failed to parse legal hold config: %v", err)
- s3err.WriteErrorResponse(w, r, s3err.ErrMalformedXML)
- return
- }
-
- // Validate legal hold configuration
- if err := validateLegalHold(legalHold); err != nil {
- glog.Errorf("PutObjectLegalHoldHandler: invalid legal hold config: %v", err)
- s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
- return
- }
-
- // Set legal hold on the object
- if err := s3a.setObjectLegalHold(bucket, object, versionId, legalHold); err != nil {
- glog.Errorf("PutObjectLegalHoldHandler: failed to set legal hold: %v", err)
-
- // Handle specific error cases
- if errors.Is(err, ErrObjectNotFound) || errors.Is(err, ErrVersionNotFound) {
- s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
- return
- }
-
- s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
- return
- }
-
- // Record metrics
- stats_collect.RecordBucketActiveTime(bucket)
-
- // Return success (HTTP 200 with no body)
- w.WriteHeader(http.StatusOK)
- glog.V(3).Infof("PutObjectLegalHoldHandler: successfully set legal hold for %s/%s", bucket, object)
-}
-
-// GetObjectLegalHoldHandler Get object Legal Hold
-// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectLegalHold.html
-func (s3a *S3ApiServer) GetObjectLegalHoldHandler(w http.ResponseWriter, r *http.Request) {
- bucket, object := s3_constants.GetBucketAndObject(r)
- glog.V(3).Infof("GetObjectLegalHoldHandler %s %s", bucket, object)
-
- // Check if Object Lock is available for this bucket (requires versioning)
- if !s3a.handleObjectLockAvailabilityCheck(w, r, bucket, "GetObjectLegalHoldHandler") {
- return
- }
-
- // Get version ID from query parameters
- versionId := r.URL.Query().Get("versionId")
-
- // Get legal hold configuration for the object
- legalHold, err := s3a.getObjectLegalHold(bucket, object, versionId)
- if err != nil {
- glog.Errorf("GetObjectLegalHoldHandler: failed to get legal hold: %v", err)
-
- // Handle specific error cases
- if errors.Is(err, ErrObjectNotFound) || errors.Is(err, ErrVersionNotFound) {
- s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
- return
- }
-
- if errors.Is(err, ErrNoLegalHoldConfiguration) {
- s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchObjectLegalHold)
- return
- }
-
- s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
- return
- }
-
- // Marshal legal hold configuration to XML
- legalHoldXML, err := xml.Marshal(legalHold)
- if err != nil {
- glog.Errorf("GetObjectLegalHoldHandler: failed to marshal legal hold: %v", err)
- s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
- return
- }
-
- // Set response headers
- w.Header().Set("Content-Type", "application/xml")
- w.WriteHeader(http.StatusOK)
-
- // Write XML response
- if _, err := w.Write([]byte(xml.Header)); err != nil {
- glog.Errorf("GetObjectLegalHoldHandler: failed to write XML header: %v", err)
- return
- }
-
- if _, err := w.Write(legalHoldXML); err != nil {
- glog.Errorf("GetObjectLegalHoldHandler: failed to write legal hold XML: %v", err)
- return
- }
-
- // Record metrics
- stats_collect.RecordBucketActiveTime(bucket)
-
- glog.V(3).Infof("GetObjectLegalHoldHandler: successfully retrieved legal hold for %s/%s", bucket, object)
-}
-
-// PutObjectLockConfigurationHandler Put object Lock configuration
-// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLockConfiguration.html
-func (s3a *S3ApiServer) PutObjectLockConfigurationHandler(w http.ResponseWriter, r *http.Request) {
- bucket, _ := s3_constants.GetBucketAndObject(r)
- glog.V(3).Infof("PutObjectLockConfigurationHandler %s", bucket)
-
- // Check if Object Lock is available for this bucket (requires versioning)
- if !s3a.handleObjectLockAvailabilityCheck(w, r, bucket, "PutObjectLockConfigurationHandler") {
- return
- }
-
- // Parse object lock configuration from request body
- config, err := parseObjectLockConfiguration(r)
- if err != nil {
- glog.Errorf("PutObjectLockConfigurationHandler: failed to parse object lock config: %v", err)
- s3err.WriteErrorResponse(w, r, s3err.ErrMalformedXML)
- return
- }
-
- // Validate object lock configuration
- if err := validateObjectLockConfiguration(config); err != nil {
- glog.Errorf("PutObjectLockConfigurationHandler: invalid object lock config: %v", err)
- s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
- return
- }
-
- // Set object lock configuration on the bucket
- errCode := s3a.updateBucketConfig(bucket, func(bucketConfig *BucketConfig) error {
- if bucketConfig.Entry.Extended == nil {
- bucketConfig.Entry.Extended = make(map[string][]byte)
- }
-
- // Store the configuration as JSON in extended attributes
- configXML, err := xml.Marshal(config)
- if err != nil {
- return err
- }
-
- bucketConfig.Entry.Extended[s3_constants.ExtObjectLockConfigKey] = configXML
-
- if config.ObjectLockEnabled != "" {
- bucketConfig.Entry.Extended[s3_constants.ExtObjectLockEnabledKey] = []byte(config.ObjectLockEnabled)
- }
-
- return nil
- })
-
- if errCode != s3err.ErrNone {
- glog.Errorf("PutObjectLockConfigurationHandler: failed to set object lock config: %v", errCode)
- s3err.WriteErrorResponse(w, r, errCode)
- return
- }
-
- // Record metrics
- stats_collect.RecordBucketActiveTime(bucket)
-
- // Return success (HTTP 200 with no body)
- w.WriteHeader(http.StatusOK)
- glog.V(3).Infof("PutObjectLockConfigurationHandler: successfully set object lock config for %s", bucket)
-}
-
-// GetObjectLockConfigurationHandler Get object Lock configuration
-// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectLockConfiguration.html
-func (s3a *S3ApiServer) GetObjectLockConfigurationHandler(w http.ResponseWriter, r *http.Request) {
- bucket, _ := s3_constants.GetBucketAndObject(r)
- glog.V(3).Infof("GetObjectLockConfigurationHandler %s", bucket)
-
- // Get bucket configuration
- bucketConfig, errCode := s3a.getBucketConfig(bucket)
- if errCode != s3err.ErrNone {
- glog.Errorf("GetObjectLockConfigurationHandler: failed to get bucket config: %v", errCode)
- s3err.WriteErrorResponse(w, r, errCode)
- return
- }
-
- // Check if object lock configuration exists
- if bucketConfig.Entry.Extended == nil {
- s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchObjectLockConfiguration)
- return
- }
-
- configXML, exists := bucketConfig.Entry.Extended[s3_constants.ExtObjectLockConfigKey]
- if !exists {
- s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchObjectLockConfiguration)
- return
- }
-
- // Set response headers
- w.Header().Set("Content-Type", "application/xml")
- w.WriteHeader(http.StatusOK)
-
- // Write XML response
- if _, err := w.Write([]byte(xml.Header)); err != nil {
- glog.Errorf("GetObjectLockConfigurationHandler: failed to write XML header: %v", err)
- return
- }
-
- if _, err := w.Write(configXML); err != nil {
- glog.Errorf("GetObjectLockConfigurationHandler: failed to write config XML: %v", err)
- return
- }
-
- // Record metrics
- stats_collect.RecordBucketActiveTime(bucket)
-
- glog.V(3).Infof("GetObjectLockConfigurationHandler: successfully retrieved object lock config for %s", bucket)
-}
diff --git a/weed/s3api/s3api_object_lock_fix_test.go b/weed/s3api/s3api_object_lock_fix_test.go
new file mode 100644
index 000000000..e8a3cf6ba
--- /dev/null
+++ b/weed/s3api/s3api_object_lock_fix_test.go
@@ -0,0 +1,90 @@
+package s3api
+
+import (
+ "testing"
+
+ "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
+ "github.com/stretchr/testify/assert"
+)
+
+// TestVeeamObjectLockBugFix tests the fix for the bug where GetObjectLockConfigurationHandler
+// would return NoSuchObjectLockConfiguration for buckets with no extended attributes,
+// even when Object Lock was enabled. This caused Veeam to think Object Lock wasn't supported.
+func TestVeeamObjectLockBugFix(t *testing.T) {
+
+ t.Run("Bug case: bucket with no extended attributes", func(t *testing.T) {
+ // This simulates the bug case where a bucket has no extended attributes at all
+ // The old code would immediately return NoSuchObjectLockConfiguration
+ // The new code correctly checks if Object Lock is enabled before returning an error
+
+ bucketConfig := &BucketConfig{
+ Name: "test-bucket",
+ Entry: &filer_pb.Entry{
+ Name: "test-bucket",
+ Extended: nil, // This is the key - no extended attributes
+ },
+ }
+
+ // Simulate the isObjectLockEnabledForBucket logic
+ enabled := false
+ if bucketConfig.Entry.Extended != nil {
+ if enabledBytes, exists := bucketConfig.Entry.Extended[s3_constants.ExtObjectLockEnabledKey]; exists {
+ enabled = string(enabledBytes) == s3_constants.ObjectLockEnabled || string(enabledBytes) == "true"
+ }
+ }
+
+ // Should correctly return false (not enabled) - this would trigger 404 correctly
+ assert.False(t, enabled, "Object Lock should not be enabled when no extended attributes exist")
+ })
+
+ t.Run("Fix verification: bucket with Object Lock enabled via boolean flag", func(t *testing.T) {
+ // This verifies the fix works when Object Lock is enabled via boolean flag
+
+ bucketConfig := &BucketConfig{
+ Name: "test-bucket",
+ Entry: &filer_pb.Entry{
+ Name: "test-bucket",
+ Extended: map[string][]byte{
+ s3_constants.ExtObjectLockEnabledKey: []byte("true"),
+ },
+ },
+ }
+
+ // Simulate the isObjectLockEnabledForBucket logic
+ enabled := false
+ if bucketConfig.Entry.Extended != nil {
+ if enabledBytes, exists := bucketConfig.Entry.Extended[s3_constants.ExtObjectLockEnabledKey]; exists {
+ enabled = string(enabledBytes) == s3_constants.ObjectLockEnabled || string(enabledBytes) == "true"
+ }
+ }
+
+ // Should correctly return true (enabled) - this would generate minimal XML response
+ assert.True(t, enabled, "Object Lock should be enabled when boolean flag is set")
+ })
+
+ t.Run("Fix verification: bucket with Object Lock enabled via Enabled constant", func(t *testing.T) {
+ // Test using the s3_constants.ObjectLockEnabled constant
+
+ bucketConfig := &BucketConfig{
+ Name: "test-bucket",
+ Entry: &filer_pb.Entry{
+ Name: "test-bucket",
+ Extended: map[string][]byte{
+ s3_constants.ExtObjectLockEnabledKey: []byte(s3_constants.ObjectLockEnabled),
+ },
+ },
+ }
+
+ // Simulate the isObjectLockEnabledForBucket logic
+ enabled := false
+ if bucketConfig.Entry.Extended != nil {
+ if enabledBytes, exists := bucketConfig.Entry.Extended[s3_constants.ExtObjectLockEnabledKey]; exists {
+ enabled = string(enabledBytes) == s3_constants.ObjectLockEnabled || string(enabledBytes) == "true"
+ }
+ }
+
+ // Should correctly return true (enabled)
+ assert.True(t, enabled, "Object Lock should be enabled when constant is used")
+ })
+}
diff --git a/weed/s3api/s3api_server.go b/weed/s3api/s3api_server.go
index 5d113c645..0d1ff98b3 100644
--- a/weed/s3api/s3api_server.go
+++ b/weed/s3api/s3api_server.go
@@ -88,7 +88,7 @@ func NewS3ApiServerWithStore(router *mux.Router, option *S3ApiServerOption, expl
filerGuard: security.NewGuard([]string{}, signingKey, expiresAfterSec, readSigningKey, readExpiresAfterSec),
cb: NewCircuitBreaker(option),
credentialManager: iam.credentialManager,
- bucketConfigCache: NewBucketConfigCache(5 * time.Minute),
+ bucketConfigCache: NewBucketConfigCache(60 * time.Minute), // Increased TTL since cache is now event-driven
}
if option.Config != "" {
@@ -286,8 +286,8 @@ func (s3a *S3ApiServer) registerRouter(router *mux.Router) {
bucket.Methods(http.MethodPut).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutBucketVersioningHandler, ACTION_WRITE)), "PUT")).Queries("versioning", "")
// GetObjectLockConfiguration / PutObjectLockConfiguration (bucket-level operations)
- bucket.Methods(http.MethodGet).Path("/").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetObjectLockConfigurationHandler, ACTION_READ)), "GET")).Queries("object-lock", "")
- bucket.Methods(http.MethodPut).Path("/").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectLockConfigurationHandler, ACTION_WRITE)), "PUT")).Queries("object-lock", "")
+ bucket.Methods(http.MethodGet).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetObjectLockConfigurationHandler, ACTION_READ)), "GET")).Queries("object-lock", "")
+ bucket.Methods(http.MethodPut).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectLockConfigurationHandler, ACTION_WRITE)), "PUT")).Queries("object-lock", "")
// GetBucketTagging
bucket.Methods(http.MethodGet).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketTaggingHandler, ACTION_TAGGING)), "GET")).Queries("tagging", "")