Compare commits

...

2 Commits

Author SHA1 Message Date
MadDogOwner
0866b9075f fix(link): correct link cache mode bitwise comparison (#1635)
* fix(link): correct link cache mode bitwise comparison

Signed-off-by: MadDogOwner <xiaoran@xrgzs.top>

* refactor(link): use explicit flag equality for link cache mode bitmask checks

Signed-off-by: MadDogOwner <xiaoran@xrgzs.top>

---------

Signed-off-by: MadDogOwner <xiaoran@xrgzs.top>
2025-11-13 13:52:33 +08:00
KirCute
055696f576 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
2025-11-13 13:22:17 +08:00
9 changed files with 142 additions and 65 deletions

View File

@@ -5,7 +5,6 @@ import (
"errors" "errors"
stdpath "path" stdpath "path"
"strings" "strings"
"sync"
"time" "time"
"github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/driver"
@@ -17,9 +16,15 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
type detailWithIndex struct {
idx int
val *model.StorageDetails
}
func (d *Alias) listRoot(ctx context.Context, withDetails, refresh bool) []model.Obj { func (d *Alias) listRoot(ctx context.Context, withDetails, refresh bool) []model.Obj {
var objs []model.Obj var objs []model.Obj
var wg sync.WaitGroup detailsChan := make(chan detailWithIndex, len(d.pathMap))
workerCount := 0
for _, k := range d.rootOrder { for _, k := range d.rootOrder {
obj := model.Object{ obj := model.Object{
Name: k, Name: k,
@@ -47,22 +52,26 @@ func (d *Alias) listRoot(ctx context.Context, withDetails, refresh bool) []model
DriverName: remoteDriver.Config().Name, DriverName: remoteDriver.Config().Name,
}, },
} }
wg.Add(1) workerCount++
go func() { go func(dri driver.Driver, i int) {
defer wg.Done() details, e := op.GetStorageDetails(ctx, dri, refresh)
c, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
details, e := op.GetStorageDetails(c, remoteDriver, refresh)
if e != nil { if e != nil {
if !errors.Is(e, errs.NotImplement) && !errors.Is(e, errs.StorageNotInit) { if !errors.Is(e, errs.NotImplement) && !errors.Is(e, errs.StorageNotInit) {
log.Errorf("failed get %s storage details: %+v", remoteDriver.GetStorage().MountPath, e) log.Errorf("failed get %s storage details: %+v", dri.GetStorage().MountPath, e)
} }
return
} }
objs[idx].(*model.ObjStorageDetails).StorageDetails = details detailsChan <- detailWithIndex{idx: i, val: details}
}() }(remoteDriver, idx)
}
for workerCount > 0 {
select {
case r := <-detailsChan:
objs[r.idx].(*model.ObjStorageDetails).StorageDetails = r.val
workerCount--
case <-time.After(time.Second):
workerCount = 0
}
} }
wg.Wait()
return objs return objs
} }

View File

