feat(style): add driver icons and disk usage (#1274)

* feat(style): add driver icons and disk usage

* feat(driver): add disk usage for 115_open, 123_open, aliyundrive_open and baidu_netdisk

* feat(driver): add disk usage for crypt, sftp and smb

* chore: clean unused variable

* feat(driver): add disk usage for cloudreve_v4

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

* fix(local): disk label check when getting disk usage

* feat(style): return details when accessing the manage page

---------

Signed-off-by: MadDogOwner <xiaoran@xrgzs.top>
Co-authored-by: MadDogOwner <xiaoran@xrgzs.top>
This commit is contained in:
KirCute
2025-09-19 11:59:11 +08:00
committed by GitHub
parent d3bc6321f4
commit cc16cb35bf
26 changed files with 428 additions and 81 deletions

View File

@ -337,6 +337,27 @@ func (d *Open115) OfflineList(ctx context.Context) (*sdk.OfflineTaskListResp, er
return resp, nil return resp, nil
} }
func (d *Open115) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
userInfo, err := d.client.UserInfo(ctx)
if err != nil {
return nil, err
}
total, err := userInfo.RtSpaceInfo.AllTotal.Size.Int64()
if err != nil {
return nil, err
}
free, err := userInfo.RtSpaceInfo.AllRemain.Size.Int64()
if err != nil {
return nil, err
}
return &model.StorageDetails{
DiskUsage: model.DiskUsage{
TotalSpace: uint64(total),
FreeSpace: uint64(free),
},
}, nil
}
// func (d *Open115) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) { // func (d *Open115) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
// // TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional // // TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional
// return nil, errs.NotImplement // return nil, errs.NotImplement

View File

@ -214,5 +214,20 @@ func (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
return nil, fmt.Errorf("upload complete timeout") return nil, fmt.Errorf("upload complete timeout")
} }
func (d *Open123) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
userInfo, err := d.getUserInfo()
if err != nil {
return nil, err
}
total := userInfo.Data.SpacePermanent + userInfo.Data.SpaceTemp
free := total - userInfo.Data.SpaceUsed
return &model.StorageDetails{
DiskUsage: model.DiskUsage{
TotalSpace: total,
FreeSpace: free,
},
}, nil
}
var _ driver.Driver = (*Open123)(nil) var _ driver.Driver = (*Open123)(nil)
var _ driver.PutResult = (*Open123)(nil) var _ driver.PutResult = (*Open123)(nil)

View File

@ -133,9 +133,9 @@ type UserInfoResp struct {
// HeadImage string `json:"headImage"` // HeadImage string `json:"headImage"`
// Passport string `json:"passport"` // Passport string `json:"passport"`
// Mail string `json:"mail"` // Mail string `json:"mail"`
// SpaceUsed int64 `json:"spaceUsed"` SpaceUsed uint64 `json:"spaceUsed"`
// SpacePermanent int64 `json:"spacePermanent"` SpacePermanent uint64 `json:"spacePermanent"`
// SpaceTemp int64 `json:"spaceTemp"` SpaceTemp uint64 `json:"spaceTemp"`
// SpaceTempExpr int64 `json:"spaceTempExpr"` // SpaceTempExpr int64 `json:"spaceTempExpr"`
// Vip bool `json:"vip"` // Vip bool `json:"vip"`
// DirectTraffic int64 `json:"directTraffic"` // DirectTraffic int64 `json:"directTraffic"`

View File

@ -291,6 +291,21 @@ func (d *AliyundriveOpen) Other(ctx context.Context, args model.OtherArgs) (inte
return resp, nil return resp, nil
} }
func (d *AliyundriveOpen) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
res, err := d.request(ctx, limiterOther, "/adrive/v1.0/user/getSpaceInfo", http.MethodPost, nil)
if err != nil {
return nil, err
}
total := utils.Json.Get(res, "personal_space_info", "total_size").ToUint64()
used := utils.Json.Get(res, "personal_space_info", "used_size").ToUint64()
return &model.StorageDetails{
DiskUsage: model.DiskUsage{
TotalSpace: total,
FreeSpace: total - used,
},
}, nil
}
var _ driver.Driver = (*AliyundriveOpen)(nil) var _ driver.Driver = (*AliyundriveOpen)(nil)
var _ driver.MkdirResult = (*AliyundriveOpen)(nil) var _ driver.MkdirResult = (*AliyundriveOpen)(nil)
var _ driver.MoveResult = (*AliyundriveOpen)(nil) var _ driver.MoveResult = (*AliyundriveOpen)(nil)

View File

@ -364,4 +364,12 @@ func (d *BaiduNetdisk) uploadSlice(ctx context.Context, params map[string]string
return nil return nil
} }
func (d *BaiduNetdisk) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
du, err := d.quota()
if err != nil {
return nil, err
}
return &model.StorageDetails{DiskUsage: *du}, nil
}
var _ driver.Driver = (*BaiduNetdisk)(nil) var _ driver.Driver = (*BaiduNetdisk)(nil)

View File

@ -189,3 +189,12 @@ type PrecreateResp struct {
// return_type=2 // return_type=2
File File `json:"info"` File File `json:"info"`
} }
type QuotaResp struct {
Errno int `json:"errno"`
RequestId int64 `json:"request_id"`
Total uint64 `json:"total"`
Used uint64 `json:"used"`
//Free uint64 `json:"free"`
//Expire bool `json:"expire"`
}

View File

@ -381,6 +381,18 @@ func (d *BaiduNetdisk) getSliceSize(filesize int64) int64 {
return maxSliceSize return maxSliceSize
} }
func (d *BaiduNetdisk) quota() (*model.DiskUsage, error) {
var resp QuotaResp
_, err := d.request("https://pan.baidu.com/api/quota", http.MethodGet, nil, &resp)
if err != nil {
return nil, err
}
return &model.DiskUsage{
TotalSpace: resp.Total,
FreeSpace: resp.Total - resp.Used,
}, nil
}
// func encodeURIComponent(str string) string { // func encodeURIComponent(str string) string {
// r := url.QueryEscape(str) // r := url.QueryEscape(str)
// r = strings.ReplaceAll(r, "+", "%20") // r = strings.ReplaceAll(r, "+", "%20")

View File

@ -339,6 +339,21 @@ func (d *CloudreveV4) ArchiveDecompress(ctx context.Context, srcObj, dstDir mode
return nil, errs.NotImplement return nil, errs.NotImplement
} }
func (d *CloudreveV4) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
// TODO return storage details (total space, free space, etc.)
var r CapacityResp
err := d.request(http.MethodGet, "/user/capacity", nil, &r)
if err != nil {
return nil, err
}
return &model.StorageDetails{
DiskUsage: model.DiskUsage{
TotalSpace: r.Total,
FreeSpace: r.Total - r.Used,
},
}, nil
}
//func (d *CloudreveV4) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { //func (d *CloudreveV4) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport // return nil, errs.NotSupport
//} //}

View File

@ -204,3 +204,9 @@ type FolderSummaryResp struct {
CalculatedAt time.Time `json:"calculated_at"` CalculatedAt time.Time `json:"calculated_at"`
} `json:"folder_summary"` } `json:"folder_summary"`
} }
type CapacityResp struct {
Total uint64 `json:"total"`
Used uint64 `json:"used"`
// StoragePackTotal uint64 `json:"storage_pack_total"`
}

View File

@ -411,6 +411,20 @@ func (d *Crypt) Put(ctx context.Context, dstDir model.Obj, streamer model.FileSt
return nil return nil
} }
func (d *Crypt) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
wd, ok := d.remoteStorage.(driver.WithDetails)
if !ok {
return nil, errs.NotImplement
}
remoteDetails, err := wd.GetDetails(ctx)
if err != nil {
return nil, err
}
return &model.StorageDetails{
DiskUsage: remoteDetails.DiskUsage,
}, nil
}
//func (d *Safe) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { //func (d *Safe) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport // return nil, errs.NotSupport
//} //}

View File

@ -434,4 +434,14 @@ func (d *Local) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
return nil return nil
} }
func (d *Local) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
du, err := getDiskUsage(d.RootFolderPath)
if err != nil {
return nil, err
}
return &model.StorageDetails{
DiskUsage: du,
}, nil
}
var _ driver.Driver = (*Local)(nil) var _ driver.Driver = (*Local)(nil)

View File

