diff --git a/weed/s3api/s3_constants/header.go b/weed/s3api/s3_constants/header.go
index e4c0ad77b..1ef6f62c5 100644
--- a/weed/s3api/s3_constants/header.go
+++ b/weed/s3api/s3_constants/header.go
@@ -23,6 +23,11 @@ import (
"github.com/gorilla/mux"
)
+// S3 XML namespace
+const (
+ S3Namespace = "http://s3.amazonaws.com/doc/2006-03-01/"
+)
+
// Standard S3 HTTP request constants
const (
// S3 storage class
diff --git a/weed/s3api/s3api_bucket_handlers_object_lock_config.go b/weed/s3api/s3api_bucket_handlers_object_lock_config.go
index c779f80d7..23b52648e 100644
--- a/weed/s3api/s3api_bucket_handlers_object_lock_config.go
+++ b/weed/s3api/s3api_bucket_handlers_object_lock_config.go
@@ -86,6 +86,9 @@ func (s3a *S3ApiServer) GetObjectLockConfigurationHandler(w http.ResponseWriter,
// Check if we have cached Object Lock configuration
if bucketConfig.ObjectLockConfig != nil {
+ // Set namespace for S3 compatibility
+ bucketConfig.ObjectLockConfig.XMLNS = s3_constants.S3Namespace
+
// Use cached configuration and marshal it to XML for response
marshaledXML, err := xml.Marshal(bucketConfig.ObjectLockConfig)
if err != nil {
@@ -139,6 +142,9 @@ func (s3a *S3ApiServer) GetObjectLockConfigurationHandler(w http.ResponseWriter,
// not just ObjectLockConfig, before resetting the TTL
s3a.updateBucketConfigCacheFromEntry(freshEntry)
+ // Set namespace for S3 compatibility
+ objectLockConfig.XMLNS = s3_constants.S3Namespace
+
// Marshal and return the configuration
marshaledXML, err := xml.Marshal(objectLockConfig)
if err != nil {
diff --git a/weed/s3api/s3api_object_retention.go b/weed/s3api/s3api_object_retention.go
index 5bb2faf54..ef298eb43 100644
--- a/weed/s3api/s3api_object_retention.go
+++ b/weed/s3api/s3api_object_retention.go
@@ -57,37 +57,40 @@ const (
// ObjectRetention represents S3 Object Retention configuration
type ObjectRetention struct {
- XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Retention"`
- Mode string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Mode,omitempty"`
- RetainUntilDate *time.Time `xml:"http://s3.amazonaws.com/doc/2006-03-01/ RetainUntilDate,omitempty"`
+ XMLNS string `xml:"xmlns,attr,omitempty"`
+ XMLName xml.Name `xml:"Retention"`
+ Mode string `xml:"Mode,omitempty"`
+ RetainUntilDate *time.Time `xml:"RetainUntilDate,omitempty"`
}
// ObjectLegalHold represents S3 Object Legal Hold configuration
type ObjectLegalHold struct {
- XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ LegalHold"`
- Status string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Status,omitempty"`
+ XMLNS string `xml:"xmlns,attr,omitempty"`
+ XMLName xml.Name `xml:"LegalHold"`
+ Status string `xml:"Status,omitempty"`
}
// ObjectLockConfiguration represents S3 Object Lock Configuration
type ObjectLockConfiguration struct {
- XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ObjectLockConfiguration"`
- ObjectLockEnabled string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ObjectLockEnabled,omitempty"`
- Rule *ObjectLockRule `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Rule,omitempty"`
+ XMLNS string `xml:"xmlns,attr,omitempty"`
+ XMLName xml.Name `xml:"ObjectLockConfiguration"`
+ ObjectLockEnabled string `xml:"ObjectLockEnabled,omitempty"`
+ Rule *ObjectLockRule `xml:"Rule,omitempty"`
}
// ObjectLockRule represents an Object Lock Rule
type ObjectLockRule struct {
- XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Rule"`
- DefaultRetention *DefaultRetention `xml:"http://s3.amazonaws.com/doc/2006-03-01/ DefaultRetention,omitempty"`
+ XMLName xml.Name `xml:"Rule"`
+ DefaultRetention *DefaultRetention `xml:"DefaultRetention,omitempty"`
}
// DefaultRetention represents default retention settings
// Implements custom XML unmarshal to track if Days/Years were present in XML
type DefaultRetention struct {
- XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ DefaultRetention"`
- Mode string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Mode,omitempty"`
- Days int `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Days,omitempty"`
- Years int `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Years,omitempty"`
+ XMLName xml.Name `xml:"DefaultRetention"`
+ Mode string `xml:"Mode,omitempty"`
+ Days int `xml:"Days,omitempty"`
+ Years int `xml:"Years,omitempty"`
DaysSet bool `xml:"-"`
YearsSet bool `xml:"-"`
}
@@ -102,8 +105,8 @@ func (dr *DefaultRetention) UnmarshalXML(d *xml.Decoder, start xml.StartElement)
type Alias DefaultRetention
aux := &struct {
*Alias
- Days *int `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Days,omitempty"`
- Years *int `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Years,omitempty"`
+ Days *int `xml:"Days,omitempty"`
+ Years *int `xml:"Years,omitempty"`
}{Alias: (*Alias)(dr)}
if err := d.DecodeElement(aux, &start); err != nil {
glog.V(2).Infof("DefaultRetention.UnmarshalXML: decode error: %v", err)
@@ -245,6 +248,8 @@ func (s3a *S3ApiServer) getObjectRetention(bucket, object, versionId string) (*O
return nil, ErrNoRetentionConfiguration
}
+ // Set namespace for S3 compatibility
+ retention.XMLNS = s3_constants.S3Namespace
return retention, nil
}
@@ -386,6 +391,8 @@ func (s3a *S3ApiServer) getObjectLegalHold(bucket, object, versionId string) (*O
return nil, ErrNoLegalHoldConfiguration
}
+ // Set namespace for S3 compatibility
+ legalHold.XMLNS = s3_constants.S3Namespace
return legalHold, nil
}
diff --git a/weed/s3api/s3api_object_retention_test.go b/weed/s3api/s3api_object_retention_test.go
index 20ccf60d9..34c772acd 100644
--- a/weed/s3api/s3api_object_retention_test.go
+++ b/weed/s3api/s3api_object_retention_test.go
@@ -201,6 +201,30 @@ func TestParseObjectRetention(t *testing.T) {
RetainUntilDate: timePtr(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
},
},
+ {
+ name: "Valid retention XML without namespace (Veeam compatibility)",
+ xmlBody: `
+ GOVERNANCE
+ 2024-12-31T23:59:59Z
+ `,
+ expectError: false,
+ expectedResult: &ObjectRetention{
+ Mode: "GOVERNANCE",
+ RetainUntilDate: timePtr(time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC)),
+ },
+ },
+ {
+ name: "Valid compliance retention XML without namespace (Veeam compatibility)",
+ xmlBody: `
+ COMPLIANCE
+ 2025-01-01T00:00:00Z
+ `,
+ expectError: false,
+ expectedResult: &ObjectRetention{
+ Mode: "COMPLIANCE",
+ RetainUntilDate: timePtr(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
+ },
+ },
{
name: "Empty XML body",
xmlBody: "",
@@ -311,6 +335,26 @@ func TestParseObjectLegalHold(t *testing.T) {
Status: "OFF",
},
},
+ {
+ name: "Valid legal hold ON without namespace",
+ xmlBody: `
+ ON
+ `,
+ expectError: false,
+ expectedResult: &ObjectLegalHold{
+ Status: "ON",
+ },
+ },
+ {
+ name: "Valid legal hold OFF without namespace",
+ xmlBody: `
+ OFF
+ `,
+ expectError: false,
+ expectedResult: &ObjectLegalHold{
+ Status: "OFF",
+ },
+ },
{
name: "Empty XML body",
xmlBody: "",
@@ -405,6 +449,38 @@ func TestParseObjectLockConfiguration(t *testing.T) {
},
},
},
+ {
+ name: "Valid object lock configuration without namespace",
+ xmlBody: `
+ Enabled
+ `,
+ expectError: false,
+ expectedResult: &ObjectLockConfiguration{
+ ObjectLockEnabled: "Enabled",
+ },
+ },
+ {
+ name: "Valid object lock configuration with rule without namespace",
+ xmlBody: `
+ Enabled
+
+
+ GOVERNANCE
+ 30
+
+
+ `,
+ expectError: false,
+ expectedResult: &ObjectLockConfiguration{
+ ObjectLockEnabled: "Enabled",
+ Rule: &ObjectLockRule{
+ DefaultRetention: &DefaultRetention{
+ Mode: "GOVERNANCE",
+ Days: 30,
+ },
+ },
+ },
+ },
{
name: "Empty XML body",
xmlBody: "",