mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2025-09-23 04:13:35 +08:00
Fix get object lock configuration handler (#6996)
* fix GetObjectLockConfigurationHandler * cache and use bucket object lock config * subscribe to bucket configuration changes * increase bucket config cache TTL * refactor * Update weed/s3api/s3api_server.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * avoid duplidated work * rename variable * Update s3api_object_handlers_put.go * fix routing * admin ui and api handler are consistent now * use fields instead of xml * fix test * address comments * Update weed/s3api/s3api_object_handlers_put.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test/s3/retention/s3_retention_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update weed/s3api/object_lock_utils.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * change error style * errorf --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
}
|
||||
|
232
weed/s3api/object_lock_utils.go
Normal file
232
weed/s3api/object_lock_utils.go
Normal file
@@ -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
|
||||
}
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
})
|
||||
|
139
weed/s3api/s3api_bucket_handlers_object_lock_config.go
Normal file
139
weed/s3api/s3api_bucket_handlers_object_lock_config.go
Normal file
@@ -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)
|
||||
}
|
126
weed/s3api/s3api_object_handlers_legal_hold.go
Normal file
126
weed/s3api/s3api_object_handlers_legal_hold.go
Normal file
@@ -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)
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
90
weed/s3api/s3api_object_lock_fix_test.go
Normal file
90
weed/s3api/s3api_object_lock_fix_test.go
Normal file
@@ -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")
|
||||
})
|
||||
}
|
@@ -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", "")
|
||||
|
Reference in New Issue
Block a user