feat: add upload policies, daily limits, and storage quotas
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m8s
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m8s
- Add environment variables to configure anonymous uploads, daily upload caps, and default user storage limits. - Update config loader to parse and validate the new settings. - Implement backend logic to track daily usage and active storage per user. - Update README and `.env.example` to document the new settings and admin panels.
This commit is contained in:
@@ -48,23 +48,25 @@ type AuthService struct {
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"passwordHash"`
|
||||
Role string `json:"role"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"passwordHash"`
|
||||
Role string `json:"role"`
|
||||
Status string `json:"status"`
|
||||
StorageQuotaMB *float64 `json:"storageQuotaMb,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type PublicUser struct {
|
||||
ID string
|
||||
Username string
|
||||
Email string
|
||||
Role string
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
ID string
|
||||
Username string
|
||||
Email string
|
||||
Role string
|
||||
Status string
|
||||
StorageQuotaMB *float64
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
@@ -366,6 +368,19 @@ func (s *AuthService) SetPassword(userID, password string) error {
|
||||
return s.saveUser(user)
|
||||
}
|
||||
|
||||
func (s *AuthService) SetUserStorageQuota(userID string, quotaMB *float64) error {
|
||||
if quotaMB != nil && *quotaMB <= 0 {
|
||||
return fmt.Errorf("storage quota must be positive")
|
||||
}
|
||||
user, err := s.UserByID(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.StorageQuotaMB = quotaMB
|
||||
user.UpdatedAt = time.Now().UTC()
|
||||
return s.saveUser(user)
|
||||
}
|
||||
|
||||
func (s *AuthService) UserByID(id string) (User, error) {
|
||||
var user User
|
||||
err := s.db.View(func(tx *bbolt.Tx) error {
|
||||
@@ -455,12 +470,13 @@ func (s *AuthService) CollectionByID(id string) (Collection, error) {
|
||||
|
||||
func (s *AuthService) PublicUser(user User) PublicUser {
|
||||
return PublicUser{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
Status: user.Status,
|
||||
CreatedAt: user.CreatedAt,
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
Status: user.Status,
|
||||
StorageQuotaMB: user.StorageQuotaMB,
|
||||
CreatedAt: user.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
245
backend/libs/services/settings.go
Normal file
245
backend/libs/services/settings.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
"warpbox.dev/backend/libs/config"
|
||||
)
|
||||
|
||||
var (
|
||||
settingsBucket = []byte("settings")
|
||||
usageBucket = []byte("usage")
|
||||
)
|
||||
|
||||
var settingsKey = []byte("upload_policy")
|
||||
|
||||
type UploadPolicySettings struct {
|
||||
AnonymousUploadsEnabled bool `json:"anonymousUploadsEnabled"`
|
||||
AnonymousMaxUploadMB float64 `json:"anonymousMaxUploadMb"`
|
||||
AnonymousDailyUploadMB float64 `json:"anonymousDailyUploadMb"`
|
||||
UserDailyUploadMB float64 `json:"userDailyUploadMb"`
|
||||
DefaultUserStorageMB float64 `json:"defaultUserStorageMb"`
|
||||
UsageRetentionDays int `json:"usageRetentionDays"`
|
||||
}
|
||||
|
||||
type UsageRecord struct {
|
||||
Key string `json:"key"`
|
||||
SubjectType string `json:"subjectType"`
|
||||
Subject string `json:"subject"`
|
||||
Date string `json:"date"`
|
||||
UploadedBytes int64 `json:"uploadedBytes"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type SettingsService struct {
|
||||
db *bbolt.DB
|
||||
defaults UploadPolicySettings
|
||||
}
|
||||
|
||||
func NewSettingsService(db *bbolt.DB, defaults config.SettingsDefaults) (*SettingsService, error) {
|
||||
service := &SettingsService{
|
||||
db: db,
|
||||
defaults: UploadPolicySettings{
|
||||
AnonymousUploadsEnabled: defaults.AnonymousUploadsEnabled,
|
||||
AnonymousMaxUploadMB: defaults.AnonymousMaxUploadMB,
|
||||
AnonymousDailyUploadMB: defaults.AnonymousDailyUploadMB,
|
||||
UserDailyUploadMB: defaults.UserDailyUploadMB,
|
||||
DefaultUserStorageMB: defaults.DefaultUserStorageMB,
|
||||
UsageRetentionDays: defaults.UsageRetentionDays,
|
||||
},
|
||||
}
|
||||
if err := service.validate(service.defaults); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := db.Update(func(tx *bbolt.Tx) error {
|
||||
for _, bucket := range [][]byte{settingsBucket, usageBucket} {
|
||||
if _, err := tx.CreateBucketIfNotExists(bucket); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (s *SettingsService) UploadPolicy() (UploadPolicySettings, error) {
|
||||
settings := s.defaults
|
||||
err := s.db.View(func(tx *bbolt.Tx) error {
|
||||
data := tx.Bucket(settingsBucket).Get(settingsKey)
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(data, &settings)
|
||||
})
|
||||
if err != nil {
|
||||
return UploadPolicySettings{}, err
|
||||
}
|
||||
if err := s.validate(settings); err != nil {
|
||||
return UploadPolicySettings{}, err
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func (s *SettingsService) UpdateUploadPolicy(settings UploadPolicySettings) error {
|
||||
if err := s.validate(settings); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.db.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.Bucket(settingsBucket).Put(settingsKey, data)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SettingsService) Usage(subjectType, subject string, now time.Time) (UsageRecord, error) {
|
||||
key := usageKey(subjectType, subject, now)
|
||||
var record UsageRecord
|
||||
err := s.db.View(func(tx *bbolt.Tx) error {
|
||||
data := tx.Bucket(usageBucket).Get([]byte(key))
|
||||
if data == nil {
|
||||
record = UsageRecord{Key: key, SubjectType: subjectType, Subject: subject, Date: usageDate(now)}
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(data, &record)
|
||||
})
|
||||
return record, err
|
||||
}
|
||||
|
||||
func (s *SettingsService) AddUsage(subjectType, subject string, bytes int64, now time.Time) error {
|
||||
if bytes <= 0 {
|
||||
return nil
|
||||
}
|
||||
key := usageKey(subjectType, subject, now)
|
||||
return s.db.Update(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket(usageBucket)
|
||||
record := UsageRecord{Key: key, SubjectType: subjectType, Subject: subject, Date: usageDate(now)}
|
||||
data := bucket.Get([]byte(key))
|
||||
if data != nil {
|
||||
if err := json.Unmarshal(data, &record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
record.UploadedBytes += bytes
|
||||
record.UpdatedAt = now.UTC()
|
||||
next, err := json.Marshal(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.Put([]byte(key), next)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SettingsService) CleanupUsage(now time.Time, retentionDays int) error {
|
||||
if retentionDays <= 0 {
|
||||
return fmt.Errorf("usage retention days must be positive")
|
||||
}
|
||||
cutoff := now.UTC().AddDate(0, 0, -retentionDays)
|
||||
return s.db.Update(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket(usageBucket)
|
||||
return bucket.ForEach(func(key, value []byte) error {
|
||||
var record UsageRecord
|
||||
if err := json.Unmarshal(value, &record); err != nil {
|
||||
return err
|
||||
}
|
||||
date, err := time.Parse("2006-01-02", record.Date)
|
||||
if err != nil || date.Before(cutoff) {
|
||||
return bucket.Delete(key)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SettingsService) UsageForUser(userID string, now time.Time) (UsageRecord, error) {
|
||||
return s.Usage("user", userID, now)
|
||||
}
|
||||
|
||||
func (s *SettingsService) UsageForIP(ip string, now time.Time) (UsageRecord, error) {
|
||||
return s.Usage("ip", ip, now)
|
||||
}
|
||||
|
||||
func (s *SettingsService) validate(settings UploadPolicySettings) error {
|
||||
if settings.AnonymousMaxUploadMB <= 0 {
|
||||
return fmt.Errorf("anonymous max upload must be positive")
|
||||
}
|
||||
if settings.AnonymousDailyUploadMB <= 0 {
|
||||
return fmt.Errorf("anonymous daily upload must be positive")
|
||||
}
|
||||
if settings.UserDailyUploadMB <= 0 {
|
||||
return fmt.Errorf("user daily upload must be positive")
|
||||
}
|
||||
if settings.DefaultUserStorageMB <= 0 {
|
||||
return fmt.Errorf("default user storage must be positive")
|
||||
}
|
||||
if settings.UsageRetentionDays <= 0 {
|
||||
return fmt.Errorf("usage retention days must be positive")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseMegabytesValue(value string) (float64, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0, fmt.Errorf("megabyte value is required")
|
||||
}
|
||||
value = strings.TrimSuffix(value, "MB")
|
||||
value = strings.TrimSuffix(value, "Mb")
|
||||
value = strings.TrimSuffix(value, "mb")
|
||||
value = strings.TrimSpace(value)
|
||||
parsed, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if parsed <= 0 {
|
||||
return 0, fmt.Errorf("megabyte value must be positive")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func MegabytesToBytes(value float64) int64 {
|
||||
return int64(value * 1024 * 1024)
|
||||
}
|
||||
|
||||
func FormatMegabytesFromBytes(value int64) string {
|
||||
mb := float64(value) / 1024 / 1024
|
||||
return FormatMegabytesLabel(mb)
|
||||
}
|
||||
|
||||
func FormatMegabytesLabel(value float64) string {
|
||||
return strconv.FormatFloat(value, 'f', -1, 64) + " MB"
|
||||
}
|
||||
|
||||
func usageKey(subjectType, subject string, now time.Time) string {
|
||||
return subjectType + ":" + subject + ":" + usageDate(now)
|
||||
}
|
||||
|
||||
func usageDate(now time.Time) string {
|
||||
return now.UTC().Format("2006-01-02")
|
||||
}
|
||||
|
||||
func ClientIP(remoteAddr, forwardedFor string) string {
|
||||
if forwardedFor != "" {
|
||||
parts := strings.Split(forwardedFor, ",")
|
||||
if ip := strings.TrimSpace(parts[0]); ip != "" {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
host := remoteAddr
|
||||
if strings.Contains(remoteAddr, ":") {
|
||||
if splitHost, _, err := net.SplitHostPort(remoteAddr); err == nil {
|
||||
host = splitHost
|
||||
}
|
||||
}
|
||||
return host
|
||||
}
|
||||
173
backend/libs/services/settings_test.go
Normal file
173
backend/libs/services/settings_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"warpbox.dev/backend/libs/config"
|
||||
)
|
||||
|
||||
func TestSettingsLoadDefaultsAndOverrides(t *testing.T) {
|
||||
settings := newTestSettingsService(t)
|
||||
policy, err := settings.UploadPolicy()
|
||||
if err != nil {
|
||||
t.Fatalf("UploadPolicy returned error: %v", err)
|
||||
}
|
||||
if !policy.AnonymousUploadsEnabled || policy.AnonymousMaxUploadMB != 512 {
|
||||
t.Fatalf("default policy = %+v", policy)
|
||||
}
|
||||
|
||||
policy.AnonymousUploadsEnabled = false
|
||||
policy.UserDailyUploadMB = 123
|
||||
if err := settings.UpdateUploadPolicy(policy); err != nil {
|
||||
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
||||
}
|
||||
next, err := settings.UploadPolicy()
|
||||
if err != nil {
|
||||
t.Fatalf("UploadPolicy returned error: %v", err)
|
||||
}
|
||||
if next.AnonymousUploadsEnabled || next.UserDailyUploadMB != 123 {
|
||||
t.Fatalf("override policy = %+v", next)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsUseNewEnvDefaultsUntilSaved(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
upload, err := NewUploadService(1024*1024, filepath.Join(root, "data"), "http://example.test", slog.Default())
|
||||
if err != nil {
|
||||
t.Fatalf("NewUploadService returned error: %v", err)
|
||||
}
|
||||
defer upload.Close()
|
||||
|
||||
first, err := NewSettingsService(upload.DB(), config.SettingsDefaults{
|
||||
AnonymousUploadsEnabled: true,
|
||||
AnonymousMaxUploadMB: 111,
|
||||
AnonymousDailyUploadMB: 222,
|
||||
UserDailyUploadMB: 333,
|
||||
DefaultUserStorageMB: 444,
|
||||
UsageRetentionDays: 30,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewSettingsService first returned error: %v", err)
|
||||
}
|
||||
firstPolicy, err := first.UploadPolicy()
|
||||
if err != nil {
|
||||
t.Fatalf("UploadPolicy first returned error: %v", err)
|
||||
}
|
||||
if firstPolicy.AnonymousMaxUploadMB != 111 {
|
||||
t.Fatalf("first AnonymousMaxUploadMB = %v, want 111", firstPolicy.AnonymousMaxUploadMB)
|
||||
}
|
||||
|
||||
second, err := NewSettingsService(upload.DB(), config.SettingsDefaults{
|
||||
AnonymousUploadsEnabled: true,
|
||||
AnonymousMaxUploadMB: 555,
|
||||
AnonymousDailyUploadMB: 666,
|
||||
UserDailyUploadMB: 777,
|
||||
DefaultUserStorageMB: 888,
|
||||
UsageRetentionDays: 30,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewSettingsService second returned error: %v", err)
|
||||
}
|
||||
secondPolicy, err := second.UploadPolicy()
|
||||
if err != nil {
|
||||
t.Fatalf("UploadPolicy second returned error: %v", err)
|
||||
}
|
||||
if secondPolicy.AnonymousMaxUploadMB != 555 {
|
||||
t.Fatalf("second AnonymousMaxUploadMB = %v, want 555", secondPolicy.AnonymousMaxUploadMB)
|
||||
}
|
||||
|
||||
if err := second.UpdateUploadPolicy(secondPolicy); err != nil {
|
||||
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
||||
}
|
||||
third, err := NewSettingsService(upload.DB(), config.SettingsDefaults{
|
||||
AnonymousUploadsEnabled: true,
|
||||
AnonymousMaxUploadMB: 999,
|
||||
AnonymousDailyUploadMB: 999,
|
||||
UserDailyUploadMB: 999,
|
||||
DefaultUserStorageMB: 999,
|
||||
UsageRetentionDays: 30,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewSettingsService third returned error: %v", err)
|
||||
}
|
||||
thirdPolicy, err := third.UploadPolicy()
|
||||
if err != nil {
|
||||
t.Fatalf("UploadPolicy third returned error: %v", err)
|
||||
}
|
||||
if thirdPolicy.AnonymousMaxUploadMB != 555 {
|
||||
t.Fatalf("third AnonymousMaxUploadMB = %v, want persisted 555", thirdPolicy.AnonymousMaxUploadMB)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsRejectInvalidMegabytes(t *testing.T) {
|
||||
if _, err := ParseMegabytesValue("0"); err == nil {
|
||||
t.Fatalf("ParseMegabytesValue accepted zero")
|
||||
}
|
||||
settings := newTestSettingsService(t)
|
||||
policy, err := settings.UploadPolicy()
|
||||
if err != nil {
|
||||
t.Fatalf("UploadPolicy returned error: %v", err)
|
||||
}
|
||||
policy.DefaultUserStorageMB = -1
|
||||
if err := settings.UpdateUploadPolicy(policy); err == nil {
|
||||
t.Fatalf("UpdateUploadPolicy accepted negative storage")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDailyUsageAndCleanup(t *testing.T) {
|
||||
settings := newTestSettingsService(t)
|
||||
now := time.Date(2026, 5, 30, 12, 0, 0, 0, time.UTC)
|
||||
if err := settings.AddUsage("ip", "127.0.0.1", 1024, now); err != nil {
|
||||
t.Fatalf("AddUsage returned error: %v", err)
|
||||
}
|
||||
if err := settings.AddUsage("ip", "127.0.0.1", 2048, now); err != nil {
|
||||
t.Fatalf("AddUsage returned error: %v", err)
|
||||
}
|
||||
usage, err := settings.UsageForIP("127.0.0.1", now)
|
||||
if err != nil {
|
||||
t.Fatalf("UsageForIP returned error: %v", err)
|
||||
}
|
||||
if usage.UploadedBytes != 3072 {
|
||||
t.Fatalf("UploadedBytes = %d, want 3072", usage.UploadedBytes)
|
||||
}
|
||||
|
||||
if err := settings.CleanupUsage(now.AddDate(0, 0, 31), 30); err != nil {
|
||||
t.Fatalf("CleanupUsage returned error: %v", err)
|
||||
}
|
||||
usage, err = settings.UsageForIP("127.0.0.1", now)
|
||||
if err != nil {
|
||||
t.Fatalf("UsageForIP returned error: %v", err)
|
||||
}
|
||||
if usage.UploadedBytes != 0 {
|
||||
t.Fatalf("UploadedBytes after cleanup = %d, want 0", usage.UploadedBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestSettingsService(t *testing.T) *SettingsService {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
upload, err := NewUploadService(1024*1024, filepath.Join(root, "data"), "http://example.test", slog.Default())
|
||||
if err != nil {
|
||||
t.Fatalf("NewUploadService returned error: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := upload.Close(); err != nil {
|
||||
t.Fatalf("Close returned error: %v", err)
|
||||
}
|
||||
})
|
||||
settings, err := NewSettingsService(upload.DB(), config.SettingsDefaults{
|
||||
AnonymousUploadsEnabled: true,
|
||||
AnonymousMaxUploadMB: 512,
|
||||
AnonymousDailyUploadMB: 2048,
|
||||
UserDailyUploadMB: 8192,
|
||||
DefaultUserStorageMB: 51200,
|
||||
UsageRetentionDays: 30,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewSettingsService returned error: %v", err)
|
||||
}
|
||||
return settings
|
||||
}
|
||||
@@ -384,15 +384,27 @@ func (s *UploadService) UserBoxes(userID string, collectionNames map[string]stri
|
||||
}
|
||||
|
||||
func (s *UploadService) UserStorageUsed(userID string) (int64, error) {
|
||||
return s.userStorageUsed(userID, false)
|
||||
}
|
||||
|
||||
func (s *UploadService) UserActiveStorageUsed(userID string) (int64, error) {
|
||||
return s.userStorageUsed(userID, true)
|
||||
}
|
||||
|
||||
func (s *UploadService) userStorageUsed(userID string, activeOnly bool) (int64, error) {
|
||||
boxes, err := s.ListBoxes(0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var total int64
|
||||
now := time.Now().UTC()
|
||||
for _, box := range boxes {
|
||||
if box.OwnerID != userID {
|
||||
continue
|
||||
}
|
||||
if activeOnly && !box.ExpiresAt.After(now) {
|
||||
continue
|
||||
}
|
||||
for _, file := range box.Files {
|
||||
total += file.Size
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDeleteTokenVerification(t *testing.T) {
|
||||
@@ -59,6 +60,39 @@ func TestDeleteBoxWithTokenRemovesMetadataAndFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserActiveStorageUsedIgnoresExpiredBoxes(t *testing.T) {
|
||||
service := newTestUploadService(t)
|
||||
active, err := service.CreateBox(testFileHeaders(t, "file", "active.txt", "active"), UploadOptions{MaxDays: 1, OwnerID: "user-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateBox active returned error: %v", err)
|
||||
}
|
||||
expired, err := service.CreateBox(testFileHeaders(t, "file", "expired.txt", "expired"), UploadOptions{MaxDays: 1, OwnerID: "user-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateBox expired returned error: %v", err)
|
||||
}
|
||||
expiredBox, err := service.GetBox(expired.BoxID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetBox returned error: %v", err)
|
||||
}
|
||||
expiredBox.ExpiresAt = time.Now().UTC().Add(-time.Hour)
|
||||
if err := service.SaveBox(expiredBox); err != nil {
|
||||
t.Fatalf("SaveBox returned error: %v", err)
|
||||
}
|
||||
|
||||
activeBox, err := service.GetBox(active.BoxID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetBox active returned error: %v", err)
|
||||
}
|
||||
want := activeBox.Files[0].Size
|
||||
got, err := service.UserActiveStorageUsed("user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("UserActiveStorageUsed returned error: %v", err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("UserActiveStorageUsed = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestUploadService(t *testing.T) *UploadService {
|
||||
t.Helper()
|
||||
service, err := NewUploadService(1024*1024, t.TempDir(), "http://example.test", slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
|
||||
Reference in New Issue
Block a user