246 lines
6.6 KiB
Go
246 lines
6.6 KiB
Go
|
|
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
|
||
|
|
}
|