132 lines
3.1 KiB
Go
132 lines
3.1 KiB
Go
|
|
package storage
|
||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/rand"
|
||
|
|
"encoding/hex"
|
||
|
|
"fmt"
|
||
|
|
"net/url"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
const EphemeralKeyPrefix = "ephemeral/"
|
||
|
|
|
||
|
|
const defaultEphemeralTTL = 15 * time.Minute
|
||
|
|
|
||
|
|
type ephemeralEntry struct {
|
||
|
|
data []byte
|
||
|
|
contentType string
|
||
|
|
expiresAt time.Time
|
||
|
|
}
|
||
|
|
|
||
|
|
// EphemeralAssetStore holds short-lived blobs for Threads image_url fetch (reply direct transfer).
|
||
|
|
type EphemeralAssetStore struct {
|
||
|
|
mu sync.RWMutex
|
||
|
|
items map[string]*ephemeralEntry
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewEphemeralAssetStore() *EphemeralAssetStore {
|
||
|
|
return &EphemeralAssetStore{items: make(map[string]*ephemeralEntry)}
|
||
|
|
}
|
||
|
|
|
||
|
|
var defaultEphemeralStore = NewEphemeralAssetStore()
|
||
|
|
|
||
|
|
func DefaultEphemeralAssetStore() *EphemeralAssetStore {
|
||
|
|
return defaultEphemeralStore
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *EphemeralAssetStore) Register(data []byte, contentType string, ttl time.Duration) (string, error) {
|
||
|
|
if s == nil {
|
||
|
|
return "", fmt.Errorf("ephemeral store is not configured")
|
||
|
|
}
|
||
|
|
if len(data) == 0 {
|
||
|
|
return "", fmt.Errorf("ephemeral asset is empty")
|
||
|
|
}
|
||
|
|
if ttl <= 0 {
|
||
|
|
ttl = defaultEphemeralTTL
|
||
|
|
}
|
||
|
|
id, err := randomEphemeralID()
|
||
|
|
if err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
s.mu.Lock()
|
||
|
|
s.items[id] = &ephemeralEntry{
|
||
|
|
data: append([]byte(nil), data...),
|
||
|
|
contentType: strings.TrimSpace(contentType),
|
||
|
|
expiresAt: time.Now().Add(ttl),
|
||
|
|
}
|
||
|
|
s.mu.Unlock()
|
||
|
|
return EphemeralKeyPrefix + id, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *EphemeralAssetStore) Get(key string) ([]byte, string, bool) {
|
||
|
|
if s == nil {
|
||
|
|
return nil, "", false
|
||
|
|
}
|
||
|
|
id, ok := parseEphemeralID(key)
|
||
|
|
if !ok {
|
||
|
|
return nil, "", false
|
||
|
|
}
|
||
|
|
s.mu.RLock()
|
||
|
|
entry := s.items[id]
|
||
|
|
s.mu.RUnlock()
|
||
|
|
if entry == nil || time.Now().After(entry.expiresAt) {
|
||
|
|
s.Delete(key)
|
||
|
|
return nil, "", false
|
||
|
|
}
|
||
|
|
contentType := entry.contentType
|
||
|
|
if contentType == "" {
|
||
|
|
contentType = "application/octet-stream"
|
||
|
|
}
|
||
|
|
return append([]byte(nil), entry.data...), contentType, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *EphemeralAssetStore) Delete(key string) {
|
||
|
|
if s == nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
id, ok := parseEphemeralID(key)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
s.mu.Lock()
|
||
|
|
delete(s.items, id)
|
||
|
|
s.mu.Unlock()
|
||
|
|
}
|
||
|
|
|
||
|
|
func parseEphemeralID(key string) (string, bool) {
|
||
|
|
key = strings.TrimSpace(key)
|
||
|
|
if !strings.HasPrefix(key, EphemeralKeyPrefix) {
|
||
|
|
return "", false
|
||
|
|
}
|
||
|
|
id := strings.TrimSpace(strings.TrimPrefix(key, EphemeralKeyPrefix))
|
||
|
|
if id == "" {
|
||
|
|
return "", false
|
||
|
|
}
|
||
|
|
return id, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func randomEphemeralID() (string, error) {
|
||
|
|
buf := make([]byte, 16)
|
||
|
|
if _, err := rand.Read(buf); err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
return hex.EncodeToString(buf), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// BuildEphemeralPublicURL turns a staged ephemeral key into a Threads-fetchable URL.
|
||
|
|
func BuildEphemeralPublicURL(publicBaseURL, ephemeralKey string) (string, error) {
|
||
|
|
ephemeralKey = strings.TrimSpace(ephemeralKey)
|
||
|
|
if !strings.HasPrefix(ephemeralKey, EphemeralKeyPrefix) {
|
||
|
|
return "", fmt.Errorf("invalid ephemeral asset key")
|
||
|
|
}
|
||
|
|
base := strings.TrimRight(strings.TrimSpace(publicBaseURL), "/")
|
||
|
|
if base == "" {
|
||
|
|
return "", fmt.Errorf("HAIXUN_STORAGE_S3_PUBLIC_BASE_URL is required for Threads image publish")
|
||
|
|
}
|
||
|
|
if strings.HasSuffix(base, "/public/assets") {
|
||
|
|
return base + "?key=" + url.QueryEscape(ephemeralKey), nil
|
||
|
|
}
|
||
|
|
return base + "/" + ephemeralKey, nil
|
||
|
|
}
|