package logic import ( "context" "strconv" "time" "spark-server/internal/pkg/ratelimit" ) // Presigner issues short-lived object-storage URLs. Infrastructure implements it. type Presigner interface { PresignPut(ctx context.Context, key, contentType string, ttl time.Duration) (string, error) PresignGet(ctx context.Context, key string, ttl time.Duration) (string, error) } type IDGen interface { NextID() (int64, error) } // AvatarACL gates group_avatar uploads. Nil skips the check (tests). type AvatarACL interface { CanSetAvatar(ctx context.Context, userID, groupID int64) error } type Options struct { TTL time.Duration Limit *ratelimit.Limiter Now func() time.Time Groups AvatarACL } // Service issues upload/download credentials. HTTP and TCP share it. type Service struct { store Presigner ids IDGen ttl time.Duration limit *ratelimit.Limiter now func() time.Time groups AvatarACL } func NewService(store Presigner, ids IDGen, opt Options) *Service { ttl := opt.TTL if ttl <= 0 { ttl = 15 * time.Minute } clock := opt.Now if clock == nil { clock = func() time.Time { return time.Now().UTC() } } return &Service{store: store, ids: ids, ttl: ttl, limit: opt.Limit, now: clock, groups: opt.Groups} } func (s *Service) ready() bool { return s != nil && s.store != nil && s.ids != nil } func (s *Service) expiresIn() int64 { return int64(s.ttl / time.Second) } func (s *Service) allow(userID int64) bool { if s.limit == nil { return true } return s.limit.Allow("upload:" + strconv.FormatInt(userID, 10)) } func dayOf(t time.Time) string { return t.UTC().Format("20060102") }