1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
|
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")
})
}
})
}
|