WebDAV clients such as Mountain Duck upload large files as a sequence of PUT requests carrying "Content-Range: bytes start-end/total". handlePut ignored the header and treated every request as a complete overwrite, so each chunk produced a new version entity whose predecessor went stale — a 300MB upload became ~300 recycled 1MB entities (#3005), and very large uploads compounded the failure surface (#3118). handlePut now parses Content-Range. Full-body ranges keep the existing m.Update path; partial ranges are assembled through a per-(user,path) upload session: CreateUploadSession sizes the placeholder to the total, each chunk writes via m.Upload at its byte offset, and MarkRangeUploaded records merged byte-interval coverage in KV so CompleteUpload fires only when [0,total) has been received. Only local storage policies are accepted — remote drivers cannot honor arbitrary write offsets and previously corrupted data silently; they now get 501. A PUT with no length information (chunked encoding, neither Content-Length nor X-Expected-Entity-Length) previously produced a silent empty file; it now returns 411 Length Required. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>pull/3582/head
parent
7bbdbd2057
commit
59f1c21e40
@ -0,0 +1,139 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
)
|
||||
|
||||
func TestMergeByteRanges(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ranges [][2]int64
|
||||
start int64
|
||||
end int64
|
||||
want [][2]int64
|
||||
}{
|
||||
{"empty", nil, 0, 10, [][2]int64{{0, 10}}},
|
||||
{"append gap", [][2]int64{{0, 10}}, 20, 30, [][2]int64{{0, 10}, {20, 30}}},
|
||||
{"prepend gap", [][2]int64{{20, 30}}, 0, 10, [][2]int64{{0, 10}, {20, 30}}},
|
||||
{"adjacent merge", [][2]int64{{0, 10}}, 10, 20, [][2]int64{{0, 20}}},
|
||||
{"bridge two", [][2]int64{{0, 10}, {20, 30}}, 10, 20, [][2]int64{{0, 30}}},
|
||||
{"contained", [][2]int64{{0, 30}}, 5, 15, [][2]int64{{0, 30}}},
|
||||
{"extend right", [][2]int64{{0, 10}}, 5, 20, [][2]int64{{0, 20}}},
|
||||
{"extend left", [][2]int64{{10, 20}}, 0, 15, [][2]int64{{0, 20}}},
|
||||
{"middle gap", [][2]int64{{0, 5}, {30, 40}}, 10, 20, [][2]int64{{0, 5}, {10, 20}, {30, 40}}},
|
||||
{"merge all", [][2]int64{{0, 5}, {10, 15}, {20, 25}}, 4, 21, [][2]int64{{0, 25}}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := mergeByteRanges(tt.ranges, tt.start, tt.end)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("got %v, want %v", got, tt.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Fatalf("got %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRangesCoverFull(t *testing.T) {
|
||||
if !rangesCoverFull([][2]int64{{0, 100}}, 100) {
|
||||
t.Fatal("expected full coverage")
|
||||
}
|
||||
if rangesCoverFull([][2]int64{{0, 99}}, 100) {
|
||||
t.Fatal("partial coverage reported as full")
|
||||
}
|
||||
if rangesCoverFull([][2]int64{{0, 50}, {50, 100}}, 100) {
|
||||
t.Fatal("unmerged intervals should not report full coverage")
|
||||
}
|
||||
if rangesCoverFull(nil, 100) {
|
||||
t.Fatal("empty intervals reported as full")
|
||||
}
|
||||
}
|
||||
|
||||
func newRangedSession(id string, size int64) fs.UploadSession {
|
||||
return fs.UploadSession{
|
||||
Props: &fs.UploadProps{
|
||||
UploadSessionID: id,
|
||||
Size: size,
|
||||
ExpireAt: time.Now().Add(time.Hour),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkRangeUploaded(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
m := &manager{kv: cache.NewMemoStore("", nil)}
|
||||
session := newRangedSession("test-range-session", 100)
|
||||
if err := m.kv.Set(UploadSessionCachePrefix+"test-range-session", session, 60); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Out-of-order arrival: second half first.
|
||||
all, err := m.MarkRangeUploaded(ctx, &session, 50, 50)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if all {
|
||||
t.Fatal("reported complete after only second half")
|
||||
}
|
||||
|
||||
all, err = m.MarkRangeUploaded(ctx, &session, 0, 50)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !all {
|
||||
t.Fatal("expected complete after full coverage")
|
||||
}
|
||||
|
||||
// Session record carries merged coverage.
|
||||
raw, ok := m.kv.Get(UploadSessionCachePrefix + "test-range-session")
|
||||
if !ok {
|
||||
t.Fatal("session missing from KV")
|
||||
}
|
||||
stored := raw.(fs.UploadSession)
|
||||
if len(stored.RangesReceived) != 1 || stored.RangesReceived[0] != [2]int64{0, 100} {
|
||||
t.Fatalf("unexpected ranges: %v", stored.RangesReceived)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkRangeUploadedGap(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
m := &manager{kv: cache.NewMemoStore("", nil)}
|
||||
session := newRangedSession("test-gap-session", 100)
|
||||
if err := m.kv.Set(UploadSessionCachePrefix+"test-gap-session", session, 60); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, r := range [][2]int64{{0, 40}, {60, 100}} {
|
||||
all, err := m.MarkRangeUploaded(ctx, &session, r[0], r[1]-r[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if all {
|
||||
t.Fatal("reported complete with a gap in coverage")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkRangeUploadedMissingSession(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
m := &manager{kv: cache.NewMemoStore("", nil)}
|
||||
session := newRangedSession("gone", 100)
|
||||
|
||||
all, err := m.MarkRangeUploaded(ctx, &session, 0, 100)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if all {
|
||||
t.Fatal("missing session reported complete")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue