Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions drivers/onedrive/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,19 @@ func (d *Onedrive) GetDetails(ctx context.Context) (*model.StorageDetails, error
}, nil
}

func (d *Onedrive) GetDirectUploadTools() []string {
if !d.EnableDirectUpload {
return nil
}
return []string{"HttpDirect"}
}

// GetDirectUploadInfo returns the direct upload info for OneDrive
func (d *Onedrive) GetDirectUploadInfo(ctx context.Context, _ string, dstDir model.Obj, fileName string, _ int64) (any, error) {
if !d.EnableDirectUpload {
return nil, errs.NotImplement
}
return d.getDirectUploadInfo(ctx, path.Join(dstDir.GetPath(), fileName))
}

var _ driver.Driver = (*Onedrive)(nil)
1 change: 1 addition & 0 deletions drivers/onedrive/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Addition struct {
ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
DisableDiskUsage bool `json:"disable_disk_usage" default:"false"`
EnableDirectUpload bool `json:"enable_direct_upload" default:"false" help:"Enable direct upload from client to OneDrive"`
}

var config = driver.Config{
Expand Down
33 changes: 30 additions & 3 deletions drivers/onedrive/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func (d *Onedrive) _refreshToken() error {
return nil
}

func (d *Onedrive) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
func (d *Onedrive) Request(url string, method string, callback base.ReqCallback, resp interface{}, noRetry ...bool) ([]byte, error) {
if d.ref != nil {
return d.ref.Request(url, method, callback, resp)
}
Expand All @@ -152,7 +152,7 @@ func (d *Onedrive) Request(url string, method string, callback base.ReqCallback,
return nil, err
}
if e.Error.Code != "" {
if e.Error.Code == "InvalidAuthenticationToken" {
if e.Error.Code == "InvalidAuthenticationToken" && !utils.IsBool(noRetry...) {
err = d.refreshToken()
if err != nil {
return nil, err
Expand Down Expand Up @@ -310,9 +310,36 @@ func (d *Onedrive) getDrive(ctx context.Context) (*DriveResp, error) {
var resp DriveResp
_, err := d.Request(api, http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
}, &resp)
}, &resp, true)
if err != nil {
return nil, err
}
return &resp, nil
}

func (d *Onedrive) getDirectUploadInfo(ctx context.Context, path string) (*model.HttpDirectUploadInfo, error) {
// Create upload session
url := d.GetMetaUrl(false, path) + "/createUploadSession"
metadata := map[string]any{
"item": map[string]any{
"@microsoft.graph.conflictBehavior": "rename",
},
}

res, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(metadata).SetContext(ctx)
}, nil)
if err != nil {
return nil, err
}

uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
if uploadUrl == "" {
return nil, fmt.Errorf("failed to get upload URL from response")
}
return &model.HttpDirectUploadInfo{
UploadURL: uploadUrl,
ChunkSize: d.ChunkSize * 1024 * 1024, // Convert MB to bytes
Method: "PUT",
}, nil
}
14 changes: 14 additions & 0 deletions drivers/onedrive_app/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,18 @@ func (d *OnedriveAPP) GetDetails(ctx context.Context) (*model.StorageDetails, er
}, nil
}

func (d *OnedriveAPP) GetDirectUploadTools() []string {
if !d.EnableDirectUpload {
return nil
}
return []string{"HttpDirect"}
}

func (d *OnedriveAPP) GetDirectUploadInfo(ctx context.Context, _ string, dstDir model.Obj, fileName string, _ int64) (any, error) {
if !d.EnableDirectUpload {
return nil, errs.NotImplement
}
return d.getDirectUploadInfo(ctx, path.Join(dstDir.GetPath(), fileName))
}

var _ driver.Driver = (*OnedriveAPP)(nil)
17 changes: 9 additions & 8 deletions drivers/onedrive_app/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import (

type Addition struct {
driver.RootPath
Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"`
ClientID string `json:"client_id" required:"true"`
ClientSecret string `json:"client_secret" required:"true"`
TenantID string `json:"tenant_id"`
Email string `json:"email"`
ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
DisableDiskUsage bool `json:"disable_disk_usage" default:"false"`
Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"`
ClientID string `json:"client_id" required:"true"`
ClientSecret string `json:"client_secret" required:"true"`
TenantID string `json:"tenant_id"`
Email string `json:"email"`
ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
DisableDiskUsage bool `json:"disable_disk_usage" default:"false"`
EnableDirectUpload bool `json:"enable_direct_upload" default:"false" help:"Enable direct upload from client to OneDrive"`
}

var config = driver.Config{
Expand Down
33 changes: 30 additions & 3 deletions drivers/onedrive_app/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (d *OnedriveAPP) _accessToken() error {
return nil
}

func (d *OnedriveAPP) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
func (d *OnedriveAPP) Request(url string, method string, callback base.ReqCallback, resp interface{}, noRetry ...bool) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
if callback != nil {
Expand All @@ -104,7 +104,7 @@ func (d *OnedriveAPP) Request(url string, method string, callback base.ReqCallba
return nil, err
}
if e.Error.Code != "" {
if e.Error.Code == "InvalidAuthenticationToken" {
if e.Error.Code == "InvalidAuthenticationToken" && !utils.IsBool(noRetry...) {
err = d.accessToken()
if err != nil {
return nil, err
Expand Down Expand Up @@ -216,9 +216,36 @@ func (d *OnedriveAPP) getDrive(ctx context.Context) (*DriveResp, error) {
var resp DriveResp
_, err := d.Request(api, http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
}, &resp)
}, &resp, true)
if err != nil {
return nil, err
}
return &resp, nil
}

func (d *OnedriveAPP) getDirectUploadInfo(ctx context.Context, path string) (*model.HttpDirectUploadInfo, error) {
// Create upload session
url := d.GetMetaUrl(false, path) + "/createUploadSession"
metadata := map[string]any{
"item": map[string]any{
"@microsoft.graph.conflictBehavior": "rename",
},
}

res, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(metadata).SetContext(ctx)
}, nil)
if err != nil {
return nil, err
}

uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
if uploadUrl == "" {
return nil, fmt.Errorf("failed to get upload URL from response")
}
return &model.HttpDirectUploadInfo{
UploadURL: uploadUrl,
ChunkSize: d.ChunkSize * 1024 * 1024, // Convert MB to bytes
Method: "PUT",
}, nil
}
9 changes: 9 additions & 0 deletions internal/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,12 @@ type LinkCacheModeResolver interface {
// ResolveLinkCacheMode returns the LinkCacheMode for the given path.
ResolveLinkCacheMode(path string) LinkCacheMode
}

type DirectUploader interface {
// GetDirectUploadTools returns available frontend-direct upload tools
GetDirectUploadTools() []string
// GetDirectUploadInfo returns the information needed for direct upload from client to storage
// actualPath is the path relative to the storage root (after removing mount path prefix)
// return errs.NotImplement if the driver does not support the given direct upload tool
GetDirectUploadInfo(ctx context.Context, tool string, dstDir model.Obj, fileName string, fileSize int64) (any, error)
}
7 changes: 4 additions & 3 deletions internal/errs/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import (
)

var (
ObjectNotFound = errors.New("object not found")
NotFolder = errors.New("not a folder")
NotFile = errors.New("not a file")
ObjectNotFound = errors.New("object not found")
ObjectAlreadyExists = errors.New("object already exists")
NotFolder = errors.New("not a folder")
NotFile = errors.New("not a file")
)

func IsObjectNotFound(err error) bool {
Expand Down
16 changes: 16 additions & 0 deletions internal/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ func GetStorage(path string, args *GetStoragesArgs) (driver.Driver, error) {
return storageDriver, nil
}

func GetStorageAndActualPath(path string) (driver.Driver, string, error) {
return op.GetStorageAndActualPath(path)
}

func GetByActualPath(ctx context.Context, storage driver.Driver, actualPath string) (model.Obj, error) {
return op.Get(ctx, storage, actualPath)
}

func Other(ctx context.Context, args model.FsOtherArgs) (interface{}, error) {
res, err := other(ctx, args)
if err != nil {
Expand All @@ -190,3 +198,11 @@ func PutURL(ctx context.Context, path, dstName, urlStr string) error {
}
return op.PutURL(ctx, storage, dstDirActualPath, dstName, urlStr)
}

func GetDirectUploadInfo(ctx context.Context, tool, path, dstName string, fileSize int64) (any, error) {
info, err := getDirectUploadInfo(ctx, tool, path, dstName, fileSize)
if err != nil {
log.Errorf("failed get %s direct upload info for %s(%d bytes): %+v", path, dstName, fileSize, err)
}
return info, err
}
8 changes: 8 additions & 0 deletions internal/fs/put.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,11 @@ func putDirectly(ctx context.Context, dstDirPath string, file model.FileStreamer
}
return op.Put(ctx, storage, dstDirActualPath, file, nil, lazyCache...)
}

func getDirectUploadInfo(ctx context.Context, tool, dstDirPath, dstName string, fileSize int64) (any, error) {
storage, dstDirActualPath, err := op.GetStorageAndActualPath(dstDirPath)
if err != nil {
return nil, errors.WithMessage(err, "failed get storage")
}
return op.GetDirectUploadInfo(ctx, tool, storage, dstDirActualPath, dstName, fileSize)
}
8 changes: 8 additions & 0 deletions internal/model/direct_upload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package model

type HttpDirectUploadInfo struct {
UploadURL string `json:"upload_url"` // The URL to upload the file
ChunkSize int64 `json:"chunk_size"` // The chunk size for uploading, 0 means no chunking required
Headers map[string]string `json:"headers,omitempty"` // Optional headers to include in the upload request
Method string `json:"method,omitempty"` // HTTP method, default is PUT
}
48 changes: 44 additions & 4 deletions internal/op/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,15 +568,15 @@ func PutURL(ctx context.Context, storage driver.Driver, dstDirPath, dstName, url
dstPath := stdpath.Join(dstDirPath, dstName)
_, err := GetUnwrap(ctx, storage, dstPath)
if err == nil {
return errors.New("obj already exists")
return errors.WithStack(errs.ObjectAlreadyExists)
}
err = MakeDir(ctx, storage, dstDirPath)
if err != nil {
return errors.WithMessagef(err, "failed to put url")
return errors.WithMessagef(err, "failed to make dir [%s]", dstDirPath)
}
dstDir, err := GetUnwrap(ctx, storage, dstDirPath)
if err != nil {
return errors.WithMessagef(err, "failed to put url")
return errors.WithMessagef(err, "failed to get dir [%s]", dstDirPath)
}
switch s := storage.(type) {
case driver.PutURLResult:
Expand All @@ -599,8 +599,48 @@ func PutURL(ctx context.Context, storage driver.Driver, dstDirPath, dstName, url
}
}
default:
return errs.NotImplement
return errors.WithStack(errs.NotImplement)
}
log.Debugf("put url [%s](%s) done", dstName, url)
return errors.WithStack(err)
}

func GetDirectUploadTools(storage driver.Driver) []string {
du, ok := storage.(driver.DirectUploader)
if !ok {
return nil
}
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
return nil
}
return du.GetDirectUploadTools()
}

func GetDirectUploadInfo(ctx context.Context, tool string, storage driver.Driver, dstDirPath, dstName string, fileSize int64) (any, error) {
du, ok := storage.(driver.DirectUploader)
if !ok {
return nil, errors.WithStack(errs.NotImplement)
}
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
return nil, errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
dstDirPath = utils.FixAndCleanPath(dstDirPath)
dstPath := stdpath.Join(dstDirPath, dstName)
_, err := GetUnwrap(ctx, storage, dstPath)
if err == nil {
return nil, errors.WithStack(errs.ObjectAlreadyExists)
}
err = MakeDir(ctx, storage, dstDirPath)
if err != nil {
return nil, errors.WithMessagef(err, "failed to make dir [%s]", dstDirPath)
}
dstDir, err := GetUnwrap(ctx, storage, dstDirPath)
if err != nil {
return nil, errors.WithMessagef(err, "failed to get dir [%s]", dstDirPath)
}
info, err := du.GetDirectUploadInfo(ctx, tool, dstDir, dstName, fileSize)
if err != nil {
return nil, errors.WithStack(err)
}
return info, nil
}
4 changes: 2 additions & 2 deletions server/ftp/afero.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (a *AferoAdapter) GetHandle(name string, flags int, offset int64) (ftpserve
return nil, err
}
if (flags & os.O_EXCL) != 0 {
return nil, errors.New("file already exists")
return nil, errs.ObjectAlreadyExists
}
if (flags & os.O_WRONLY) != 0 {
return nil, errors.New("cannot write to uploading file")
Expand All @@ -122,7 +122,7 @@ func (a *AferoAdapter) GetHandle(name string, flags int, offset int64) (ftpserve
return nil, errs.ObjectNotFound
}
if (flags&os.O_EXCL) != 0 && exists {
return nil, errors.New("file already exists")
return nil, errs.ObjectAlreadyExists
}
if (flags & os.O_WRONLY) != 0 {
if offset != 0 {
Expand Down
54 changes: 54 additions & 0 deletions server/handles/direct_upload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package handles

import (
"net/url"

"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/fs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/server/common"
"github.com/gin-gonic/gin"
)

type FsGetDirectUploadInfoReq struct {
Path string `json:"path" form:"path"`
FileName string `json:"file_name" form:"file_name"`
FileSize int64 `json:"file_size" form:"file_size"`
Tool string `json:"tool" form:"tool"`
}

// FsGetDirectUploadInfo returns the direct upload info if supported by the driver
// If the driver does not support direct upload, returns null for upload_info
func FsGetDirectUploadInfo(c *gin.Context) {
var req FsGetDirectUploadInfoReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
// Decode path
path, err := url.PathUnescape(req.Path)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
// Get user and join path
user := c.Request.Context().Value(conf.UserKey).(*model.User)
path, err = user.JoinPath(path)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
overwrite := c.GetHeader("Overwrite") != "false"
if !overwrite {
if res, _ := fs.Get(c.Request.Context(), path, &fs.GetArgs{NoLog: true}); res != nil {
common.ErrorStrResp(c, "file exists", 403)
return
}
}
directUploadInfo, err := fs.GetDirectUploadInfo(c, req.Tool, path, req.FileName, req.FileSize)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c, directUploadInfo)
}
Loading