Skip to content

Commit 055696f

Browse files
authored
feat(s3): support frontend direct upload (#1631)
* feat(s3): support frontend direct upload * feat(s3): support custom direct upload host * fix: apply suggestions of Copilot
1 parent 8544151 commit 055696f

File tree

8 files changed

+140
-63
lines changed

8 files changed

+140
-63
lines changed

drivers/alias/util.go

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"errors"
66
stdpath "path"
77
"strings"
8-
"sync"
98
"time"
109

1110
"github.com/OpenListTeam/OpenList/v4/internal/driver"
@@ -17,9 +16,15 @@ import (
1716
log "github.com/sirupsen/logrus"
1817
)
1918

19+
type detailWithIndex struct {
20+
idx int
21+
val *model.StorageDetails
22+
}
23+
2024
func (d *Alias) listRoot(ctx context.Context, withDetails, refresh bool) []model.Obj {
2125
var objs []model.Obj
22-
var wg sync.WaitGroup
26+
detailsChan := make(chan detailWithIndex, len(d.pathMap))
27+
workerCount := 0
2328
for _, k := range d.rootOrder {
2429
obj := model.Object{
2530
Name: k,
@@ -47,22 +52,26 @@ func (d *Alias) listRoot(ctx context.Context, withDetails, refresh bool) []model
4752
DriverName: remoteDriver.Config().Name,
4853
},
4954
}
50-
wg.Add(1)
51-
go func() {
52-
defer wg.Done()
53-
c, cancel := context.WithTimeout(ctx, time.Second)
54-
defer cancel()
55-
details, e := op.GetStorageDetails(c, remoteDriver, refresh)
55+
workerCount++
56+
go func(dri driver.Driver, i int) {
57+
details, e := op.GetStorageDetails(ctx, dri, refresh)
5658
if e != nil {
5759
if !errors.Is(e, errs.NotImplement) && !errors.Is(e, errs.StorageNotInit) {
58-
log.Errorf("failed get %s storage details: %+v", remoteDriver.GetStorage().MountPath, e)
60+
log.Errorf("failed get %s storage details: %+v", dri.GetStorage().MountPath, e)
5961
}
60-
return
6162
}
62-
objs[idx].(*model.ObjStorageDetails).StorageDetails = details
63-
}()
63+
detailsChan <- detailWithIndex{idx: i, val: details}
64+
}(remoteDriver, idx)
65+
}
66+
for workerCount > 0 {
67+
select {
68+
case r := <-detailsChan:
69+
objs[r.idx].(*model.ObjStorageDetails).StorageDetails = r.val
70+
workerCount--
71+
case <-time.After(time.Second):
72+
workerCount = 0
73+
}
6474
}
65-
wg.Wait()
6675
return objs
6776
}
6877

