Merge a4da4f92fe409d01946d9d51cd4b666dca844fca into 41bdab49aa8acca9e88862c3db55cd7a8a84ba6a

This commit is contained in:
Eternal Future 2025-04-19 14:35:30 +08:00 committed by GitHub
commit 4abfa0e552
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 273 additions and 0 deletions

View File

@ -20,6 +20,7 @@ func Init() {
bootstrap.InitStreamLimit()
bootstrap.InitIndex()
bootstrap.InitUpgradePatch()
bootstrap.InitUsage()
}
func Release() {

View File

@ -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},

View File

@ -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()
}

View File

@ -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"

View File

@ -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 {
}

View File

@ -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

134
internal/usage/usage.go Normal file
View File

@ -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()
}
}

10
pkg/utils/convert.go Normal file
View File

@ -0,0 +1,10 @@
package utils
import (
"strconv"
)
// ParseInt 将字符串解析为int64
func ParseInt(s string) (int64, error) {
return strconv.ParseInt(s, 10, 64)
}

View File

@ -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))

View File

@ -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
}