Skip to content
Closed
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
1 change: 1 addition & 0 deletions cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func Init() {
bootstrap.InitStreamLimit()
bootstrap.InitIndex()
bootstrap.InitUpgradePatch()
bootstrap.InitUsage()
}

func Release() {
Expand Down
2 changes: 2 additions & 0 deletions internal/bootstrap/data/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
12 changes: 12 additions & 0 deletions internal/bootstrap/usage.go
Original file line number Diff line number Diff line change
@@ -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()
}
2 changes: 2 additions & 0 deletions internal/conf/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
65 changes: 65 additions & 0 deletions internal/model/storage.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -57,3 +119,6 @@ func (p Proxy) WebdavProxy() bool {
func (p Proxy) WebdavNative() bool {
return !p.Webdav302() && !p.WebdavProxy()
}

type MountedStorage struct {
}
21 changes: 21 additions & 0 deletions internal/search/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
134 changes: 134 additions & 0 deletions internal/usage/usage.go
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 0 deletions pkg/utils/convert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package utils

import (
"strconv"
)

// ParseInt 将字符串解析为int64
func ParseInt(s string) (int64, error) {
return strconv.ParseInt(s, 10, 64)
}
2 changes: 2 additions & 0 deletions server/webdav.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))
Expand Down
24 changes: 24 additions & 0 deletions server/webdav/prop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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
}
Loading