优化结构
This commit is contained in:
@@ -69,6 +69,10 @@ func (s *aliyunStorage) Compose(ctx context.Context, names []string, destination
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *aliyunStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
prefix, err := normalizeDeletePrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := ""
|
||||
for {
|
||||
items, next, more, err := s.List(ctx, prefix, cursor, 1000)
|
||||
@@ -83,7 +87,10 @@ func (s *aliyunStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
if !more {
|
||||
return nil
|
||||
}
|
||||
cursor = next
|
||||
cursor, err = advanceDeletePrefixCursor(cursor, next, more)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
func (s *aliyunStorage) List(_ context.Context, prefix, cursor string, limit int) ([]*system.StoredFile, string, bool, error) {
|
||||
|
||||
@@ -106,8 +106,13 @@ func (s *awsStorage) Compose(ctx context.Context, names []string, destination st
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *awsStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
prefix, err := normalizeDeletePrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := ""
|
||||
for {
|
||||
items, _, more, err := s.List(ctx, prefix, "", 1000)
|
||||
items, next, more, err := s.List(ctx, prefix, cursor, 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -116,9 +121,13 @@ func (s *awsStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !more || len(items) == 0 {
|
||||
if !more {
|
||||
return nil
|
||||
}
|
||||
cursor, err = advanceDeletePrefixCursor(cursor, next, more)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
func (s *awsStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*system.StoredFile, string, bool, error) {
|
||||
|
||||
@@ -43,6 +43,14 @@ func composeStreams(
|
||||
errCh <- nil
|
||||
}()
|
||||
putErr := put(ctx, destination, reader)
|
||||
// A failed destination may stop reading before the producer reaches EOF.
|
||||
// Close the pipe reader in that case so the producer's next write returns
|
||||
// instead of leaving the goroutine blocked forever.
|
||||
if putErr != nil {
|
||||
_ = reader.CloseWithError(putErr)
|
||||
} else {
|
||||
_ = reader.Close()
|
||||
}
|
||||
composeErr := <-errCh
|
||||
if putErr != nil {
|
||||
_ = remove(ctx, destination)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestComposeStreams(t *testing.T) {
|
||||
@@ -48,3 +49,62 @@ func TestComposeStreamsRemovesPartialDestination(t *testing.T) {
|
||||
t.Fatalf("compose error = %v, removed = %v", err, removed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeStreamsUnblocksProducerWhenDestinationFails(t *testing.T) {
|
||||
putErr := errors.New("destination failed")
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := composeStreams(context.Background(), []string{"large"}, func(context.Context, string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader(strings.Repeat("x", 1<<20))), nil
|
||||
}, func(context.Context, string, io.Reader) error {
|
||||
return putErr
|
||||
}, func(context.Context, string) error { return nil }, "out")
|
||||
done <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if !errors.Is(err, putErr) {
|
||||
t.Fatalf("compose error = %v, want %v", err, putErr)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("compose remained blocked after destination failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeDeletePrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
valid string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "canonicalizes", value: "/uploads/chunks/", valid: "uploads/chunks"},
|
||||
{name: "cleans duplicate separators", value: "uploads//chunks", valid: "uploads/chunks"},
|
||||
{name: "rejects empty", value: " / ", wantErr: true},
|
||||
{name: "rejects traversal", value: "uploads/../", wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := normalizeDeletePrefix(tt.value)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("normalizeDeletePrefix(%q) succeeded with %q", tt.value, got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil || got != tt.valid {
|
||||
t.Fatalf("normalizeDeletePrefix(%q) = %q, %v; want %q", tt.value, got, err, tt.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdvanceDeletePrefixCursor(t *testing.T) {
|
||||
if _, err := advanceDeletePrefixCursor("cursor", "cursor", true); err == nil {
|
||||
t.Fatal("same cursor should fail")
|
||||
}
|
||||
if next, err := advanceDeletePrefixCursor("", "next", true); err != nil || next != "next" {
|
||||
t.Fatalf("advance cursor = %q, %v", next, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,10 @@ func (s *huaweiStorage) Compose(ctx context.Context, names []string, destination
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *huaweiStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
prefix, err := normalizeDeletePrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := ""
|
||||
for {
|
||||
items, next, more, err := s.List(ctx, prefix, cursor, 1000)
|
||||
@@ -82,7 +86,10 @@ func (s *huaweiStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
if !more {
|
||||
return nil
|
||||
}
|
||||
cursor = next
|
||||
cursor, err = advanceDeletePrefixCursor(cursor, next, more)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
func (s *huaweiStorage) List(_ context.Context, prefix, cursor string, limit int) ([]*system.StoredFile, string, bool, error) {
|
||||
|
||||
@@ -138,7 +138,11 @@ func (s *fileStorage) Compose(ctx context.Context, names []string, destination s
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *fileStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
path, err := s.resolve(strings.TrimSuffix(prefix, "/") + "/placeholder")
|
||||
prefix, err := normalizeDeletePrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, err := s.resolve(prefix + "/placeholder")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLocalDeletePrefixRejectsEmptyPrefix(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
sentinel := filepath.Join(root, "keep.txt")
|
||||
if err := os.WriteFile(sentinel, []byte("keep"), 0o600); err != nil {
|
||||
t.Fatalf("write sentinel: %v", err)
|
||||
}
|
||||
storage := &fileStorage{root: root, urlPrefix: "/files"}
|
||||
|
||||
if err := storage.DeletePrefix(context.Background(), " /"); err == nil {
|
||||
t.Fatal("DeletePrefix() accepted an empty prefix")
|
||||
}
|
||||
if _, err := os.Stat(sentinel); err != nil {
|
||||
t.Fatalf("sentinel was removed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// normalizeDeletePrefix rejects requests that could accidentally target the
|
||||
// storage root and returns the canonical key form used by all backends.
|
||||
func normalizeDeletePrefix(prefix string) (string, error) {
|
||||
prefix = strings.TrimSpace(strings.ReplaceAll(prefix, "\\", "/"))
|
||||
prefix = strings.Trim(prefix, "/")
|
||||
if prefix == "" {
|
||||
return "", errors.New("storage delete prefix is required")
|
||||
}
|
||||
for _, part := range strings.Split(prefix, "/") {
|
||||
if part == ".." {
|
||||
return "", errors.New("invalid storage delete prefix")
|
||||
}
|
||||
}
|
||||
clean := path.Clean(prefix)
|
||||
if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") {
|
||||
return "", errors.New("invalid storage delete prefix")
|
||||
}
|
||||
return clean, nil
|
||||
}
|
||||
|
||||
// advanceDeletePrefixCursor prevents a backend that reports a truncated page
|
||||
// without a new cursor from making DeletePrefix loop forever.
|
||||
func advanceDeletePrefixCursor(current, next string, more bool) (string, error) {
|
||||
if !more {
|
||||
return "", nil
|
||||
}
|
||||
current = strings.TrimSpace(current)
|
||||
next = strings.TrimSpace(next)
|
||||
if next == "" || next == current {
|
||||
return "", errors.New("storage delete prefix pagination made no progress")
|
||||
}
|
||||
return next, nil
|
||||
}
|
||||
@@ -96,8 +96,13 @@ func (s *qiniuStorage) Compose(ctx context.Context, names []string, destination
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *qiniuStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
prefix, err := normalizeDeletePrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := ""
|
||||
for {
|
||||
items, _, more, err := s.List(ctx, prefix, "", 1000)
|
||||
items, next, more, err := s.List(ctx, prefix, cursor, 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -109,6 +114,10 @@ func (s *qiniuStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
if !more {
|
||||
return nil
|
||||
}
|
||||
cursor, err = advanceDeletePrefixCursor(cursor, next, more)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
func (s *qiniuStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*system.StoredFile, string, bool, error) {
|
||||
|
||||
@@ -98,6 +98,10 @@ func (s *s3Storage) Compose(ctx context.Context, names []string, destination str
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *s3Storage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
prefix, err := normalizeDeletePrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items := s.client.ListObjects(ctx, s.bucket, minio.ListObjectsOptions{Prefix: s.key(prefix), Recursive: true})
|
||||
for item := range items {
|
||||
if item.Err != nil {
|
||||
|
||||
@@ -82,6 +82,10 @@ func (s *tencentStorage) Compose(ctx context.Context, names []string, destinatio
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *tencentStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
prefix, err := normalizeDeletePrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := ""
|
||||
for {
|
||||
items, next, more, err := s.List(ctx, prefix, cursor, 1000)
|
||||
@@ -96,7 +100,10 @@ func (s *tencentStorage) DeletePrefix(ctx context.Context, prefix string) error
|
||||
if !more {
|
||||
return nil
|
||||
}
|
||||
cursor = next
|
||||
cursor, err = advanceDeletePrefixCursor(cursor, next, more)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
func (s *tencentStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*system.StoredFile, string, bool, error) {
|
||||
|
||||
Reference in New Issue
Block a user