package oss import ( "bytes" "context" "fmt" "sync" "time" "goalfymax-admin/internal/config" "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" ) type s3Client struct { client *s3.Client presign *s3.PresignClient cfg config.OssConfig } var ( clientOnce sync.Once c *s3Client ) func initClient() error { var initErr error clientOnce.Do(func() { cfg := config.GetConfig().Oss if cfg.Region == "" || cfg.Bucket == "" || cfg.AccessKeyID == "" || cfg.AccessKeySecret == "" { initErr = fmt.Errorf("OSS未配置: region/bucket/ak/sk 不能为空") return } awsCfg := aws.Config{ Region: cfg.Region, Credentials: credentials.NewStaticCredentialsProvider(cfg.AccessKeyID, cfg.AccessKeySecret, ""), } // 如果配置了自定义 endpoint (如阿里云 OSS),则设置 endpoint resolver if cfg.Endpoint != "" { awsCfg.BaseEndpoint = aws.String("https://" + cfg.Endpoint) } // 创建 S3 客户端,使用虚拟主机样式(阿里云 OSS 要求) s3c := s3.NewFromConfig(awsCfg, func(o *s3.Options) { // 阿里云 OSS 使用虚拟主机样式: bucket.endpoint/key // AWS S3 默认也是虚拟主机样式,所以保持 UsePathStyle = false (默认值) o.UsePathStyle = false }) c = &s3Client{ client: s3c, presign: s3.NewPresignClient(s3c), cfg: cfg, } }) return initErr } // GetPresignedGetURL 生成S3对象的预签名下载URL func GetPresignedGetURL(ctx context.Context, key string) (string, error) { if err := initClient(); err != nil { return "", err } expire := c.cfg.PresignUrlExpire if expire <= 0 { expire = 10 * time.Minute } req, err := c.presign.PresignGetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(c.cfg.Bucket), Key: aws.String(key), }, func(po *s3.PresignOptions) { po.Expires = expire }) if err != nil { return "", fmt.Errorf("生成预签名URL失败: %w", err) } return req.URL, nil } // DownloadFileContent 从S3下载文件内容和MIME类型 func DownloadFileContent(ctx context.Context, key string) ([]byte, string, error) { if err := initClient(); err != nil { return nil, "", err } resp, err := c.client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(c.cfg.Bucket), Key: aws.String(key), }) if err != nil { return nil, "", fmt.Errorf("从S3下载文件失败: %w", err) } defer resp.Body.Close() buf := new(bytes.Buffer) _, err = buf.ReadFrom(resp.Body) if err != nil { return nil, "", fmt.Errorf("读取文件内容失败: %w", err) } mimeType := "application/octet-stream" // 默认MIME类型 if resp.ContentType != nil && *resp.ContentType != "" { mimeType = *resp.ContentType } return buf.Bytes(), mimeType, nil }