drivers/onedrive/meta.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@ import (
77

88
type Addition struct {
99
driver.RootPath
10-
Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"`
11-
IsSharepoint bool `json:"is_sharepoint"`
12-
UseOnlineAPI bool `json:"use_online_api" default:"true"`
13-
APIAddress string `json:"api_url_address" default:"https://api.oplist.org/onedrive/renewapi"`
14-
ClientID string `json:"client_id"`
15-
ClientSecret string `json:"client_secret"`
16-
RedirectUri string `json:"redirect_uri" required:"true" default:"https://api.oplist.org/onedrive/callback"`
17-
RefreshToken string `json:"refresh_token" required:"true"`
18-
SiteId string `json:"site_id"`
19-
ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
20-
CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
21-
DisableDiskUsage bool `json:"disable_disk_usage" default:"false"`
22-
EnableDirectUpload bool `json:"enable_direct_upload" default:"false" help:"Enable direct upload from client to OneDrive"`
10+
Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"`
11+
IsSharepoint bool `json:"is_sharepoint"`
12+
UseOnlineAPI bool `json:"use_online_api" default:"true"`
13+
APIAddress string `json:"api_url_address" default:"https://api.oplist.org/onedrive/renewapi"`
14+
ClientID string `json:"client_id"`
15+
ClientSecret string `json:"client_secret"`
16+
RedirectUri string `json:"redirect_uri" required:"true" default:"https://api.oplist.org/onedrive/callback"`
17+
RefreshToken string `json:"refresh_token" required:"true"`
18+
SiteId string `json:"site_id"`
19+
ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
20+
CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
21+
DisableDiskUsage bool `json:"disable_disk_usage" default:"false"`
22+
EnableDirectUpload bool `json:"enable_direct_upload" default:"false" help:"Enable direct upload from client to OneDrive"`
2323
}
2424

2525
var config = driver.Config{

drivers/quark_uc/driver.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,10 @@ func (d *QuarkOrUC) GetDetails(ctx context.Context) (*model.StorageDetails, erro
217217
if err != nil {
218218
return nil, err
219219
}
220+
used := memberInfo.Data.UseCapacity
221+
total := memberInfo.Data.TotalCapacity
220222
return &model.StorageDetails{
221-
DiskUsage: model.DiskUsage{
222-
TotalSpace: memberInfo.Data.TotalCapacity,
223-
FreeSpace: memberInfo.Data.TotalCapacity - memberInfo.Data.UseCapacity,
224-
},
223+
DiskUsage: driver.DiskUsageFromUsedAndTotal(used, total),
225224
}, nil
226225
}
227226

drivers/s3/driver.go

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

1212
"github.com/OpenListTeam/OpenList/v4/internal/driver"
13+
"github.com/OpenListTeam/OpenList/v4/internal/errs"
1314
"github.com/OpenListTeam/OpenList/v4/internal/model"
1415
"github.com/OpenListTeam/OpenList/v4/internal/stream"
1516
"github.com/OpenListTeam/OpenList/v4/pkg/cron"
@@ -24,9 +25,10 @@ import (
2425
type S3 struct {
2526
model.Storage
2627
Addition
27-
Session *session.Session
28-
client *s3.S3
29-
linkClient *s3.S3
28+
Session *session.Session
29+
client *s3.S3
30+
linkClient *s3.S3
31+
directUploadClient *s3.S3
3032

3133
config driver.Config
3234
cron *cron.Cron
@@ -52,16 +54,18 @@ func (d *S3) Init(ctx context.Context) error {
5254
if err != nil {
5355
log.Errorln("Doge init session error:", err)
5456
}
55-
d.client = d.getClient(false)
56-
d.linkClient = d.getClient(true)
57+
d.client = d.getClient(ClientTypeNormal)
58+
d.linkClient = d.getClient(ClientTypeLink)
59+
d.directUploadClient = d.getClient(ClientTypeDirectUpload)
5760
})
5861
}
5962
err := d.initSession()
6063
if err != nil {
6164
return err
6265
}
63-
d.client = d.getClient(false)
64-
d.linkClient = d.getClient(true)
66+
d.client = d.getClient(ClientTypeNormal)
67+
d.linkClient = d.getClient(ClientTypeLink)
68+
d.directUploadClient = d.getClient(ClientTypeDirectUpload)
6569
return nil
6670
}
6771

@@ -210,4 +214,33 @@ func (d *S3) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up
210214
return err
211215
}
212216

217+
func (d *S3) GetDirectUploadTools() []string {
218+
if !d.EnableDirectUpload {
219+
return nil
220+
}
221+
return []string{"HttpDirect"}
222+
}
223+
224+
func (d *S3) GetDirectUploadInfo(ctx context.Context, _ string, dstDir model.Obj, fileName string, _ int64) (any, error) {
225+
if !d.EnableDirectUpload {
226+
return nil, errs.NotImplement
227+
}
228+
path := getKey(stdpath.Join(dstDir.GetPath(), fileName), false)
229+
req, _ := d.directUploadClient.PutObjectRequest(&s3.PutObjectInput{
230+
Bucket: &d.Bucket,
231+
Key: &path,
232+
})
233+
if req == nil {
234+
return nil, fmt.Errorf("failed to create PutObject request")
235+
}
236+
link, err := req.Presign(time.Hour * time.Duration(d.SignURLExpire))
237+
if err != nil {
238+
return nil, err
239+
}
240+
return &model.HttpDirectUploadInfo{
241+
UploadURL: link,
242+
Method: "PUT",
243+
}, nil
244+
}
245+
213246
var _ driver.Driver = (*S3)(nil)

drivers/s3/meta.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ type Addition struct {
2121
ListObjectVersion string `json:"list_object_version" type:"select" options:"v1,v2" default:"v1"`
2222
RemoveBucket bool `json:"remove_bucket" help:"Remove bucket name from path when using custom host."`
2323
AddFilenameToDisposition bool `json:"add_filename_to_disposition" help:"Add filename to Content-Disposition header."`
24+
EnableDirectUpload bool `json:"enable_direct_upload" default:"false"`
25+
DirectUploadHost string `json:"direct_upload_host" required:"false"`
2426
}
2527