@@ -7,19 +7,19 @@ import (
type Addition struct { type Addition struct {
driver.RootPath driver.RootPath
Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"` Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"`
IsSharepoint bool `json:"is_sharepoint"` IsSharepoint bool `json:"is_sharepoint"`
UseOnlineAPI bool `json:"use_online_api" default:"true"` UseOnlineAPI bool `json:"use_online_api" default:"true"`
APIAddress string `json:"api_url_address" default:"https://api.oplist.org/onedrive/renewapi"` APIAddress string `json:"api_url_address" default:"https://api.oplist.org/onedrive/renewapi"`
ClientID string `json:"client_id"` ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"` ClientSecret string `json:"client_secret"`
RedirectUri string `json:"redirect_uri" required:"true" default:"https://api.oplist.org/onedrive/callback"` RedirectUri string `json:"redirect_uri" required:"true" default:"https://api.oplist.org/onedrive/callback"`
RefreshToken string `json:"refresh_token" required:"true"` RefreshToken string `json:"refresh_token" required:"true"`
SiteId string `json:"site_id"` SiteId string `json:"site_id"`
ChunkSize int64 `json:"chunk_size" type:"number" default:"5"` ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"` CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
DisableDiskUsage bool `json:"disable_disk_usage" default:"false"` DisableDiskUsage bool `json:"disable_disk_usage" default:"false"`
EnableDirectUpload bool `json:"enable_direct_upload" default:"false" help:"Enable direct upload from client to OneDrive"` EnableDirectUpload bool `json:"enable_direct_upload" default:"false" help:"Enable direct upload from client to OneDrive"`
} }
var config = driver.Config{ var config = driver.Config{

View File

@@ -217,11 +217,10 @@ func (d *QuarkOrUC) GetDetails(ctx context.Context) (*model.StorageDetails, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
used := memberInfo.Data.UseCapacity
total := memberInfo.Data.TotalCapacity
return &model.StorageDetails{ return &model.StorageDetails{
DiskUsage: model.DiskUsage{ DiskUsage: driver.DiskUsageFromUsedAndTotal(used, total),
TotalSpace: memberInfo.Data.TotalCapacity,
FreeSpace: memberInfo.Data.TotalCapacity - memberInfo.Data.UseCapacity,
},
}, nil }, nil
} }

View File

@@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/cron" "github.com/OpenListTeam/OpenList/v4/pkg/cron"
@@ -24,9 +25,10 @@ import (
type S3 struct { type S3 struct {
model.Storage model.Storage
Addition Addition
Session *session.Session Session *session.Session
client *s3.S3 client *s3.S3
linkClient *s3.S3 linkClient *s3.S3
directUploadClient *s3.S3
config driver.Config config driver.Config
cron *cron.Cron cron *cron.Cron
@@ -52,16 +54,18 @@ func (d *S3) Init(ctx context.Context) error {
if err != nil { if err != nil {
log.Errorln("Doge init session error:", err) log.Errorln("Doge init session error:", err)
} }
d.client = d.getClient(false) d.client = d.getClient(ClientTypeNormal)
d.linkClient = d.getClient(true) d.linkClient = d.getClient(ClientTypeLink)
d.directUploadClient = d.getClient(ClientTypeDirectUpload)
}) })
} }
err := d.initSession() err := d.initSession()
if err != nil { if err != nil {
return err return err
} }
d.client = d.getClient(false) d.client = d.getClient(ClientTypeNormal)
d.linkClient = d.getClient(true) d.linkClient = d.getClient(ClientTypeLink)
d.directUploadClient = d.getClient(ClientTypeDirectUpload)
return nil return nil
} }
@@ -210,4 +214,33 @@ func (d *S3) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up
return err return err
} }
func (d *S3) GetDirectUploadTools() []string {
if !d.EnableDirectUpload {
return nil
}
return []string{"HttpDirect"}
}
func (d *S3) GetDirectUploadInfo(ctx context.Context, _ string, dstDir model.Obj, fileName string, _ int64) (any, error) {
if !d.EnableDirectUpload {
return nil, errs.NotImplement
}
path := getKey(stdpath.Join(dstDir.GetPath(), fileName), false)
req, _ := d.directUploadClient.PutObjectRequest(&s3.PutObjectInput{
Bucket: &d.Bucket,
Key: &path,
})
if req == nil {
return nil, fmt.Errorf("failed to create PutObject request")
}
link, err := req.Presign(time.Hour * time.Duration(d.SignURLExpire))
if err != nil {
return nil, err
}
return &model.HttpDirectUploadInfo{
UploadURL: link,
Method: "PUT",
}, nil
}
var _ driver.Driver = (*S3)(nil) var _ driver.Driver = (*S3)(nil)

View File

