Introduces comprehensive quota management with real-time usage tracking, frontend components, backend API endpoints, and TUS upload integration for storage limit enforcement.
162 lines
4.3 KiB
Go
162 lines
4.3 KiB
Go
package users
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/spf13/afero"
|
|
|
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
|
)
|
|
|
|
const (
|
|
// KB represents kilobyte in bytes
|
|
KB = 1024
|
|
// MB represents megabyte in bytes
|
|
MB = 1024 * KB
|
|
// GB represents gigabyte in bytes
|
|
GB = 1024 * MB
|
|
// TB represents terabyte in bytes
|
|
TB = 1024 * GB
|
|
// QuotaCalculationTimeout is the maximum time allowed for quota calculation
|
|
QuotaCalculationTimeout = 30 * time.Second
|
|
)
|
|
|
|
// CalculateUserQuota calculates the total disk usage for a user's scope.
|
|
// It walks the entire directory tree and sums all file sizes.
|
|
func CalculateUserQuota(fs afero.Fs, scope string) (uint64, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), QuotaCalculationTimeout)
|
|
defer cancel()
|
|
|
|
var totalSize uint64
|
|
done := make(chan error, 1)
|
|
|
|
go func() {
|
|
err := afero.Walk(fs, "/", func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
// Skip files/directories that can't be accessed
|
|
return nil
|
|
}
|
|
|
|
// Skip directories, only count files
|
|
if !info.IsDir() {
|
|
totalSize += uint64(info.Size())
|
|
}
|
|
|
|
return nil
|
|
})
|
|
done <- err
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return 0, fmt.Errorf("quota calculation timed out after %v", QuotaCalculationTimeout)
|
|
case err := <-done:
|
|
return totalSize, err
|
|
}
|
|
}
|
|
|
|
// ConvertToBytes converts a value with a unit (KB, MB, GB or TB) to bytes.
|
|
func ConvertToBytes(value float64, unit string) (uint64, error) {
|
|
switch unit {
|
|
case "KB":
|
|
return uint64(value * float64(KB)), nil
|
|
case "MB":
|
|
return uint64(value * float64(MB)), nil
|
|
case "GB":
|
|
return uint64(value * float64(GB)), nil
|
|
case "TB":
|
|
return uint64(value * float64(TB)), nil
|
|
default:
|
|
return 0, fberrors.ErrInvalidQuotaUnit
|
|
}
|
|
}
|
|
|
|
// ConvertFromBytes converts bytes to a value in the specified unit (KB, MB, GB or TB).
|
|
func ConvertFromBytes(bytes uint64, unit string) (float64, error) {
|
|
switch unit {
|
|
case "KB":
|
|
return float64(bytes) / float64(KB), nil
|
|
case "MB":
|
|
return float64(bytes) / float64(MB), nil
|
|
case "GB":
|
|
return float64(bytes) / float64(GB), nil
|
|
case "TB":
|
|
return float64(bytes) / float64(TB), nil
|
|
default:
|
|
return 0, fberrors.ErrInvalidQuotaUnit
|
|
}
|
|
}
|
|
|
|
// FormatQuotaDisplay formats bytes for display, automatically selecting the appropriate unit.
|
|
func FormatQuotaDisplay(bytes uint64) string {
|
|
if bytes >= TB {
|
|
return fmt.Sprintf("%.2f TB", float64(bytes)/float64(TB))
|
|
}
|
|
if bytes >= GB {
|
|
return fmt.Sprintf("%.2f GB", float64(bytes)/float64(GB))
|
|
}
|
|
if bytes >= 1024*1024 {
|
|
return fmt.Sprintf("%.2f MB", float64(bytes)/(1024*1024))
|
|
}
|
|
if bytes >= 1024 {
|
|
return fmt.Sprintf("%.2f KB", float64(bytes)/1024)
|
|
}
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
|
|
// CheckQuotaExceeded checks if a file operation would exceed the user's quota.
|
|
// Returns true if the quota would be exceeded, false otherwise.
|
|
func CheckQuotaExceeded(user *User, additionalSize int64) (bool, uint64, error) {
|
|
// If quota is not set (0) or not enforced, never exceeded
|
|
if user.QuotaLimit == 0 {
|
|
return false, 0, nil
|
|
}
|
|
|
|
// Calculate current usage
|
|
currentUsage, err := CalculateUserQuota(user.Fs, user.Scope)
|
|
if err != nil {
|
|
return false, 0, fmt.Errorf("failed to calculate quota: %w", err)
|
|
}
|
|
|
|
// Check if adding the new file would exceed the limit
|
|
newTotal := currentUsage + uint64(additionalSize)
|
|
exceeded := newTotal > user.QuotaLimit
|
|
|
|
return exceeded, currentUsage, nil
|
|
}
|
|
|
|
// GetQuotaInfo returns detailed quota information for a user.
|
|
type QuotaInfo struct {
|
|
Limit uint64 `json:"limit"` // Quota limit in bytes
|
|
Used uint64 `json:"used"` // Current usage in bytes
|
|
Unit string `json:"unit"` // Display unit (GB or TB)
|
|
Enforce bool `json:"enforce"` // Whether quota is enforced
|
|
Percentage float64 `json:"percentage"` // Usage percentage
|
|
}
|
|
|
|
// GetQuotaInfo retrieves quota information for a user.
|
|
func GetQuotaInfo(user *User) (*QuotaInfo, error) {
|
|
// Calculate current usage
|
|
used, err := CalculateUserQuota(user.Fs, user.Scope)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to calculate quota usage: %w", err)
|
|
}
|
|
|
|
// Calculate percentage
|
|
var percentage float64
|
|
if user.QuotaLimit > 0 {
|
|
percentage = (float64(used) / float64(user.QuotaLimit)) * 100
|
|
}
|
|
|
|
return &QuotaInfo{
|
|
Limit: user.QuotaLimit,
|
|
Used: used,
|
|
Unit: user.QuotaUnit,
|
|
Enforce: user.EnforceQuota,
|
|
Percentage: percentage,
|
|
}, nil
|
|
}
|