filebrowser/users/quota.go
Jon_K 0a3d1642f7 Add user quota management system
Introduces comprehensive quota management with real-time usage tracking, frontend components, backend API endpoints, and TUS upload integration for storage limit enforcement.
2026-01-03 01:07:26 -05:00

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
}