You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
182 lines
6.6 KiB
182 lines
6.6 KiB
package s3api
|
|
|
|
import (
|
|
"path"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// TestVersionedObjectListingPathConstruction tests that versioned objects are listed with correct paths.
|
|
// This is an integration test that validates the fix for GitHub discussion #7573.
|
|
//
|
|
// Issue: When using Velero with Kopia and S3 bucket versioning enabled, list operations were
|
|
// returning doubled paths like "kopia/logpaste/kopia/logpaste/file" instead of "kopia/logpaste/file".
|
|
// This caused Kopia to fail with "unable to load pack indexes despite 10 retries".
|
|
//
|
|
// Root cause: getLatestVersionEntryForListOperation was setting entry.Name to the full object path
|
|
// instead of just the base filename. When eachEntryFn combined dir + entry.Name, paths got doubled.
|
|
//
|
|
// Fix: Use path.Base(object) to extract only the filename portion.
|
|
func TestVersionedObjectListingPathConstruction(t *testing.T) {
|
|
// Test that getLatestVersionEntryForListOperation creates entries with correct Name field
|
|
t.Run("entry name should be base filename for versioned objects", func(t *testing.T) {
|
|
// Simulate the fix: when creating logical entries for versioned object listing,
|
|
// we use path.Base(object) instead of the full path
|
|
testCases := []struct {
|
|
name string
|
|
objectPath string
|
|
expectedName string
|
|
dir string
|
|
bucketPrefix string
|
|
expectedKey string
|
|
}{
|
|
{
|
|
name: "kopia file in nested directory",
|
|
objectPath: "kopia/logpaste/kopia.blobcfg",
|
|
expectedName: "kopia.blobcfg",
|
|
dir: "/buckets/velero/kopia/logpaste",
|
|
bucketPrefix: "/buckets/velero/",
|
|
expectedKey: "kopia/logpaste/kopia.blobcfg",
|
|
},
|
|
{
|
|
name: "kopia repository file",
|
|
objectPath: "kopia/logpaste/kopia.repository",
|
|
expectedName: "kopia.repository",
|
|
dir: "/buckets/velero/kopia/logpaste",
|
|
bucketPrefix: "/buckets/velero/",
|
|
expectedKey: "kopia/logpaste/kopia.repository",
|
|
},
|
|
{
|
|
name: "backup file in nested directory",
|
|
objectPath: "backups/test1/velero-backup.json",
|
|
expectedName: "velero-backup.json",
|
|
dir: "/buckets/velero/backups/test1",
|
|
bucketPrefix: "/buckets/velero/",
|
|
expectedKey: "backups/test1/velero-backup.json",
|
|
},
|
|
{
|
|
name: "file at bucket root",
|
|
objectPath: "file.txt",
|
|
expectedName: "file.txt",
|
|
dir: "/buckets/velero",
|
|
bucketPrefix: "/buckets/velero/",
|
|
expectedKey: "file.txt",
|
|
},
|
|
{
|
|
name: "deeply nested file",
|
|
objectPath: "a/b/c/d/e/file.json",
|
|
expectedName: "file.json",
|
|
dir: "/buckets/velero/a/b/c/d/e",
|
|
bucketPrefix: "/buckets/velero/",
|
|
expectedKey: "a/b/c/d/e/file.json",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Simulate what the fix does: use path.Base to get just the filename
|
|
entryName := path.Base(tc.objectPath)
|
|
assert.Equal(t, tc.expectedName, entryName, "entry.Name should be base filename only")
|
|
|
|
// Verify that combining dir + entry.Name produces correct key
|
|
// This is what eachEntryFn does in the list operation
|
|
key := (tc.dir + "/" + entryName)[len(tc.bucketPrefix):]
|
|
assert.Equal(t, tc.expectedKey, key, "final key should not have doubled paths")
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("verify path doubling bug is fixed", func(t *testing.T) {
|
|
// This test demonstrates the exact bug that was fixed
|
|
objectPath := "kopia/logpaste/kopia.blobcfg"
|
|
dir := "/buckets/velero/kopia/logpaste"
|
|
bucketPrefix := "/buckets/velero/"
|
|
|
|
// WRONG behavior (before fix): entry.Name = full path
|
|
wrongEntryName := objectPath
|
|
wrongKey := (dir + "/" + wrongEntryName)[len(bucketPrefix):]
|
|
assert.Equal(t, "kopia/logpaste/kopia/logpaste/kopia.blobcfg", wrongKey,
|
|
"bug: using full path as entry.Name causes path doubling")
|
|
|
|
// CORRECT behavior (after fix): entry.Name = base filename only
|
|
correctEntryName := path.Base(objectPath)
|
|
correctKey := (dir + "/" + correctEntryName)[len(bucketPrefix):]
|
|
assert.Equal(t, "kopia/logpaste/kopia.blobcfg", correctKey,
|
|
"fix: using path.Base for entry.Name produces correct path")
|
|
})
|
|
}
|
|
|
|
// TestVersionedEntryCreation tests that versioned entries are created correctly
|
|
// with all necessary metadata for list operations.
|
|
func TestVersionedEntryCreation(t *testing.T) {
|
|
t.Run("logical entry should have correct attributes", func(t *testing.T) {
|
|
// Simulate creating a logical entry for a versioned object
|
|
objectPath := "kopia/logpaste/kopia.repository"
|
|
|
|
// Create a mock versioned entry (as would be returned from .versions directory)
|
|
mockVersionedEntry := &filer_pb.Entry{
|
|
Name: "v_1880a8e61c70fd480b47df7f530b796b", // Version file name
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Mtime: 1234567890,
|
|
FileSize: 1024,
|
|
},
|
|
Extended: map[string][]byte{
|
|
s3_constants.ExtVersionIdKey: []byte("1880a8e61c70fd480b47df7f530b796b"),
|
|
s3_constants.ExtETagKey: []byte("\"abc123\""),
|
|
},
|
|
}
|
|
|
|
// Simulate what getLatestVersionEntryForListOperation does with the fix
|
|
logicalEntry := &filer_pb.Entry{
|
|
Name: path.Base(objectPath), // This is the fix!
|
|
IsDirectory: false,
|
|
Attributes: mockVersionedEntry.Attributes,
|
|
Extended: mockVersionedEntry.Extended,
|
|
Chunks: mockVersionedEntry.Chunks,
|
|
}
|
|
|
|
// Verify the logical entry has the correct name
|
|
assert.Equal(t, "kopia.repository", logicalEntry.Name,
|
|
"logical entry should have base filename as Name")
|
|
assert.False(t, logicalEntry.IsDirectory,
|
|
"logical entry should not be a directory")
|
|
assert.NotNil(t, logicalEntry.Extended,
|
|
"logical entry should preserve extended attributes")
|
|
})
|
|
}
|
|
|
|
// TestVersionsFolderPathHandling tests that .versions folder paths are handled correctly
|
|
func TestVersionsFolderPathHandling(t *testing.T) {
|
|
t.Run("versions folder suffix handling", func(t *testing.T) {
|
|
testCases := []struct {
|
|
versionsDir string
|
|
expectedObject string
|
|
}{
|
|
{
|
|
versionsDir: "kopia.blobcfg.versions",
|
|
expectedObject: "kopia.blobcfg",
|
|
},
|
|
{
|
|
versionsDir: "kopia.repository.versions",
|
|
expectedObject: "kopia.repository",
|
|
},
|
|
{
|
|
versionsDir: "file.txt.versions",
|
|
expectedObject: "file.txt",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.versionsDir, func(t *testing.T) {
|
|
// This is what the list handler does to extract object name
|
|
baseObjectName := tc.versionsDir[:len(tc.versionsDir)-len(s3_constants.VersionsFolder)]
|
|
assert.Equal(t, tc.expectedObject, baseObjectName,
|
|
"object name should be correctly extracted from versions directory")
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|