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>
110 lines
2.8 KiB
Go
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
|
|
}
|
|
}
|