diff --git a/cmd/common.go b/cmd/common.go index 8a73f9b0..624622fb 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -20,6 +20,7 @@ func Init() { bootstrap.InitStreamLimit() bootstrap.InitIndex() bootstrap.InitUpgradePatch() + bootstrap.InitUsage() } func Release() { diff --git a/internal/bootstrap/data/setting.go b/internal/bootstrap/data/setting.go index 407a5c64..a9c37cf1 100644 --- a/internal/bootstrap/data/setting.go +++ b/internal/bootstrap/data/setting.go @@ -160,6 +160,8 @@ func InitialSettings() []model.SettingItem { {Key: conf.ForwardDirectLinkParams, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL}, {Key: conf.IgnoreDirectLinkParams, Value: "sign,alist_ts", Type: conf.TypeString, Group: model.GLOBAL}, {Key: conf.WebauthnLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC}, + {Key: conf.GlobalStorageSize, Value: "-1", Type: conf.TypeNumber, Group: model.GLOBAL, Flag: model.PUBLIC}, + {Key: conf.UsageScanInterval, Value: "3600", Type: conf.TypeNumber, Group: model.GLOBAL, Flag: model.PRIVATE}, // single settings {Key: conf.Token, Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE}, diff --git a/internal/bootstrap/usage.go b/internal/bootstrap/usage.go new file mode 100644 index 00000000..527e38d5 --- /dev/null +++ b/internal/bootstrap/usage.go @@ -0,0 +1,12 @@ +package bootstrap + +import ( + "github.com/alist-org/alist/v3/internal/usage" + log "github.com/sirupsen/logrus" +) + +// InitUsage 初始化使用量统计 +func InitUsage() { + log.Info("init usage calculation") + usage.Init() +} diff --git a/internal/conf/const.go b/internal/conf/const.go index 5cb8d850..754f303a 100644 --- a/internal/conf/const.go +++ b/internal/conf/const.go @@ -45,6 +45,8 @@ const ( ForwardDirectLinkParams = "forward_direct_link_params" IgnoreDirectLinkParams = "ignore_direct_link_params" WebauthnLoginEnabled = "webauthn_login_enabled" + GlobalStorageSize = "global_storage_size" + UsageScanInterval = "usage_scan_interval" // index SearchIndex = "search_index" diff --git a/internal/model/storage.go b/internal/model/storage.go index e3c7e1f9..b5af4f4b 100644 --- a/internal/model/storage.go +++ b/internal/model/storage.go @@ -1,9 +1,71 @@ package model import ( + "fmt" "time" ) +type Size int64 + +func formatSize(size int64) string { + if size < 0 { + return "Unknown" + } + units := []string{"B", "KB", "MB", "GB", "TB", "PB"} + index := 0 + fsize := float64(size) + for fsize > 1024 && index < len(units)-1 { + fsize /= 1024 + index++ + } + return fmt.Sprintf("%.2f %s", fsize, units[index]) +} + +func (s Size) String() string { + return formatSize(int64(s)) +} + +// Usage 存储使用量信息 +type Usage struct { + Available int64 `json:"available"` + Used int64 `json:"used"` + Total int64 `json:"total"` +} + +// NewEmptyUsage 创建一个新的未知使用量信息(无限容量) +func NewEmptyUsage() *Usage { + return &Usage{ + Available: -1, + Used: 0, + Total: -1, + } +} + +// SetTotalGB 设置总容量(GB单位) +func (u *Usage) SetTotalGB(totalGB int64) { + if totalGB <= 0 { + u.Total = -1 + u.Available = -1 + return + } + u.Total = totalGB * 1024 * 1024 * 1024 + u.Available = u.Total - u.Used + if u.Available < 0 { + u.Available = 0 + } +} + +// AddUsed 增加已使用容量 +func (u *Usage) AddUsed(size int64) { + u.Used += size + if u.Total > 0 { + u.Available = u.Total - u.Used + if u.Available < 0 { + u.Available = 0 + } + } +} + type Storage struct { ID uint `json:"id" gorm:"primaryKey"` // unique key MountPath string `json:"mount_path" gorm:"unique" binding:"required"` // must be standardized @@ -57,3 +119,6 @@ func (p Proxy) WebdavProxy() bool { func (p Proxy) WebdavNative() bool { return !p.Webdav302() && !p.WebdavProxy() } + +type MountedStorage struct { +} diff --git a/internal/search/search.go b/internal/search/search.go index d420eb16..aad68d82 100644 --- a/internal/search/search.go +++ b/internal/search/search.go @@ -51,6 +51,27 @@ func Search(ctx context.Context, req model.SearchReq) ([]model.SearchNode, int64 return instance.Search(ctx, req) } +// GetAllNodes 获取所有索引的节点 +func GetAllNodes(ctx context.Context) ([]model.SearchNode, error) { + if instance == nil { + return nil, errs.SearchNotAvailable + } + + // 使用一个空白的搜索请求,获取所有文件 + req := model.SearchReq{ + Parent: "", + Keywords: "", + Scope: 0, // 所有类型 + PageReq: model.PageReq{ + Page: 1, + PerPage: model.MaxInt, // 获取所有结果 + }, + } + + nodes, _, err := instance.Search(ctx, req) + return nodes, err +} + func Index(ctx context.Context, parent string, obj model.Obj) error { if instance == nil { return errs.SearchNotAvailable diff --git a/internal/usage/usage.go b/internal/usage/usage.go new file mode 100644 index 00000000..4dc30b91 --- /dev/null +++ b/internal/usage/usage.go @@ -0,0 +1,134 @@ +package usage + +import ( + "context" + "sync" + "time" + + "github.com/alist-org/alist/v3/internal/conf" + "github.com/alist-org/alist/v3/internal/model" + "github.com/alist-org/alist/v3/internal/search" + "github.com/alist-org/alist/v3/internal/setting" + "github.com/alist-org/alist/v3/pkg/utils" + log "github.com/sirupsen/logrus" +) + +var ( + globalUsage = model.NewEmptyUsage() + usageLock = &sync.RWMutex{} + lastScanTime = time.Time{} + isScanning = false + scanningLock = &sync.Mutex{} +) + +// GetGlobalUsage 获取全局使用量信息 +func GetGlobalUsage() *model.Usage { + usageLock.RLock() + defer usageLock.RUnlock() + return globalUsage +} + +// GetGlobalStorageSizeGB 获取全局存储容量(GB) +func GetGlobalStorageSizeGB() int64 { + sizeStr := setting.GetStr(conf.GlobalStorageSize) + size, err := utils.ParseInt(sizeStr) + if err != nil || size <= -2 { + // 如果解析失败或值非法,使用默认值 -1 + log.Warnf("invalid global storage size: %s, using default -1", sizeStr) + return -1 + } + return size +} + +// GetScanInterval 获取扫描间隔(秒) +func GetScanInterval() int64 { + intervalStr := setting.GetStr(conf.UsageScanInterval) + interval, err := utils.ParseInt(intervalStr) + if err != nil || interval < 0 { + // 如果解析失败或值为负数,使用默认值 3600 + log.Warnf("invalid scan interval: %s, using default 3600", intervalStr) + return 3600 + } + return interval +} + +// ScanUsageIfNeeded 如果需要则扫描使用量 +func ScanUsageIfNeeded() { + // 如果全局容量设置为 -1 或 0,则不扫描 + globalSize := GetGlobalStorageSizeGB() + if globalSize <= 0 { + return + } + + // 检查是否超过扫描间隔 + scanInterval := GetScanInterval() + now := time.Now() + + scanningLock.Lock() + if isScanning || now.Sub(lastScanTime).Seconds() < float64(scanInterval) { + scanningLock.Unlock() + return + } + isScanning = true + scanningLock.Unlock() + + go func() { + defer func() { + scanningLock.Lock() + isScanning = false + lastScanTime = time.Now() + scanningLock.Unlock() + }() + + log.Infof("start scanning usage") + ctx := context.Background() + size, err := CalculateUsage(ctx) + if err != nil { + log.Errorf("calculate usage error: %+v", err) + return + } + + usageLock.Lock() + globalUsage.Used = size + globalUsage.SetTotalGB(globalSize) + usageLock.Unlock() + + log.Infof("usage scan completed: used %s, total %s", + model.Size(size).String(), + model.Size(globalUsage.Total).String()) + }() +} + +// CalculateUsage 计算所有存储的使用量 +func CalculateUsage(ctx context.Context) (int64, error) { + // 使用搜索来计算总大小 + var totalSize int64 = 0 + + // 获取所有文件信息 + nodes, err := search.GetAllNodes(ctx) + if err != nil { + return 0, err + } + + // 累计文件大小 + for _, node := range nodes { + if !node.IsDir { + totalSize += node.Size + } + } + + return totalSize, nil +} + +// Init 初始化使用量模块 +func Init() { + globalSize := GetGlobalStorageSizeGB() + usageLock.Lock() + globalUsage.SetTotalGB(globalSize) + usageLock.Unlock() + + // 初始进行一次扫描 + if globalSize > 0 { + go ScanUsageIfNeeded() + } +} diff --git a/pkg/utils/convert.go b/pkg/utils/convert.go new file mode 100644 index 00000000..e1e6aa0e --- /dev/null +++ b/pkg/utils/convert.go @@ -0,0 +1,10 @@ +package utils + +import ( + "strconv" +) + +// ParseInt 将字符串解析为int64 +func ParseInt(s string) (int64, error) { + return strconv.ParseInt(s, 10, 64) +} diff --git a/server/webdav.go b/server/webdav.go index a735e285..5e0bfc14 100644 --- a/server/webdav.go +++ b/server/webdav.go @@ -13,6 +13,7 @@ import ( "github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/op" "github.com/alist-org/alist/v3/internal/setting" + "github.com/alist-org/alist/v3/internal/usage" "github.com/alist-org/alist/v3/server/webdav" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" @@ -44,6 +45,7 @@ func WebDav(dav *gin.RouterGroup) { } func ServeWebDAV(c *gin.Context) { + usage.ScanUsageIfNeeded() user := c.MustGet("user").(*model.User) ctx := context.WithValue(c.Request.Context(), "user", user) handler.ServeHTTP(c.Writer, c.Request.WithContext(ctx)) diff --git a/server/webdav/prop.go b/server/webdav/prop.go index a81f31b0..47f52df7 100644 --- a/server/webdav/prop.go +++ b/server/webdav/prop.go @@ -18,6 +18,7 @@ import ( "time" "github.com/alist-org/alist/v3/internal/model" + "github.com/alist-org/alist/v3/internal/usage" "github.com/alist-org/alist/v3/server/common" ) @@ -165,6 +166,14 @@ var liveProps = map[xml.Name]struct { findFn: findChecksums, dir: false, }, + {Space: "DAV:", Local: "quota-available-bytes"}: { + findFn: findQuotaAvailable, + dir: true, + }, + {Space: "DAV:", Local: "quota-used-bytes"}: { + findFn: findQuotaUsed, + dir: true, + }, } // TODO(nigeltao) merge props and allprop? @@ -496,3 +505,18 @@ func findChecksums(ctx context.Context, ls LockSystem, name string, fi model.Obj } return checksums, nil } + +func findQuotaAvailable(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) { + globalUsage := usage.GetGlobalUsage() + if globalUsage.Available < 0 { + // 如果是无限空间,返回一个大数值 + return "9223372036854775807", nil // maxInt64 + } + + return strconv.FormatInt(globalUsage.Available, 10), nil +} + +func findQuotaUsed(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) { + globalUsage := usage.GetGlobalUsage() + return strconv.FormatInt(globalUsage.Used, 10), nil +}