2628
func init() {

drivers/s3/util.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,15 @@ func (d *S3) initSession() error {
4141
return err
4242
}
4343

44-
func (d *S3) getClient(link bool) *s3.S3 {
44+
const (
45+
ClientTypeNormal = iota
46+
ClientTypeLink
47+
ClientTypeDirectUpload
48+
)
49+
50+
func (d *S3) getClient(clientType int) *s3.S3 {
4551
client := s3.New(d.Session)
46-
if link && d.CustomHost != "" {
52+
if clientType == ClientTypeLink && d.CustomHost != "" {
4753
client.Handlers.Build.PushBack(func(r *request.Request) {
4854
if r.HTTPRequest.Method != http.MethodGet {
4955
return
@@ -58,6 +64,20 @@ func (d *S3) getClient(link bool) *s3.S3 {
5864
}
5965
})
6066
}
67+
if clientType == ClientTypeDirectUpload && d.DirectUploadHost != "" {
68+
client.Handlers.Build.PushBack(func(r *request.Request) {
69+
if r.HTTPRequest.Method != http.MethodPut {
70+
return
71+
}
72+
split := strings.SplitN(d.DirectUploadHost, "://", 2)
73+
if utils.SliceContains([]string{"http", "https"}, split[0]) {
74+
r.HTTPRequest.URL.Scheme = split[0]
75+
r.HTTPRequest.URL.Host = split[1]
76+
} else {
77+
r.HTTPRequest.URL.Host = d.DirectUploadHost
78+
}
79+
})
80+
}
6181
return client
6282
}
6383

internal/op/storage.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -358,16 +358,21 @@ func GetStorageVirtualFilesWithDetailsByPath(ctx context.Context, prefix string,
358358
DriverName: d.Config().Name,
359359
},
360360
}
361-
timeoutCtx, cancel := context.WithTimeout(ctx, time.Second)
362-
defer cancel()
363-
details, err := GetStorageDetails(timeoutCtx, d, refresh)
364-
if err != nil {
365-
if !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.StorageNotInit) {
366-
log.Errorf("failed get %s storage details: %+v", d.GetStorage().MountPath, err)
361+
resultChan := make(chan *model.StorageDetails, 1)
362+
go func(dri driver.Driver) {
363+
details, err := GetStorageDetails(ctx, dri, refresh)
364+
if err != nil {
365+
if !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.StorageNotInit) {
366+
log.Errorf("failed get %s storage details: %+v", dri.GetStorage().MountPath, err)
367+
}
367368
}
368-
return ret
369+
resultChan <- details
370+
}(d)
371+
select {
372+
case r := <-resultChan:
373+
ret.StorageDetails = r
374+
case <-time.After(time.Second):
369375
}
370-
ret.StorageDetails = details
371376
return ret
372377
})
373378
}

server/handles/storage.go

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"errors"
66
"strconv"
7-
"sync"
87
"time"
98

109
"github.com/OpenListTeam/OpenList/v4/internal/conf"
@@ -24,9 +23,15 @@ type StorageResp struct {
2423
MountDetails *model.StorageDetails `json:"mount_details,omitempty"`
2524
}
2625

27-
func makeStorageResp(c *gin.Context, storages []model.Storage) []*StorageResp {
26+
type detailWithIndex struct {
27+
idx int
28+
val *model.StorageDetails
29+
}
30+
31+
func makeStorageResp(ctx *gin.Context, storages []model.Storage) []*StorageResp {
2832
ret := make([]*StorageResp, len(storages))
29-
var wg sync.WaitGroup
33+
detailsChan := make(chan detailWithIndex, len(storages))
34+
workerCount := 0
3035
for i, s := range storages {
3136
ret[i] = &StorageResp{
3237
Storage: s,
@@ -43,22 +48,26 @@ func makeStorageResp(c *gin.Context, storages []model.Storage) []*StorageResp {
4348
if !ok {
4449
continue
4550
}
46-
wg.Add(1)
47-
go func() {
48-
defer wg.Done()
49-
ctx, cancel := context.WithTimeout(c, time.Second*3)
50-
defer cancel()
51-
details, err := op.GetStorageDetails(ctx, d)
52-
if err != nil {
53-
if !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.StorageNotInit) {
54-
log.Errorf("failed get %s details: %+v", s.MountPath, err)
51+
workerCount++
52+
go func(dri driver.Driver, idx int) {
53+
details, e := op.GetStorageDetails(ctx, dri)
54+
if e != nil {
55+
if !errors.Is(e, errs.NotImplement) && !errors.Is(e, errs.StorageNotInit) {
56+
log.Errorf("failed get %s details: %+v", dri.GetStorage().MountPath, e)
5557
}
56-
return
5758
}
58-
ret[i].MountDetails = details
59-
}()
59+
detailsChan <- detailWithIndex{idx: idx, val: details}
60+
}(d, i)
61+
}
62+
for workerCount > 0 {
63+
select {
64+
case r := <-detailsChan:
65+
ret[r.idx].MountDetails = r.val
66+
workerCount--
67+
case <-time.After(time.Second * 3):
68+
workerCount = 0
69+
}
6070
}
61-
wg.Wait()
6271
return ret
6372
}
6473

0 commit comments

Comments
 (0)