Skip to content

Commit b51d1c8

Browse files
author
Nostalgia
authored
fix(s3): close ranged response bodies (#3095)
1 parent f286862 commit b51d1c8

2 files changed

Lines changed: 135 additions & 2 deletions

File tree

‎server/s3/backend.go‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ func (b *s3Backend) GetObject(ctx context.Context, bucketName, objectName string
193193
return nil, fmt.Errorf("the remote storage driver need to be enhanced to support s3")
194194
}
195195

196-
var rd io.Reader
196+
var rd io.ReadCloser
197197
if rnge != nil {
198198
rd, err = rrf.RangeRead(ctx, http_range.Range(*rnge))
199199
} else {
@@ -215,6 +215,7 @@ func (b *s3Backend) GetObject(ctx context.Context, bucketName, objectName string
215215
meta[k] = v
216216
}
217217
}
218+
closers := utils.NewClosers(rd, link)
218219

219220
return &gofakes3.Object{
220221
// Name: gofakes3.URLEncode(objectName),
@@ -223,7 +224,7 @@ func (b *s3Backend) GetObject(ctx context.Context, bucketName, objectName string
223224
Metadata: meta,
224225
Size: size,
225226
Range: rnge,
226-
Contents: utils.ReadCloser{Reader: rd, Closer: link},
227+
Contents: utils.ReadCloser{Reader: rd, Closer: &closers},
227228
}, nil
228229
}
229230

‎server/s3/get_object_close_test.go‎

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package s3
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"io"
8+
"os"
9+
"path/filepath"
10+
"slices"
11+
"strings"
12+
"testing"
13+
14+
"github.com/OpenListTeam/OpenList/v4/drivers/local"
15+
"github.com/OpenListTeam/OpenList/v4/internal/conf"
16+
"github.com/OpenListTeam/OpenList/v4/internal/db"
17+
"github.com/OpenListTeam/OpenList/v4/internal/driver"
18+
"github.com/OpenListTeam/OpenList/v4/internal/model"
19+
"github.com/OpenListTeam/OpenList/v4/internal/op"
20+
"github.com/OpenListTeam/OpenList/v4/internal/stream"
21+
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
22+
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
23+
"gorm.io/gorm"
24+
)
25+
26+
const closeTrackingDriverName = "S3CloseTrackingLocal"
27+
28+
type closeTrackingDriver struct {
29+
local.Local
30+
closed *[]string
31+
}
32+
33+
func (d *closeTrackingDriver) Config() driver.Config {
34+
c := d.Local.Config()
35+
c.Name = closeTrackingDriverName
36+
return c
37+
}
38+
39+
func (d *closeTrackingDriver) Link(context.Context, model.Obj, model.LinkArgs) (*model.Link, error) {
40+
link := &model.Link{
41+
ContentLength: 4,
42+
RangeReader: stream.RangeReaderFunc(func(context.Context, http_range.Range) (io.ReadCloser, error) {
43+
return utils.NewReadCloser(strings.NewReader("body"), func() error {
44+
*d.closed = append(*d.closed, "body")
45+
return nil
46+
}), nil
47+
}),
48+
RequireReference: true,
49+
}
50+
link.SyncClosers.Add(utils.CloseFunc(func() error {
51+
*d.closed = append(*d.closed, "link")
52+
return nil
53+
}))
54+
return link, nil
55+
}
56+
57+
func TestGetObjectClosesRangeBodyBeforeLink(t *testing.T) {
58+
ctx := context.Background()
59+
var closed []string
60+
op.RegisterDriver(func() driver.Driver {
61+
return &closeTrackingDriver{closed: &closed}
62+
})
63+
64+
root := t.TempDir()
65+
if err := os.WriteFile(filepath.Join(root, "fixture.txt"), []byte("body"), 0o600); err != nil {
66+
t.Fatal(err)
67+
}
68+
addition, err := json.Marshal(struct {
69+
RootFolderPath string `json:"root_folder_path"`
70+
}{RootFolderPath: root})
71+
if err != nil {
72+
t.Fatal(err)
73+
}
74+
mount := "/" + sanitizeTestName(t.Name())
75+
storageID, err := op.CreateStorage(ctx, model.Storage{
76+
Driver: closeTrackingDriverName,
77+
MountPath: mount,
78+
Addition: string(addition),
79+
})
80+
if err != nil {
81+
t.Fatal(err)
82+
}
83+
t.Cleanup(func() {
84+
if err := op.DeleteStorageById(ctx, storageID); err != nil {
85+
t.Errorf("delete fixture storage: %v", err)
86+
}
87+
})
88+
89+
previousBuckets, previousBucketsErr := op.GetSettingItemByKey(conf.S3Buckets)
90+
if previousBucketsErr != nil && !errors.Is(previousBucketsErr, gorm.ErrRecordNotFound) {
91+
t.Fatal(previousBucketsErr)
92+
}
93+
if err := op.SaveSettingItem(&model.SettingItem{
94+
Key: conf.S3Buckets,
95+
Value: `[{"name":"close","path":"` + mount + `"}]`,
96+
}); err != nil {
97+
t.Fatal(err)
98+
}
99+
t.Cleanup(func() {
100+
if previousBucketsErr == nil {
101+
if err := op.SaveSettingItem(previousBuckets); err != nil {
102+
t.Errorf("restore S3 buckets: %v", err)
103+
}
104+
return
105+
}
106+
if err := db.DeleteSettingItemByKey(conf.S3Buckets); err != nil {
107+
t.Errorf("delete fixture S3 buckets: %v", err)
108+
}
109+
op.SettingCacheUpdate()
110+
})
111+
112+
object, err := newBackend().(*s3Backend).GetObject(ctx, "close", "fixture.txt", nil)
113+
if err != nil {
114+
t.Fatal(err)
115+
}
116+
contents, err := io.ReadAll(object.Contents)
117+
if err != nil {
118+
t.Fatal(err)
119+
}
120+
if string(contents) != "body" {
121+
t.Fatalf("contents = %q, want body", contents)
122+
}
123+
if err := object.Contents.Close(); err != nil {
124+
t.Fatal(err)
125+
}
126+
if err := object.Contents.Close(); err != nil {
127+
t.Fatal(err)
128+
}
129+
if !slices.Equal(closed, []string{"body", "link"}) {
130+
t.Fatalf("close order = %v, want [body link] exactly once", closed)
131+
}
132+
}

0 commit comments

Comments
 (0)