mirror of
https://github.com/pgsty/minio.git
synced 2026-10-18 07:26:10 +03:00
3dd53eb81f
Signed-off-by: Feng Ruohang <rh@vonng.com> (cherry picked from commit 1445fc1d85d5166de24c7a378cee3479cfc95a43) Signed-off-by: Feng Ruohang <rh@vonng.com>
182 lines
6.1 KiB
Go
182 lines
6.1 KiB
Go
// Copyright (c) 2026 Ruohang Feng
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type listingDirectoryWalkSnapshot struct {
|
|
StorageAPI
|
|
bucket string
|
|
snapshot []byte
|
|
}
|
|
|
|
func (d listingDirectoryWalkSnapshot) WalkDir(ctx context.Context, opts WalkDirOptions, out io.Writer) error {
|
|
if opts.Bucket != d.bucket {
|
|
return d.StorageAPI.WalkDir(ctx, opts, out)
|
|
}
|
|
_, err := out.Write(d.snapshot)
|
|
return err
|
|
}
|
|
|
|
// Capture two actual directory-walk generations for a directory object, then
|
|
// acknowledge its deletion. A different pool still contains dir/child. The
|
|
// stale source walkers disagree, while every current source drive is absent.
|
|
func TestListingRecoveryDeletedDirectoryKeepsCommonPrefix(t *testing.T) {
|
|
for _, marker := range []bool{false, true} {
|
|
for _, sourcePool := range []int{0, 1} {
|
|
t.Run(fmt.Sprintf("pool=%d/marker=%t", sourcePool, marker), func(t *testing.T) {
|
|
z, bucket, router := listingPoolsBackend(t)
|
|
ctx := t.Context()
|
|
source := z.serverPools[sourcePool].sets[0]
|
|
target := z.serverPools[1-sourcePool].sets[0]
|
|
childBody := []byte("surviving child")
|
|
put := func(set *erasureObjects, key string, tm time.Time) {
|
|
var body []byte
|
|
if key == "dir/child" {
|
|
body = childBody
|
|
}
|
|
_, err := set.PutObject(ctx, bucket, encodeDirObject(key), mustGetPutObjReader(t, bytes.NewReader(body), int64(len(body)), "", ""), ObjectOptions{MTime: tm, VersionSuspended: marker})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
tm := time.Now().UTC().Add(-time.Hour)
|
|
put(target, "dir/child", tm)
|
|
disks, switchDisks := installListingDiskSwitch(source)
|
|
snapshots := make([][]byte, len(disks))
|
|
for gen := range 3 {
|
|
if gen < 2 {
|
|
put(source, "dir/", tm.Add(time.Duration(gen)*time.Minute))
|
|
} else {
|
|
if _, err := source.DeleteObject(ctx, bucket, encodeDirObject("dir/"), ObjectOptions{VersionSuspended: marker, DeleteMarker: marker, MTime: tm.Add(2 * time.Minute)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for i, disk := range disks {
|
|
wanted := 0
|
|
if i >= 6 {
|
|
wanted = 1
|
|
}
|
|
if i >= 12 {
|
|
wanted = 2
|
|
}
|
|
if wanted != gen {
|
|
continue
|
|
}
|
|
var b bytes.Buffer
|
|
if err := disk.WalkDir(ctx, WalkDirOptions{Bucket: bucket, Recursive: false}, &b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
snapshots[i] = append([]byte(nil), b.Bytes()...)
|
|
}
|
|
}
|
|
for _, key := range []string{"dir/child", "dir/"} {
|
|
request, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucket, key), 0, nil, globalActiveCred.AccessKey, globalActiveCred.SecretKey, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request.WithContext(ctx))
|
|
t.Logf("GET %s HTTP %d", key, response.Code)
|
|
want := http.StatusOK
|
|
if key == "dir/" {
|
|
want = http.StatusNotFound
|
|
}
|
|
if response.Code != want {
|
|
t.Fatalf("GET %s: got %d want %d: %s", key, response.Code, want, response.Body.String())
|
|
}
|
|
if key == "dir/child" && !bytes.Equal(response.Body.Bytes(), childBody) {
|
|
t.Fatal("surviving child content changed")
|
|
}
|
|
}
|
|
wrapped := make([]StorageAPI, len(disks))
|
|
for i, disk := range disks {
|
|
wrapped[i] = listingDirectoryWalkSnapshot{disk, bucket, snapshots[i]}
|
|
}
|
|
switchDisks(wrapped)
|
|
t.Cleanup(func() { switchDisks(disks) })
|
|
req, err := newTestSignedRequestV4(http.MethodGet, getListObjectsV2URL("", bucket, "", "1000", "", "", "/"), 0, nil, globalActiveCred.AccessKey, globalActiveCred.SecretKey, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req.WithContext(ctx))
|
|
t.Logf("LIST HTTP %d body=%s", rec.Code, rec.Body.String())
|
|
var page ListObjectsV2Response
|
|
if rec.Code != http.StatusOK || xml.Unmarshal(rec.Body.Bytes(), &page) != nil {
|
|
t.Fatalf("LIST failed: %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if len(page.CommonPrefixes) != 1 || page.CommonPrefixes[0].Prefix != "dir/" {
|
|
t.Fatalf("acknowledged child lost its CommonPrefix: %+v", page)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// The directory must remain in the sorted input when an earlier object name
|
|
// disappears. Emitting it immediately would skip an intervening name, and
|
|
// keeping only one colliding directory would leave input order significant.
|
|
func TestListingRecoveryDirectoryCollisionOrdering(t *testing.T) {
|
|
for _, object := range []string{"dir", "dir/"} {
|
|
for _, present := range []bool{false, true} {
|
|
if present && object == "dir" {
|
|
continue
|
|
}
|
|
for source := range 3 {
|
|
t.Run(fmt.Sprintf("object=%s/present=%t/source=%d", object, present, source), func(t *testing.T) {
|
|
in := make([]chan metaCacheEntry, 6)
|
|
for i := range in {
|
|
in[i] = make(chan metaCacheEntry, 1)
|
|
entry := metaCacheEntry{name: "dir/"}
|
|
if i == source {
|
|
entry = metaCacheEntry{name: object, listingSet: &erasureObjects{}}
|
|
}
|
|
if i >= 3 {
|
|
entry = metaCacheEntry{name: []string{"dir-", "dir.ext", "dir0"}[i-3], metadata: []byte{1}}
|
|
}
|
|
in[i] <- entry
|
|
close(in[i])
|
|
}
|
|
out := make(chan metaCacheEntry, len(in))
|
|
err := mergeEntryChannelsWithResolver(t.Context(), in, out, 1, nil, func(group []*metaCacheEntry) (*metaCacheEntry, error) {
|
|
if len(group) != 1 || group[0].listingSet == nil {
|
|
t.Fatalf("directory entered the object group: %+v", group)
|
|
}
|
|
if !present {
|
|
return nil, nil
|
|
}
|
|
return &metaCacheEntry{name: object, metadata: []byte{1}}, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var names []string
|
|
for entry := range out {
|
|
names = append(names, entry.name)
|
|
if entry.name == "dir/" && entry.isObject() != present {
|
|
t.Fatalf("directory collision returned the wrong entry type: %+v", entry)
|
|
}
|
|
}
|
|
want := []string{"dir-", "dir.ext", "dir/", "dir0"}
|
|
if !slices.Equal(names, want) {
|
|
t.Fatalf("names=%v, want %v", names, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|