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 }