spark-server/internal/infrastructure/objectstore/store.go
zhr ae9a71034d feat: implement Spark IM V3 modular monolith
Add HTTP /v1 MVC, TCP send path, groups with server-side member-avatar collage, object-storage upload, and friends/favorites around the architecture-v3 layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 19:52:11 +08:00

110 lines
2.8 KiB
Go

// Package objectstore talks to S3-compatible object storage (RustFS) via AWS SDK v2.
package objectstore
import (
"context"
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"go.uber.org/zap"
)
const (
defaultRegion = "us-east-1"
defaultTimeout = 15 * time.Second
)
// Store is a RustFS / S3 client. Nil or !Ready means object storage is not configured.
type Store struct {
client *s3.Client
presign *s3.PresignClient
bucket string
log *zap.Logger
}
// Options configure Open. Empty Endpoint skips the connection.
type Options struct {
Endpoint string
AccessKey string
SecretKey string
Bucket string
Region string
UseSSL bool
ChatExpireDays int
Log *zap.Logger
}
func Open(o *Options) (*Store, error) {
if o == nil || strings.TrimSpace(o.Endpoint) == "" {
return &Store{}, nil
}
host, secure, err := parseEndpoint(o.Endpoint, o.UseSSL)
if err != nil {
return nil, err
}
bucket := strings.TrimSpace(o.Bucket)
if bucket == "" {
bucket = "spark"
}
region := strings.TrimSpace(o.Region)
if region == "" {
region = defaultRegion
}
scheme := "http"
if secure {
scheme = "https"
}
client := newS3Client(region, scheme+"://"+host, o.AccessKey, o.SecretKey)
s := &Store{client: client, presign: s3.NewPresignClient(client), bucket: bucket, log: o.Log}
if s.log == nil {
s.log = zap.NewNop()
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
if err := s.ensureBucket(ctx, region, o.ChatExpireDays); err != nil {
return nil, err
}
return s, nil
}
func (s *Store) Ready() bool {
return s != nil && s.client != nil && s.presign != nil && s.bucket != ""
}
func (s *Store) Ping(ctx context.Context) error {
if !s.Ready() {
return nil
}
_, err := s.client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: aws.String(s.bucket)})
return err
}
func newS3Client(region, baseURL, access, secret string) *s3.Client {
return s3.New(s3.Options{
Region: region,
Credentials: credentials.NewStaticCredentialsProvider(access, secret, ""),
BaseEndpoint: aws.String(baseURL),
UsePathStyle: true,
RequestChecksumCalculation: aws.RequestChecksumCalculationWhenRequired,
ResponseChecksumValidation: aws.ResponseChecksumValidationWhenRequired,
})
}
func parseEndpoint(raw string, useSSL bool) (host string, secure bool, err error) {
raw = strings.TrimSpace(raw)
switch {
case raw == "":
return "", false, fmt.Errorf("objectstore: empty endpoint")
case strings.HasPrefix(raw, "https://"):
return strings.TrimPrefix(raw, "https://"), true, nil
case strings.HasPrefix(raw, "http://"):
return strings.TrimPrefix(raw, "http://"), false, nil
default:
return raw, useSSL, nil
}
}