fix listing object versions (#7006)
* fix listing object versions * Update s3api_object_versioning.go * Update s3_directory_versioning_test.go * check previous skipped tests * fix test_versioning_stack_delete_merkers * address test_bucket_list_return_data_versioning * Update s3_directory_versioning_test.go * fix test_versioning_concurrent_multi_object_delete * fix test_versioning_obj_suspend_versions test * fix empty owner * fix listing versioned objects * default owner * fix path
This commit is contained in:
@@ -4,16 +4,17 @@ import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"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"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"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"
|
||||
)
|
||||
|
||||
type OptionalString struct {
|
||||
@@ -356,6 +357,9 @@ func (s3a *S3ApiServer) doListFilerEntries(client filer_pb.SeaweedFilerClient, d
|
||||
return
|
||||
}
|
||||
|
||||
// Track .versions directories found in this directory for later processing
|
||||
var versionsDirs []string
|
||||
|
||||
for {
|
||||
resp, recvErr := stream.Recv()
|
||||
if recvErr != nil {
|
||||
@@ -386,6 +390,14 @@ func (s3a *S3ApiServer) doListFilerEntries(client filer_pb.SeaweedFilerClient, d
|
||||
if entry.Name == s3_constants.MultipartUploadsFolder { // FIXME no need to apply to all directories. this extra also affects maxKeys
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip .versions directories in regular list operations but track them for logical object creation
|
||||
if strings.HasSuffix(entry.Name, ".versions") {
|
||||
glog.V(4).Infof("Found .versions directory: %s", entry.Name)
|
||||
versionsDirs = append(versionsDirs, entry.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if delimiter != "/" || cursor.prefixEndsOnDelimiter {
|
||||
if cursor.prefixEndsOnDelimiter {
|
||||
cursor.prefixEndsOnDelimiter = false
|
||||
@@ -425,6 +437,48 @@ func (s3a *S3ApiServer) doListFilerEntries(client filer_pb.SeaweedFilerClient, d
|
||||
cursor.prefixEndsOnDelimiter = false
|
||||
}
|
||||
}
|
||||
|
||||
// After processing all regular entries, handle versioned objects
|
||||
// Create logical entries for objects that have .versions directories
|
||||
for _, versionsDir := range versionsDirs {
|
||||
if cursor.maxKeys <= 0 {
|
||||
cursor.isTruncated = true
|
||||
break
|
||||
}
|
||||
|
||||
// Extract object name from .versions directory name (remove .versions suffix)
|
||||
baseObjectName := strings.TrimSuffix(versionsDir, ".versions")
|
||||
|
||||
// Construct full object path relative to bucket
|
||||
// dir is something like "/buckets/sea-test-1/Veeam/Backup/vbr/Config"
|
||||
// we need to get the path relative to bucket: "Veeam/Backup/vbr/Config/Owner"
|
||||
bucketPath := strings.TrimPrefix(dir, s3a.option.BucketsPath+"/")
|
||||
bucketName := strings.Split(bucketPath, "/")[0]
|
||||
|
||||
// Remove bucket name from path to get directory within bucket
|
||||
bucketRelativePath := strings.Join(strings.Split(bucketPath, "/")[1:], "/")
|
||||
|
||||
var fullObjectPath string
|
||||
if bucketRelativePath == "" {
|
||||
// Object is at bucket root
|
||||
fullObjectPath = baseObjectName
|
||||
} else {
|
||||
// Object is in subdirectory
|
||||
fullObjectPath = bucketRelativePath + "/" + baseObjectName
|
||||
}
|
||||
|
||||
glog.V(4).Infof("Processing versioned object: baseObjectName=%s, bucketRelativePath=%s, fullObjectPath=%s",
|
||||
baseObjectName, bucketRelativePath, fullObjectPath)
|
||||
|
||||
// Get the latest version information for this object
|
||||
if latestVersionEntry, latestVersionErr := s3a.getLatestVersionEntryForListOperation(bucketName, fullObjectPath); latestVersionErr == nil {
|
||||
glog.V(4).Infof("Creating logical entry for versioned object: %s", fullObjectPath)
|
||||
eachEntryFn(dir, latestVersionEntry)
|
||||
} else {
|
||||
glog.V(4).Infof("Failed to get latest version for %s: %v", fullObjectPath, latestVersionErr)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -513,3 +567,32 @@ func (s3a *S3ApiServer) ensureDirectoryAllEmpty(filerClient filer_pb.SeaweedFile
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// getLatestVersionEntryForListOperation gets the latest version of an object and creates a logical entry for list operations
|
||||
// This is used to show versioned objects as logical object names in regular list operations
|
||||
func (s3a *S3ApiServer) getLatestVersionEntryForListOperation(bucket, object string) (*filer_pb.Entry, error) {
|
||||
// Get the latest version entry
|
||||
latestVersionEntry, err := s3a.getLatestObjectVersion(bucket, object)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get latest version: %w", err)
|
||||
}
|
||||
|
||||
// Check if this is a delete marker (should not be shown in regular list)
|
||||
if latestVersionEntry.Extended != nil {
|
||||
if deleteMarker, exists := latestVersionEntry.Extended[s3_constants.ExtDeleteMarkerKey]; exists && string(deleteMarker) == "true" {
|
||||
return nil, fmt.Errorf("latest version is a delete marker")
|
||||
}
|
||||
}
|
||||
|
||||
// Create a logical entry that appears to be stored at the object path (not the versioned path)
|
||||
// This allows the list operation to show the logical object name while preserving all metadata
|
||||
logicalEntry := &filer_pb.Entry{
|
||||
Name: strings.TrimPrefix(object, "/"),
|
||||
IsDirectory: false,
|
||||
Attributes: latestVersionEntry.Attributes,
|
||||
Extended: latestVersionEntry.Extended,
|
||||
Chunks: latestVersionEntry.Chunks,
|
||||
}
|
||||
|
||||
return logicalEntry, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user