@ -5,8 +5,25 @@ package local
import ( import (
"io/fs" "io/fs"
"strings" "strings"
"syscall"
"github.com/OpenListTeam/OpenList/v4/internal/model"
) )
func isHidden(f fs.FileInfo, _ string) bool { func isHidden(f fs.FileInfo, _ string) bool {
return strings.HasPrefix(f.Name(), ".") return strings.HasPrefix(f.Name(), ".")
} }
func getDiskUsage(path string) (model.DiskUsage, error) {
var stat syscall.Statfs_t
err := syscall.Statfs(path, &stat)
if err != nil {
return model.DiskUsage{}, err
}
total := stat.Blocks * uint64(stat.Bsize)
free := stat.Bfree * uint64(stat.Bsize)
return model.DiskUsage{
TotalSpace: total,
FreeSpace: free,
}, nil
}

View File

@ -1,22 +1,51 @@
//go:build windows //go:build windows
package local package local
import ( import (
"io/fs" "errors"
"path/filepath" "io/fs"
"syscall" "path/filepath"
) "syscall"
func isHidden(f fs.FileInfo, fullPath string) bool { "github.com/OpenListTeam/OpenList/v4/internal/model"
filePath := filepath.Join(fullPath, f.Name()) "golang.org/x/sys/windows"
namePtr, err := syscall.UTF16PtrFromString(filePath) )
if err != nil {
return false func isHidden(f fs.FileInfo, fullPath string) bool {
} filePath := filepath.Join(fullPath, f.Name())
attrs, err := syscall.GetFileAttributes(namePtr) namePtr, err := syscall.UTF16PtrFromString(filePath)
if err != nil { if err != nil {
return false return false
} }
return attrs&syscall.FILE_ATTRIBUTE_HIDDEN != 0 attrs, err := syscall.GetFileAttributes(namePtr)
} if err != nil {
return false
}
return attrs&syscall.FILE_ATTRIBUTE_HIDDEN != 0
}
func getDiskUsage(path string) (model.DiskUsage, error) {
abs, err := filepath.Abs(path)
if err != nil {
return model.DiskUsage{}, err
}
root := filepath.VolumeName(abs)
if len(root) != 2 || root[1] != ':' {
return model.DiskUsage{}, errors.New("cannot get disk label")
}
var freeBytes, totalBytes, totalFreeBytes uint64
err = windows.GetDiskFreeSpaceEx(
windows.StringToUTF16Ptr(root),
&freeBytes,
&totalBytes,
&totalFreeBytes,
)
if err != nil {
return model.DiskUsage{}, err
}
return model.DiskUsage{
TotalSpace: totalBytes,
FreeSpace: freeBytes,
}, nil
}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"os" "os"
"path" "path"
"strings"
"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/errs"
@ -127,4 +128,22 @@ func (d *SFTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStrea
return err return err
} }
func (d *SFTP) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
stat, err := d.client.StatVFS(d.RootFolderPath)
if err != nil {
if strings.Contains(err.Error(), "unimplemented") {
return nil, errs.NotImplement
}
return nil, err
}
total := stat.Blocks * stat.Bsize
free := stat.Bfree * stat.Bsize
return &model.StorageDetails{
DiskUsage: model.DiskUsage{
TotalSpace: total,
FreeSpace: free,
},
}, nil
}
var _ driver.Driver = (*SFTP)(nil) var _ driver.Driver = (*SFTP)(nil)

View File

@ -205,6 +205,22 @@ func (d *SMB) Put(ctx context.Context, dstDir model.Obj, stream model.FileStream
return nil return nil
} }
func (d *SMB) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
if err := d.checkConn(); err != nil {
return nil, err
}
stat, err := d.fs.Statfs(d.RootFolderPath)
if err != nil {
return nil, err
}
return &model.StorageDetails{
DiskUsage: model.DiskUsage{
TotalSpace: stat.BlockSize() * stat.TotalBlockCount(),
FreeSpace: stat.BlockSize() * stat.AvailableBlockCount(),
},
}, nil
}
//func (d *SMB) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { //func (d *SMB) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport // return nil, errs.NotSupport
//} //}

View File

@ -93,6 +93,11 @@ func (d *Template) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.O
return nil, errs.NotImplement return nil, errs.NotImplement
} }
func (d *Template) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
// TODO return storage details (total space, free space, etc.)
return nil, errs.NotImplement
}
//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { //func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport // return nil, errs.NotSupport
//} //}

View File

