@ -82,9 +82,14 @@ func clearCachedListMetadata(extended map[string][]byte) {
clearCachedVersionMetadata ( extended )
clearCachedVersionMetadata ( extended )
}
}
// S3ListObjectVersionsResult - Custom struct for S3 list-object-versions response
// This avoids conflicts with the XSD generated ListVersionsResult struct
// and ensures proper separation of versions and delete markers into arrays
// S3ListObjectVersionsResult - Custom struct for S3 list-object-versions response.
// This avoids conflicts with the XSD generated ListVersionsResult struct.
//
// The Entries slice holds Version, DeleteMarker, and CommonPrefix items in their
// correct interleaved sort order (by key ascending, then newest version first).
// Each entry uses a per-element MarshalXML to output the correct XML element name.
// This ensures the XML output matches the S3 API contract where these elements
// are interleaved in sort order, not grouped by type.
type S3ListObjectVersionsResult struct {
type S3ListObjectVersionsResult struct {
XMLName xml . Name ` xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListVersionsResult" `
XMLName xml . Name ` xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListVersionsResult" `
@ -98,29 +103,37 @@ type S3ListObjectVersionsResult struct {
Delimiter string ` xml:"Delimiter,omitempty" `
Delimiter string ` xml:"Delimiter,omitempty" `
IsTruncated bool ` xml:"IsTruncated" `
IsTruncated bool ` xml:"IsTruncated" `
// These are the critical fields - arrays instead of single elements
Versions [ ] VersionEntry ` xml:"Version,omitempty" ` // Array for versions
DeleteMarkers [ ] DeleteMarkerEntry ` xml:"DeleteMarker,omitempty" ` // Array for delete markers
// Entries holds all versions, delete markers, and common prefixes in their
// correct interleaved sort order. Each entry's MarshalXML outputs the correct
// XML element name (<Version>, <DeleteMarker>, or <CommonPrefixes>).
Entries [ ] VersionListEntry ` xml:"Version" `
EncodingType string ` xml:"EncodingType,omitempty" `
}
CommonPrefixes [ ] PrefixEntry ` xml:"CommonPrefixes,omitempty" `
EncodingType string ` xml:"EncodingType,omitempty" `
// VersionListEntry represents a single item in the ListObjectVersions response.
// It wraps either a VersionEntry, DeleteMarkerEntry, or PrefixEntry and outputs
// the correct XML element name via custom MarshalXML.
type VersionListEntry struct {
Version * VersionEntry
DeleteMarker * DeleteMarkerEntry
Prefix * PrefixEntry
}
}
// Original struct - keeping for compatibility but will use S3ListObjectVersionsResult for XML response
type ListObjectVersionsResult struct {
XMLName xml . Name ` xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListVersionsResult" `
Name string ` xml:"Name" `
Prefix string ` xml:"Prefix" `
KeyMarker string ` xml:"KeyMarker,omitempty" `
VersionIdMarker string ` xml:"VersionIdMarker,omitempty" `
NextKeyMarker string ` xml:"NextKeyMarker,omitempty" `
NextVersionIdMarker string ` xml:"NextVersionIdMarker,omitempty" `
MaxKeys int ` xml:"MaxKeys" `
Delimiter string ` xml:"Delimiter,omitempty" `
IsTruncated bool ` xml:"IsTruncated" `
Versions [ ] VersionEntry ` xml:"Version,omitempty" `
DeleteMarkers [ ] DeleteMarkerEntry ` xml:"DeleteMarker,omitempty" `
CommonPrefixes [ ] PrefixEntry ` xml:"CommonPrefixes,omitempty" `
// MarshalXML outputs the entry as <Version>, <DeleteMarker>, or <CommonPrefixes>
// depending on which field is populated. This ensures elements are interleaved in
// their correct sort order in the XML response.
func ( e VersionListEntry ) MarshalXML ( enc * xml . Encoder , start xml . StartElement ) error {
if e . DeleteMarker != nil {
start . Name . Local = "DeleteMarker"
return enc . EncodeElement ( e . DeleteMarker , start )
}
if e . Prefix != nil {
start . Name . Local = "CommonPrefixes"
return enc . EncodeElement ( e . Prefix , start )
}
start . Name . Local = "Version"
return enc . EncodeElement ( e . Version , start )
}
}
// ObjectVersion represents a version of an S3 object
// ObjectVersion represents a version of an S3 object
@ -260,7 +273,7 @@ func (s3a *S3ApiServer) listObjectVersions(bucket, prefix, keyMarker, versionIdM
// Build the final response by splitting items back into their respective fields
// Build the final response by splitting items back into their respective fields
result := s3a . splitIntoResult ( truncatedList , bucket , prefix , keyMarker , versionIdMarker , delimiter , maxKeys , isTruncated , nextKeyMarker , nextVersionIdMarker )
result := s3a . splitIntoResult ( truncatedList , bucket , prefix , keyMarker , versionIdMarker , delimiter , maxKeys , isTruncated , nextKeyMarker , nextVersionIdMarker )
glog . V ( 1 ) . Infof ( "listObjectVersions: final result - %d v ersio ns, %d dele te ma rkers, %d common pref ix es" , len ( result . Versions ) , len ( result . DeleteMarkers ) , len ( result . CommonPrefix es) )
glog . V ( 1 ) . Infof ( "listObjectVersions: final result - %d entries" , len ( result . Entri es) )
return result , nil
return result , nil
}
}
@ -329,7 +342,8 @@ func (s3a *S3ApiServer) truncateAndSetMarkers(combinedList []versionListItem, ma
return combinedList , nextKeyMarker , nextVersionIdMarker , isTruncated
return combinedList , nextKeyMarker , nextVersionIdMarker , isTruncated
}
}
// splitIntoResult builds the final S3ListObjectVersionsResult from the combined list
// splitIntoResult builds the final S3ListObjectVersionsResult from the combined list.
// It populates a single Entries slice that preserves the interleaved sort order.
func ( s3a * S3ApiServer ) splitIntoResult ( combinedList [ ] versionListItem , bucket , prefix , keyMarker , versionIdMarker , delimiter string , maxKeys int , isTruncated bool , nextKeyMarker , nextVersionIdMarker string ) * S3ListObjectVersionsResult {
func ( s3a * S3ApiServer ) splitIntoResult ( combinedList [ ] versionListItem , bucket , prefix , keyMarker , versionIdMarker , delimiter string , maxKeys int , isTruncated bool , nextKeyMarker , nextVersionIdMarker string ) * S3ListObjectVersionsResult {
result := & S3ListObjectVersionsResult {
result := & S3ListObjectVersionsResult {
Name : bucket ,
Name : bucket ,
@ -341,20 +355,20 @@ func (s3a *S3ApiServer) splitIntoResult(combinedList []versionListItem, bucket,
IsTruncated : isTruncated ,
IsTruncated : isTruncated ,
NextKeyMarker : nextKeyMarker ,
NextKeyMarker : nextKeyMarker ,
NextVersionIdMarker : nextVersionIdMarker ,
NextVersionIdMarker : nextVersionIdMarker ,
Versions : make ( [ ] VersionEntry , 0 ) ,
DeleteMarkers : make ( [ ] DeleteMarkerEntry , 0 ) ,
CommonPrefixes : make ( [ ] PrefixEntry , 0 ) ,
Entries : make ( [ ] VersionListEntry , 0 , len ( combinedList ) ) ,
}
}
for _ , item := range combinedList {
for _ , item := range combinedList {
if item . isPrefix {
if item . isPrefix {
result . CommonPrefixes = append ( result . CommonPrefixes , PrefixEntry { Prefix : item . key } )
result . Entries = append ( result . Entries , VersionListEntry {
Prefix : & PrefixEntry { Prefix : item . key } ,
} )
} else {
} else {
switch v := item . versionData . ( type ) {
switch v := item . versionData . ( type ) {
case * VersionEntry :
case * VersionEntry :
result . Version s = append ( result . Versions , * v )
result . Entrie s = append ( result . Entries , VersionListEntry { Version : v } )
case * DeleteMarkerEntry :
case * DeleteMarkerEntry :
result . DeleteMarker s = append ( result . DeleteMarkers , * v )
result . Entrie s = append ( result . Entries , VersionListEntry { DeleteMarker : v } )
}
}
}
}
}
}