Skip to content

Commit c570c99

Browse files
committed
feat(webdav): redirect direct uploads
- Redirect eligible WebDAV PUT requests to single direct upload URLs. - Force direct upload max parts to one for WebDAV redirects. Signed-off-by: Ariel Yu <ariel@ariels.xyz>
1 parent 7dfe260 commit c570c99

4 files changed

Lines changed: 89 additions & 6 deletions

File tree

drivers/s3/driver.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010
"time"
1111

12+
"github.com/OpenListTeam/OpenList/v4/internal/conf"
1213
"github.com/OpenListTeam/OpenList/v4/internal/driver"
1314
"github.com/OpenListTeam/OpenList/v4/internal/errs"
1415
"github.com/OpenListTeam/OpenList/v4/internal/model"
@@ -228,16 +229,19 @@ func (d *S3) GetDirectUploadInfo(ctx context.Context, _ string, dstDir model.Obj
228229
if !d.EnableDirectUpload {
229230
return nil, errs.NotImplement
230231
}
232+
if maxParts, ok := ctx.Value(conf.DirectUploadMaxPartsKey).(int64); ok {
233+
return d.getDirectUploadInfo(ctx, dstDir, fileName, fileSize, maxParts)
234+
}
231235
if d.DirectUploadMaxParts == 0 {
232236
d.DirectUploadMaxParts = maxCopyParts
233237
}
234-
return d.getDirectUploadInfo(ctx, dstDir, fileName, fileSize)
238+
return d.getDirectUploadInfo(ctx, dstDir, fileName, fileSize, d.DirectUploadMaxParts)
235239
}
236240