@ -114,6 +114,7 @@ func InitialSettings() []model.SettingItem {
{Key: "share_icon", Value: "🎁", Type: conf.TypeString, Group: model.STYLE}, {Key: "share_icon", Value: "🎁", Type: conf.TypeString, Group: model.STYLE},
{Key: "home_container", Value: "max_980px", Type: conf.TypeSelect, Options: "max_980px,hope_container", Group: model.STYLE}, {Key: "home_container", Value: "max_980px", Type: conf.TypeSelect, Options: "max_980px,hope_container", Group: model.STYLE},
{Key: "settings_layout", Value: "list", Type: conf.TypeSelect, Options: "list,responsive", Group: model.STYLE}, {Key: "settings_layout", Value: "list", Type: conf.TypeSelect, Options: "list,responsive", Group: model.STYLE},
{Key: conf.HideStorageDetails, Value: "false", Type: conf.TypeBool, Group: model.STYLE, Flag: model.PRIVATE},
// preview settings // preview settings
{Key: conf.TextTypes, Value: "txt,htm,html,xml,java,properties,sql,js,md,json,conf,ini,vue,php,py,bat,gitignore,yml,go,sh,c,cpp,h,hpp,tsx,vtt,srt,ass,rs,lrc,strm", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE}, {Key: conf.TextTypes, Value: "txt,htm,html,xml,java,properties,sql,js,md,json,conf,ini,vue,php,py,bat,gitignore,yml,go,sh,c,cpp,h,hpp,tsx,vtt,srt,ass,rs,lrc,strm", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
{Key: conf.AudioTypes, Value: "mp3,flac,ogg,m4a,wav,opus,wma", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE}, {Key: conf.AudioTypes, Value: "mp3,flac,ogg,m4a,wav,opus,wma", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},

View File

@ -17,9 +17,10 @@ const (
AllowMounted = "allow_mounted" AllowMounted = "allow_mounted"
RobotsTxt = "robots_txt" RobotsTxt = "robots_txt"
Logo = "logo" // multi-lines text, L1: light, EOL: dark Logo = "logo" // multi-lines text, L1: light, EOL: dark
Favicon = "favicon" Favicon = "favicon"
MainColor = "main_color" MainColor = "main_color"
HideStorageDetails = "hide_storage_details"
// preview // preview
TextTypes = "text_types" TextTypes = "text_types"

View File

@ -210,6 +210,11 @@ type ArchiveDecompressResult interface {
ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error)
} }
type WithDetails interface {
// GetDetails get storage details (total space, free space, etc.)
GetDetails(ctx context.Context) (*model.StorageDetails, error)
}
type Reference interface { type Reference interface {
InitReference(storage Driver) error InitReference(storage Driver) error
} }

View File

@ -19,8 +19,9 @@ import (
// then pass the actual path to the op package // then pass the actual path to the op package
type ListArgs struct { type ListArgs struct {
Refresh bool Refresh bool
NoLog bool NoLog bool
WithStorageDetails bool
} }
func List(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error) { func List(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error) {
@ -35,11 +36,12 @@ func List(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error)
} }
type GetArgs struct { type GetArgs struct {
NoLog bool NoLog bool
WithStorageDetails bool
} }
func Get(ctx context.Context, path string, args *GetArgs) (model.Obj, error) { func Get(ctx context.Context, path string, args *GetArgs) (model.Obj, error) {
res, err := get(ctx, path) res, err := get(ctx, path, args)
if err != nil { if err != nil {
if !args.NoLog { if !args.NoLog {
log.Warnf("failed get %s: %s", path, err) log.Warnf("failed get %s: %s", path, err)

View File

@ -11,11 +11,11 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func get(ctx context.Context, path string) (model.Obj, error) { func get(ctx context.Context, path string, args *GetArgs) (model.Obj, error) {
path = utils.FixAndCleanPath(path) path = utils.FixAndCleanPath(path)
// maybe a virtual file // maybe a virtual file
if path != "/" { if path != "/" {
virtualFiles := op.GetStorageVirtualFilesByPath(stdpath.Dir(path)) virtualFiles := op.GetStorageVirtualFilesWithDetailsByPath(ctx, stdpath.Dir(path), !args.WithStorageDetails)
for _, f := range virtualFiles { for _, f := range virtualFiles {
if f.GetName() == stdpath.Base(path) { if f.GetName() == stdpath.Base(path) {
return f, nil return f, nil

View File

@ -15,7 +15,7 @@ import (
func list(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error) { func list(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error) {
meta, _ := ctx.Value(conf.MetaKey).(*model.Meta) meta, _ := ctx.Value(conf.MetaKey).(*model.Meta)
user, _ := ctx.Value(conf.UserKey).(*model.User) user, _ := ctx.Value(conf.UserKey).(*model.User)
virtualFiles := op.GetStorageVirtualFilesByPath(path) virtualFiles := op.GetStorageVirtualFilesWithDetailsByPath(ctx, path, !args.WithStorageDetails)
storage, actualPath, err := op.GetStorageAndActualPath(path) storage, actualPath, err := op.GetStorageAndActualPath(path)
if err != nil && len(virtualFiles) == 0 { if err != nil && len(virtualFiles) == 0 {
return nil, errors.WithMessage(err, "failed get storage") return nil, errors.WithMessage(err, "failed get storage")

View File

@ -55,3 +55,40 @@ func (p Proxy) Webdav302() bool {
func (p Proxy) WebdavProxyURL() bool { func (p Proxy) WebdavProxyURL() bool {
return p.WebdavPolicy == "use_proxy_url" return p.WebdavPolicy == "use_proxy_url"
} }
type DiskUsage struct {
TotalSpace uint64 `json:"total_space"`
FreeSpace uint64 `json:"free_space"`
}
type StorageDetails struct {
DiskUsage
}
type StorageDetailsWithName struct {
*StorageDetails
DriverName string `json:"driver_name"`
}
type ObjWithStorageDetails interface {
GetStorageDetails() *StorageDetailsWithName
}
type ObjStorageDetails struct {
Obj
StorageDetailsWithName
}
func (o ObjStorageDetails) GetStorageDetails() *StorageDetailsWithName {
return &o.StorageDetailsWithName
}
func GetStorageDetails(obj Obj) (*StorageDetailsWithName, bool) {
if obj, ok := obj.(ObjWithStorageDetails); ok {
return obj.GetStorageDetails(), true
}
if unwrap, ok := obj.(ObjUnwrap); ok {
return GetStorageDetails(unwrap.Unwrap())
}
return nil, false
}

View File

@ -15,7 +15,6 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/generic_sync" "github.com/OpenListTeam/OpenList/v4/pkg/generic_sync"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
mapset "github.com/deckarep/golang-set/v2"
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -335,6 +334,40 @@ func getStoragesByPath(path string) []driver.Driver {
// for example, there are: /a/b,/a/c,/a/d/e,/a/b.balance1,/av // for example, there are: /a/b,/a/c,/a/d/e,/a/b.balance1,/av
// GetStorageVirtualFilesByPath(/a) => b,c,d // GetStorageVirtualFilesByPath(/a) => b,c,d
func GetStorageVirtualFilesByPath(prefix string) []model.Obj { func GetStorageVirtualFilesByPath(prefix string) []model.Obj {
return getStorageVirtualFilesByPath(prefix, func(_ driver.Driver, obj model.Obj) model.Obj {
return obj
})
}
func GetStorageVirtualFilesWithDetailsByPath(ctx context.Context, prefix string, hideDetails ...bool) []model.Obj {
if utils.IsBool(hideDetails...) {
return GetStorageVirtualFilesByPath(prefix)
}
return getStorageVirtualFilesByPath(prefix, func(d driver.Driver, obj model.Obj) model.Obj {
ret := &model.ObjStorageDetails{
Obj: obj,
StorageDetailsWithName: model.StorageDetailsWithName{
StorageDetails: nil,
DriverName: d.Config().Name,
},
}
storage, ok := d.(driver.WithDetails)
if !ok {
return ret
}
details, err := storage.GetDetails(ctx)
if err != nil {
if !errors.Is(err, errs.NotImplement) {
log.Errorf("failed get %s storage details: %+v", d.GetStorage().MountPath, err)
}
return ret
}
ret.StorageDetails = details
return ret
})
}
func getStorageVirtualFilesByPath(prefix string, rootCallback func(driver.Driver, model.Obj) model.Obj) []model.Obj {
files := make([]model.Obj, 0) files := make([]model.Obj, 0)
storages := storagesMap.Values() storages := storagesMap.Values()
sort.Slice(storages, func(i, j int) bool { sort.Slice(storages, func(i, j int) bool {
@ -345,21 +378,30 @@ func GetStorageVirtualFilesByPath(prefix string) []model.Obj {
}) })
prefix = utils.FixAndCleanPath(prefix) prefix = utils.FixAndCleanPath(prefix)
set := mapset.NewSet[string]() set := make(map[string]int)
for _, v := range storages { for _, v := range storages {
mountPath := utils.GetActualMountPath(v.GetStorage().MountPath) mountPath := utils.GetActualMountPath(v.GetStorage().MountPath)
// Exclude prefix itself and non prefix // Exclude prefix itself and non prefix
if len(prefix) >= len(mountPath) || !utils.IsSubPath(prefix, mountPath) { if len(prefix) >= len(mountPath) || !utils.IsSubPath(prefix, mountPath) {
continue continue
} }
name := strings.SplitN(strings.TrimPrefix(mountPath[len(prefix):], "/"), "/", 2)[0] names := strings.SplitN(strings.TrimPrefix(mountPath[len(prefix):], "/"), "/", 2)
if set.Add(name) { idx, ok := set[names[0]]
files = append(files, &model.Object{ if !ok {
Name: name, set[names[0]] = len(files)
obj := &model.Object{
Name: names[0],
Size: 0, Size: 0,
Modified: v.GetStorage().Modified, Modified: v.GetStorage().Modified,
IsFolder: true, IsFolder: true,
}) }
if len(names) == 1 {
files = append(files, rootCallback(v, obj))
} else {
files = append(files, obj)
}
} else if len(names) == 1 {
files[idx] = rootCallback(v, files[idx])
} }
} }
return files return files

View File

@ -33,18 +33,19 @@ type DirReq struct {
} }
type ObjResp struct { type ObjResp struct {
Id string `json:"id"` Id string `json:"id"`
Path string `json:"path"` Path string `json:"path"`
Name string `json:"name"` Name string `json:"name"`
Size int64 `json:"size"` Size int64 `json:"size"`
IsDir bool `json:"is_dir"` IsDir bool `json:"is_dir"`
Modified time.Time `json:"modified"` Modified time.Time `json:"modified"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
Sign string `json:"sign"` Sign string `json:"sign"`
Thumb string `json:"thumb"` Thumb string `json:"thumb"`
Type int `json:"type"` Type int `json:"type"`
HashInfoStr string `json:"hashinfo"` HashInfoStr string `json:"hashinfo"`
HashInfo map[*utils.HashType]string `json:"hash_info"` HashInfo map[*utils.HashType]string `json:"hash_info"`
MountDetails *model.StorageDetailsWithName `json:"mount_details,omitempty"`
} }
type FsListResp struct { type FsListResp struct {
@ -98,7 +99,10 @@ func FsList(c *gin.Context, req *ListReq, user *model.User) {
common.ErrorStrResp(c, "Refresh without permission", 403) common.ErrorStrResp(c, "Refresh without permission", 403)
return return
} }
objs, err := fs.List(c.Request.Context(), reqPath, &fs.ListArgs{Refresh: req.Refresh}) objs, err := fs.List(c.Request.Context(), reqPath, &fs.ListArgs{
Refresh: req.Refresh,
WithStorageDetails: !user.IsGuest() && !setting.GetBool(conf.HideStorageDetails),
})
if err != nil { if err != nil {
common.ErrorResp(c, err, 500) common.ErrorResp(c, err, 500)
return return
@ -224,19 +228,21 @@ func toObjsResp(objs []model.Obj, parent string, encrypt bool) []ObjResp {
var resp []ObjResp var resp []ObjResp
for _, obj := range objs { for _, obj := range objs {
thumb, _ := model.GetThumb(obj) thumb, _ := model.GetThumb(obj)
mountDetails, _ := model.GetStorageDetails(obj)
resp = append(resp, ObjResp{ resp = append(resp, ObjResp{
Id: obj.GetID(), Id: obj.GetID(),
Path: obj.GetPath(), Path: obj.GetPath(),
Name: obj.GetName(), Name: obj.GetName(),
Size: obj.GetSize(), Size: obj.GetSize(),
IsDir: obj.IsDir(), IsDir: obj.IsDir(),
Modified: obj.ModTime(), Modified: obj.ModTime(),
Created: obj.CreateTime(), Created: obj.CreateTime(),
HashInfoStr: obj.GetHash().String(), HashInfoStr: obj.GetHash().String(),
HashInfo: obj.GetHash().Export(), HashInfo: obj.GetHash().Export(),
Sign: common.Sign(obj, parent, encrypt), Sign: common.Sign(obj, parent, encrypt),
Thumb: thumb, Thumb: thumb,
Type: utils.GetObjType(obj.GetName(), obj.IsDir()), Type: utils.GetObjType(obj.GetName(), obj.IsDir()),
MountDetails: mountDetails,
}) })
} }
return resp return resp
@ -293,7 +299,9 @@ func FsGet(c *gin.Context, req *FsGetReq, user *model.User) {
common.ErrorStrResp(c, "password is incorrect or you have no permission", 403) common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
return return
} }
obj, err := fs.Get(c.Request.Context(), reqPath, &fs.GetArgs{}) obj, err := fs.Get(c.Request.Context(), reqPath, &fs.GetArgs{
WithStorageDetails: !user.IsGuest() && !setting.GetBool(conf.HideStorageDetails),
})
if err != nil { if err != nil {
common.ErrorResp(c, err, 500) common.ErrorResp(c, err, 500)
return return
@ -350,20 +358,22 @@ func FsGet(c *gin.Context, req *FsGetReq, user *model.User) {
} }
parentMeta, _ := op.GetNearestMeta(parentPath) parentMeta, _ := op.GetNearestMeta(parentPath)
thumb, _ := model.GetThumb(obj) thumb, _ := model.GetThumb(obj)
mountDetails, _ := model.GetStorageDetails(obj)
common.SuccessResp(c, FsGetResp{ common.SuccessResp(c, FsGetResp{
ObjResp: ObjResp{ ObjResp: ObjResp{
Id: obj.GetID(), Id: obj.GetID(),
Path: obj.GetPath(), Path: obj.GetPath(),
Name: obj.GetName(), Name: obj.GetName(),
Size: obj.GetSize(), Size: obj.GetSize(),
IsDir: obj.IsDir(), IsDir: obj.IsDir(),
Modified: obj.ModTime(), Modified: obj.ModTime(),
Created: obj.CreateTime(), Created: obj.CreateTime(),
HashInfoStr: obj.GetHash().String(), HashInfoStr: obj.GetHash().String(),
HashInfo: obj.GetHash().Export(), HashInfo: obj.GetHash().Export(),
Sign: common.Sign(obj, parentPath, isEncrypt(meta, reqPath)), Sign: common.Sign(obj, parentPath, isEncrypt(meta, reqPath)),
Type: utils.GetFileType(obj.GetName()), Type: utils.GetFileType(obj.GetName()),
Thumb: thumb, Thumb: thumb,
MountDetails: mountDetails,
}, },
RawURL: rawURL, RawURL: rawURL,
Readme: getReadme(meta, reqPath), Readme: getReadme(meta, reqPath),

View File

@ -3,9 +3,11 @@ package handles
import ( import (
"context" "context"
"strconv" "strconv"
"sync"
"github.com/OpenListTeam/OpenList/v4/internal/conf" "github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/db" "github.com/OpenListTeam/OpenList/v4/internal/db"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/server/common" "github.com/OpenListTeam/OpenList/v4/server/common"
@ -13,6 +15,42 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
type StorageResp struct {
model.Storage
MountDetails *model.StorageDetails `json:"mount_details,omitempty"`
}
func makeStorageResp(c *gin.Context, storages []model.Storage) []*StorageResp {
ret := make([]*StorageResp, len(storages))
var wg sync.WaitGroup
for i, s := range storages {
ret[i] = &StorageResp{
Storage: s,
MountDetails: nil,
}
d, err := op.GetStorageByMountPath(s.MountPath)
if err != nil {
continue
}
wd, ok := d.(driver.WithDetails)
if !ok {
continue
}
wg.Add(1)
go func() {
defer wg.Done()
details, err := wd.GetDetails(c)
if err != nil {
log.Errorf("failed get %s details: %+v", s.MountPath, err)
return
}
ret[i].MountDetails = details
}()
}
wg.Wait()
return ret
}
func ListStorages(c *gin.Context) { func ListStorages(c *gin.Context) {
var req model.PageReq var req model.PageReq
if err := c.ShouldBind(&req); err != nil { if err := c.ShouldBind(&req); err != nil {
@ -27,7 +65,7 @@ func ListStorages(c *gin.Context) {
return return
} }
common.SuccessResp(c, common.PageResp{ common.SuccessResp(c, common.PageResp{
Content: storages, Content: makeStorageResp(c, storages),
Total: total, Total: total,
}) })
} }