package client import ( "bytes" "context" "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" ) type FileConfig interface { GetAccessKeyID() string GetAccessKeySecret() string GetEndpoint() string GetBucketName() string GetHostURL() string } const _awsRegion = "us-east-1" const _s3ACL = "public-read" type S3FileClientImpl struct { s3 *s3.S3 cfg FileConfig } func NewFileClient(fileCfg FileConfig) *S3FileClientImpl { sess, err := session.NewSession(&aws.Config{ S3ForcePathStyle: aws.Bool(true), Endpoint: aws.String(fileCfg.GetEndpoint()), Region: aws.String(_awsRegion), Credentials: credentials.NewStaticCredentials(fileCfg.GetAccessKeyID(), fileCfg.GetAccessKeySecret(), ""), }) if err != nil { fmt.Println("Failed to create AWS session:", err) return nil } return &S3FileClientImpl{ s3: s3.New(sess), cfg: fileCfg, } } func (r *S3FileClientImpl) UploadFile(ctx context.Context, fileName string, fileContent []byte) (fileUrl string, err error) { return r.Upload(ctx, r.cfg.GetBucketName(), fileName, fileContent, "application/octet-stream") } func (r *S3FileClientImpl) Upload(ctx context.Context, bucket, key string, content []byte, contentType string) (string, error) { reader := bytes.NewReader(content) _, err := r.s3.PutObjectWithContext(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucket), Key: aws.String(key), Body: reader, ACL: aws.String(_s3ACL), ContentType: aws.String(contentType), }) if err != nil { return "", err } return r.GetPublicURL(bucket, key), nil } // EnsureBucket ensures a bucket exists (idempotent) func (r *S3FileClientImpl) EnsureBucket(ctx context.Context, bucket string) error { _, err := r.s3.HeadBucketWithContext(ctx, &s3.HeadBucketInput{Bucket: aws.String(bucket)}) if err == nil { return nil } _, err = r.s3.CreateBucketWithContext(ctx, &s3.CreateBucketInput{Bucket: aws.String(bucket)}) return err } func (r *S3FileClientImpl) GetPublicURL(bucket, key string) string { if key == "" { return "" } // HostURL expected to include scheme and optional host/path prefix; ensure single slash join return fmt.Sprintf("%s%s/%s", r.cfg.GetHostURL(), bucket, key) }