237-
func (d *S3) getDirectUploadInfo(ctx context.Context, dstDir model.Obj, fileName string, fileSize int64) (any, error) {
241+
func (d *S3) getDirectUploadInfo(ctx context.Context, dstDir model.Obj, fileName string, fileSize, maxParts int64) (any, error) {
238242
path := getKey(stdpath.Join(dstDir.GetPath(), fileName), false)
239-
if d.DirectUploadMaxParts > 1 && fileSize > minMultipartUploadPartSize {
240-
return d.getMultipartDirectUploadInfo(ctx, path, fileSize)
243+
if maxParts > 1 && fileSize > minMultipartUploadPartSize {
244+
return d.getMultipartDirectUploadInfo(ctx, path, fileSize, maxParts)
241245
}
242246
req, _ := d.directUploadClient.PutObjectRequest(&s3.PutObjectInput{
243247
Bucket: &d.Bucket,
@@ -256,8 +260,8 @@ func (d *S3) getDirectUploadInfo(ctx context.Context, dstDir model.Obj, fileName
256260
}, nil
257261
}
258262

259-
func (d *S3) getMultipartDirectUploadInfo(ctx context.Context, key string, fileSize int64) (*model.S3MultipartDirectUploadInfo, error) {
260-
partSize, err := getMultipartUploadPartSize(fileSize, d.DirectUploadMaxParts)
263+
func (d *S3) getMultipartDirectUploadInfo(ctx context.Context, key string, fileSize, maxParts int64) (*model.S3MultipartDirectUploadInfo, error) {
264+
partSize, err := getMultipartUploadPartSize(fileSize, maxParts)
261265
if err != nil {
262266
return nil, err
263267
}

drivers/s3/util_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212
"testing"
1313

14+
"github.com/OpenListTeam/OpenList/v4/internal/conf"
1415
"github.com/OpenListTeam/OpenList/v4/internal/model"
1516
"github.com/aws/aws-sdk-go/aws"
1617
"github.com/aws/aws-sdk-go/aws/credentials"
@@ -283,6 +284,34 @@ func TestGetDirectUploadInfoUsesSinglePutWhenMultipartMaxPartsIsOne(t *testing.T
283284
}
284285
}
285286

287+
func TestGetDirectUploadInfoHonorsContextMaxPartsOverride(t *testing.T) {
288+
const fileSize = 25 * 1024 * 1024
289+
putRequests := 0
290+
d := newTestS3Driver(t, func(w http.ResponseWriter, r *http.Request) {
291+
if r.Method != http.MethodPut || r.URL.Query().Get("uploadId") != "" {
292+
t.Errorf("unexpected request: %s %s", r.Method, r.URL.String())
293+
w.WriteHeader(http.StatusBadRequest)
294+
return
295+
}
296+
putRequests++
297+
w.WriteHeader(http.StatusOK)
298+
})
299+
d.EnableDirectUpload = true
300+
d.DirectUploadMaxParts = 10000
301+
ctx := context.WithValue(context.Background(), conf.DirectUploadMaxPartsKey, int64(1))
302+
303+
info, err := d.GetDirectUploadInfo(ctx, "HttpDirect", &model.Object{Path: "/"}, "large-file", fileSize)
304+
if err != nil {
305+
t.Fatalf("GetDirectUploadInfo: %v", err)
306+
}
307+
if _, ok := info.(*model.HttpDirectUploadInfo); !ok {
308+
t.Fatalf("upload info type = %T, want HttpDirectUploadInfo", info)
309+
}
310+
if putRequests != 0 {
311+
t.Fatalf("PutObject was executed while presigning, requests = %d", putRequests)
312+
}
313+
}
314+
286315
func TestDirectMultipartUploadCompletesWithUploadedPartETags(t *testing.T) {
287316
const fileSize = 25 * 1024 * 1024
288317
uploadedParts := make(map[string]string)

internal/conf/const.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,5 @@ const (
191191
PathKey
192192
SharingIDKey
193193
SkipHookKey
194+
DirectUploadMaxPartsKey
194195
)

server/webdav/webdav.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,11 @@ func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status i
337337
}
338338

339339
func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) {
340+
drainBody := true
340341
defer func() {
342+
if !drainBody {
343+
return
344+
}
341345
if n, _ := io.ReadFull(r.Body, []byte{0}); n == 1 {
342346
_, _ = utils.CopyWithBuffer(io.Discard, r.Body)
343347
}
@@ -394,6 +398,12 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
394398
if !common.CanWrite(user, parentMeta, parentPath) {
395399
return http.StatusForbidden, errs.PermissionDenied
396400
}
401+
if redirectURL, ok := directPutRedirectURL(ctx, reqPath, size); ok {
402+
drainBody = false
403+
w.Header().Set("Cache-Control", "no-store")
404+
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
405+
return 0, nil
406+
}
397407
fsStream := &stream.FileStream{
398408
Obj: &obj,
399409
Reader: r.Body,
@@ -423,6 +433,45 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
423433
return http.StatusCreated, nil
424434
}
425435

436+
func directPutRedirectURL(ctx context.Context, reqPath string, size int64) (string, bool) {
437+
if size < 0 {
438+
return "", false
439+
}
440+
storage, dstDirActualPath, err := op.GetStorageAndActualPath(path.Dir(reqPath))
441+
if err != nil || storage.Config().NoUpload {
442+
return "", false
443+
}
444+
ctx = context.WithValue(ctx, conf.DirectUploadMaxPartsKey, int64(1))
445+
info, err := op.GetDirectUploadInfo(ctx, "HttpDirect", storage, dstDirActualPath, path.Base(reqPath), size, true)
446+
if err != nil {
447+
return "", false
448+
}
449+
httpInfo, ok := asHTTPDirectUploadInfo(info)
450+
if !ok {
451+
return "", false
452+
}
453+
method := httpInfo.Method
454+
if method == "" {
455+
method = http.MethodPut
456+
}
457+
if !strings.EqualFold(method, http.MethodPut) || httpInfo.UploadURL == "" ||
458+
httpInfo.ChunkSize > 0 || len(httpInfo.Headers) > 0 {
459+
return "", false
460+
}
461+
return httpInfo.UploadURL, true
462+
}
463+
464+
func asHTTPDirectUploadInfo(info any) (*model.HttpDirectUploadInfo, bool) {
465+
switch v := info.(type) {
466+
case *model.HttpDirectUploadInfo:
467+
return v, v != nil
468+
case model.HttpDirectUploadInfo:
469+
return &v, true
470+
default:
471+
return nil, false
472+
}
473+
}
474+
426475
func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) {
427476
reqPath, status, err := h.stripPrefix(r.URL.Path)
428477
if err != nil {

0 commit comments

Comments
 (0)