@@ -21,6 +21,8 @@ type Addition struct {
ListObjectVersion string `json:"list_object_version" type:"select" options:"v1,v2" default:"v1"` ListObjectVersion string `json:"list_object_version" type:"select" options:"v1,v2" default:"v1"`
RemoveBucket bool `json:"remove_bucket" help:"Remove bucket name from path when using custom host."` RemoveBucket bool `json:"remove_bucket" help:"Remove bucket name from path when using custom host."`
AddFilenameToDisposition bool `json:"add_filename_to_disposition" help:"Add filename to Content-Disposition header."` AddFilenameToDisposition bool `json:"add_filename_to_disposition" help:"Add filename to Content-Disposition header."`
EnableDirectUpload bool `json:"enable_direct_upload" default:"false"`
DirectUploadHost string `json:"direct_upload_host" required:"false"`
} }
func init() { func init() {

View File

@@ -41,9 +41,15 @@ func (d *S3) initSession() error {
return err return err
} }
func (d *S3) getClient(link bool) *s3.S3 { const (
ClientTypeNormal = iota
ClientTypeLink
ClientTypeDirectUpload
)
func (d *S3) getClient(clientType int) *s3.S3 {
client := s3.New(d.Session) client := s3.New(d.Session)
if link && d.CustomHost != "" { if clientType == ClientTypeLink && d.CustomHost != "" {
client.Handlers.Build.PushBack(func(r *request.Request) { client.Handlers.Build.PushBack(func(r *request.Request) {
if r.HTTPRequest.Method != http.MethodGet { if r.HTTPRequest.Method != http.MethodGet {
return return
@@ -58,6 +64,20 @@ func (d *S3) getClient(link bool) *s3.S3 {
} }
}) })
} }
if clientType == ClientTypeDirectUpload && d.DirectUploadHost != "" {
client.Handlers.Build.PushBack(func(r *request.Request) {
if r.HTTPRequest.Method != http.MethodPut {
return
}
split := strings.SplitN(d.DirectUploadHost, "://", 2)
if utils.SliceContains([]string{"http", "https"}, split[0]) {
r.HTTPRequest.URL.Scheme = split[0]
r.HTTPRequest.URL.Host = split[1]
} else {
r.HTTPRequest.URL.Host = d.DirectUploadHost
}
})
}
return client return client
} }

View File

@@ -173,10 +173,10 @@ func Link(ctx context.Context, storage driver.Driver, path string, args model.Li
mode = storage.(driver.LinkCacheModeResolver).ResolveLinkCacheMode(path) mode = storage.(driver.LinkCacheModeResolver).ResolveLinkCacheMode(path)
} }
typeKey := args.Type typeKey := args.Type
if mode&driver.LinkCacheIP == 1 { if mode&driver.LinkCacheIP == driver.LinkCacheIP {
typeKey += "/" + args.IP typeKey += "/" + args.IP
} }
if mode&driver.LinkCacheUA == 1 { if mode&driver.LinkCacheUA == driver.LinkCacheUA {
typeKey += "/" + args.Header.Get("User-Agent") typeKey += "/" + args.Header.Get("User-Agent")
} }
key := Key(storage, path) key := Key(storage, path)

View File

@@ -358,16 +358,21 @@ func GetStorageVirtualFilesWithDetailsByPath(ctx context.Context, prefix string,
DriverName: d.Config().Name, DriverName: d.Config().Name,
}, },
} }
timeoutCtx, cancel := context.WithTimeout(ctx, time.Second) resultChan := make(chan *model.StorageDetails, 1)
defer cancel() go func(dri driver.Driver) {
details, err := GetStorageDetails(timeoutCtx, d, refresh) details, err := GetStorageDetails(ctx, dri, refresh)
if err != nil { if err != nil {
if !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.StorageNotInit) { if !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.StorageNotInit) {
log.Errorf("failed get %s storage details: %+v", d.GetStorage().MountPath, err) log.Errorf("failed get %s storage details: %+v", dri.GetStorage().MountPath, err)
}
} }
return ret resultChan <- details
}(d)
select {
case r := <-resultChan:
ret.StorageDetails = r
case <-time.After(time.Second):
} }
ret.StorageDetails = details
return ret return ret
}) })
} }

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/OpenListTeam/OpenList/v4/internal/conf" "github.com/OpenListTeam/OpenList/v4/internal/conf"
@@ -24,9 +23,15 @@ type StorageResp struct {
MountDetails *model.StorageDetails `json:"mount_details,omitempty"` MountDetails *model.StorageDetails `json:"mount_details,omitempty"`
} }
func makeStorageResp(c *gin.Context, storages []model.Storage) []*StorageResp { type detailWithIndex struct {
idx int
val *model.StorageDetails
}
func makeStorageResp(ctx *gin.Context, storages []model.Storage) []*StorageResp {
ret := make([]*StorageResp, len(storages)) ret := make([]*StorageResp, len(storages))
var wg sync.WaitGroup detailsChan := make(chan detailWithIndex, len(storages))
workerCount := 0
for i, s := range storages { for i, s := range storages {
ret[i] = &StorageResp{ ret[i] = &StorageResp{
Storage: s, Storage: s,
@@ -43,22 +48,26 @@ func makeStorageResp(c *gin.Context, storages []model.Storage) []*StorageResp {
if !ok { if !ok {
continue continue
} }
wg.Add(1) workerCount++
go func() { go func(dri driver.Driver, idx int) {
defer wg.Done() details, e := op.GetStorageDetails(ctx, dri)
ctx, cancel := context.WithTimeout(c, time.Second*3) if e != nil {
defer cancel() if !errors.Is(e, errs.NotImplement) && !errors.Is(e, errs.StorageNotInit) {
details, err := op.GetStorageDetails(ctx, d) log.Errorf("failed get %s details: %+v", dri.GetStorage().MountPath, e)
if err != nil {
if !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.StorageNotInit) {
log.Errorf("failed get %s details: %+v", s.MountPath, err)
} }
return
} }
ret[i].MountDetails = details detailsChan <- detailWithIndex{idx: idx, val: details}
}() }(d, i)
}
for workerCount > 0 {
select {
case r := <-detailsChan:
ret[r.idx].MountDetails = r.val
workerCount--
case <-time.After(time.Second * 3):
workerCount = 0
}
} }
wg.Wait()
return ret return ret
} }