dev #37
19
go.mod
19
go.mod
@@ -4,6 +4,11 @@ go 1.25.6
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/apolloconfig/agollo/v4 v4.4.0
|
github.com/apolloconfig/agollo/v4 v4.4.0
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.41.2
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.10
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1
|
||||||
|
github.com/aws/smithy-go v1.24.1
|
||||||
github.com/gin-contrib/pprof v1.5.3
|
github.com/gin-contrib/pprof v1.5.3
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/go-playground/validator/v10 v10.30.1
|
github.com/go-playground/validator/v10 v10.30.1
|
||||||
@@ -50,6 +55,20 @@ require (
|
|||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
|
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 // indirect
|
github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 // indirect
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/bytedance/sonic v1.14.0 // indirect
|
||||||
|
|||||||
38
go.sum
38
go.sum
@@ -798,6 +798,44 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
|
|||||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
github.com/aws/aws-sdk-go v1.45.1/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
github.com/aws/aws-sdk-go v1.45.1/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9 h1:IJRzQTvdpjHRPItx9gzNcz7Y1F+xqAR+xiy9rr5ZYl8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1 h1:giB30dEeoar5bgDnkE0q+z7cFjcHaCjulpmPVmuKR84=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1/go.mod h1:071TH4M3botFLWDbzQLfBR7tXYi7Fs2RsXSiH7nlUlY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
|
||||||
|
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
|
||||||
|
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package storage
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// Config MinIO配置
|
|
||||||
type Config struct {
|
|
||||||
Endpoint string `yaml:"endpoint" json:"endpoint"` // MinIO地址
|
|
||||||
AccessKeyID string `yaml:"access_key_id" json:"access_key_id"` // AccessKey
|
|
||||||
SecretAccessKey string `yaml:"secret_access_key" json:"secret_access_key"` // SecretKey
|
|
||||||
UseSSL bool `yaml:"use_ssl" json:"use_ssl"` // 是否使用SSL
|
|
||||||
BucketName string `yaml:"bucket_name" json:"bucket_name"` // 默认桶名称
|
|
||||||
Region string `yaml:"region" json:"region"` // 区域
|
|
||||||
CDNDomain string `yaml:"cdn_domain" json:"cdn_domain"` // CDN域名(可选)
|
|
||||||
PresignExpires time.Duration `yaml:"presign_expires" json:"presign_expires"` // 预签名URL过期时间,默认15分钟
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build minio
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -14,6 +16,18 @@ import (
|
|||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config MinIO配置
|
||||||
|
type Config struct {
|
||||||
|
Endpoint string `yaml:"endpoint" json:"endpoint"` // MinIO地址
|
||||||
|
AccessKeyID string `yaml:"access_key_id" json:"access_key_id"` // AccessKey
|
||||||
|
SecretAccessKey string `yaml:"secret_access_key" json:"secret_access_key"` // SecretKey
|
||||||
|
UseSSL bool `yaml:"use_ssl" json:"use_ssl"` // 是否使用SSL
|
||||||
|
BucketName string `yaml:"bucket_name" json:"bucket_name"` // 默认桶名称
|
||||||
|
Region string `yaml:"region" json:"region"` // 区域
|
||||||
|
CDNDomain string `yaml:"cdn_domain" json:"cdn_domain"` // CDN域名(可选)
|
||||||
|
PresignExpires time.Duration `yaml:"presign_expires" json:"presign_expires"` // 预签名URL过期时间,默认15分钟
|
||||||
|
}
|
||||||
|
|
||||||
// Client MinIO客户端
|
// Client MinIO客户端
|
||||||
type Client struct {
|
type Client struct {
|
||||||
client *minio.Client
|
client *minio.Client
|
||||||
441
pkg/storage/s3_client.go
Normal file
441
pkg/storage/s3_client.go
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
//go:build !minio
|
||||||
|
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||||
|
"github.com/aws/smithy-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config S3配置
|
||||||
|
type Config struct {
|
||||||
|
Endpoint string `yaml:"endpoint" json:"endpoint"` // S3地址(可选,留空使用AWS)
|
||||||
|
AccessKeyID string `yaml:"access_key_id" json:"access_key_id"` // AccessKey
|
||||||
|
SecretAccessKey string `yaml:"secret_access_key" json:"secret_access_key"` // SecretKey
|
||||||
|
UseSSL bool `yaml:"use_ssl" json:"use_ssl"` // 是否使用SSL
|
||||||
|
BucketName string `yaml:"bucket_name" json:"bucket_name"` // 默认桶名称
|
||||||
|
Region string `yaml:"region" json:"region"` // 区域
|
||||||
|
CDNDomain string `yaml:"cdn_domain" json:"cdn_domain"` // CDN域名(可选)
|
||||||
|
PresignExpires time.Duration `yaml:"presign_expires" json:"presign_expires"` // 预签名URL过期时间,默认15分钟
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client S3客户端
|
||||||
|
type Client struct {
|
||||||
|
client *s3.Client
|
||||||
|
presignClient *s3.PresignClient
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient 创建S3客户端
|
||||||
|
func NewClient(ctx context.Context, cfg *Config) (*Client, error) {
|
||||||
|
// 设置默认值
|
||||||
|
if cfg.PresignExpires == 0 {
|
||||||
|
cfg.PresignExpires = 15 * time.Minute
|
||||||
|
}
|
||||||
|
if cfg.UseSSL && cfg.Endpoint == "" {
|
||||||
|
cfg.UseSSL = true // AWS默认使用SSL
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建AWS配置选项
|
||||||
|
var opts []func(*config.LoadOptions) error
|
||||||
|
|
||||||
|
// 设置区域
|
||||||
|
if cfg.Region != "" {
|
||||||
|
opts = append(opts, config.WithRegion(cfg.Region))
|
||||||
|
} else {
|
||||||
|
opts = append(opts, config.WithRegion("us-east-1")) // 默认区域
|
||||||
|
cfg.Region = "us-east-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置凭证
|
||||||
|
if cfg.AccessKeyID != "" && cfg.SecretAccessKey != "" {
|
||||||
|
opts = append(opts, config.WithCredentialsProvider(
|
||||||
|
credentials.NewStaticCredentialsProvider(cfg.AccessKeyID, cfg.SecretAccessKey, ""),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
awsCfg, err := config.LoadDefaultConfig(ctx, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("加载AWS配置失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建S3客户端选项
|
||||||
|
s3Opts := []func(*s3.Options){
|
||||||
|
func(o *s3.Options) {
|
||||||
|
// 自定义端点(用于兼容S3的对象存储)
|
||||||
|
if cfg.Endpoint != "" {
|
||||||
|
o.BaseEndpoint = aws.String(cfg.Endpoint)
|
||||||
|
o.UsePathStyle = true // 使用路径风格访问,兼容MinIO
|
||||||
|
}
|
||||||
|
// SSL设置
|
||||||
|
if !cfg.UseSSL {
|
||||||
|
o.EndpointOptions.DisableHTTPS = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s3Client := s3.NewFromConfig(awsCfg, s3Opts...)
|
||||||
|
presignClient := s3.NewPresignClient(s3Client)
|
||||||
|
|
||||||
|
c := &Client{
|
||||||
|
client: s3Client,
|
||||||
|
presignClient: presignClient,
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保默认桶存在
|
||||||
|
if cfg.BucketName != "" {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
exists, err := c.BucketExists(timeoutCtx, cfg.BucketName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("检查桶失败: %w", err)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
if err := c.CreateBucket(timeoutCtx, cfg.BucketName); err != nil {
|
||||||
|
return nil, fmt.Errorf("创建桶失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadToken 上传凭证
|
||||||
|
type UploadToken struct {
|
||||||
|
Key string `json:"key"` // 文件存储路径
|
||||||
|
UploadURL string `json:"upload_url"` // 预签名上传URL
|
||||||
|
ExpiresAt time.Time `json:"expires_at"` // 过期时间
|
||||||
|
BucketName string `json:"bucket_name"` // 桶名称
|
||||||
|
AccessURL string `json:"access_url"` // 访问URL(可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadToken 下载凭证
|
||||||
|
type DownloadToken struct {
|
||||||
|
Key string `json:"key"` // 文件key
|
||||||
|
DownloadURL string `json:"download_url"` // 预签名下载URL
|
||||||
|
ExpiresAt time.Time `json:"expires_at"` // 过期时间
|
||||||
|
Filename string `json:"filename"` // 文件名(可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileInfo 文件信息
|
||||||
|
type FileInfo struct {
|
||||||
|
Key string `json:"key"` // 文件key
|
||||||
|
Size int64 `json:"size"` // 文件大小
|
||||||
|
ETag string `json:"etag"` // ETag(MD5)
|
||||||
|
ContentType string `json:"content_type"` // Content-Type
|
||||||
|
LastModified time.Time `json:"last_modified"` // 最后修改时间
|
||||||
|
Metadata map[string]string `json:"metadata"` // 元数据
|
||||||
|
URL string `json:"url"` // 访问URL
|
||||||
|
Exists bool `json:"exists"` // 是否存在
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateUploadToken 生成上传凭证
|
||||||
|
func (c *Client) GenerateUploadToken(ctx context.Context, key string, bucketName ...string) (*UploadToken, error) {
|
||||||
|
bucket := c.config.BucketName
|
||||||
|
if len(bucketName) > 0 && bucketName[0] != "" {
|
||||||
|
bucket = bucketName[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成预签名PUT URL
|
||||||
|
presignedReq, err := c.presignClient.PresignPutObject(ctx, &s3.PutObjectInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
}, s3.WithPresignExpires(c.config.PresignExpires))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("生成上传凭证失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token := &UploadToken{
|
||||||
|
Key: key,
|
||||||
|
UploadURL: presignedReq.URL,
|
||||||
|
ExpiresAt: time.Now().Add(c.config.PresignExpires),
|
||||||
|
BucketName: bucket,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果配置了CDN域名,生成访问URL
|
||||||
|
if c.config.CDNDomain != "" {
|
||||||
|
token.AccessURL = c.buildCDNURL(bucket, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateDownloadToken 生成下载凭证
|
||||||
|
func (c *Client) GenerateDownloadToken(ctx context.Context, key string, bucketName ...string) (*DownloadToken, error) {
|
||||||
|
bucket := c.config.BucketName
|
||||||
|
if len(bucketName) > 0 && bucketName[0] != "" {
|
||||||
|
bucket = bucketName[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
exists, err := c.FileExists(ctx, key, bucket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("文件不存在: %s", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成预签名GET URL
|
||||||
|
presignedReq, err := c.presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
}, s3.WithPresignExpires(c.config.PresignExpires))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("生成下载凭证失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token := &DownloadToken{
|
||||||
|
Key: key,
|
||||||
|
DownloadURL: presignedReq.URL,
|
||||||
|
ExpiresAt: time.Now().Add(c.config.PresignExpires),
|
||||||
|
Filename: filepath.Base(key),
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyFile 验证文件完整性
|
||||||
|
func (c *Client) VerifyFile(ctx context.Context, key string, expectedMD5 string, bucketName ...string) (*FileInfo, error) {
|
||||||
|
bucket := c.config.BucketName
|
||||||
|
if len(bucketName) > 0 && bucketName[0] != "" {
|
||||||
|
bucket = bucketName[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件信息
|
||||||
|
output, err := c.client.HeadObject(ctx, &s3.HeadObjectInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// 检查是否是 NoSuchKey 错误
|
||||||
|
if c.isNotFoundError(err) {
|
||||||
|
return &FileInfo{
|
||||||
|
Key: key,
|
||||||
|
Exists: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("获取文件信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换元数据
|
||||||
|
metadata := make(map[string]string)
|
||||||
|
for k, v := range output.Metadata {
|
||||||
|
metadata[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := ""
|
||||||
|
if output.ContentType != nil {
|
||||||
|
contentType = *output.ContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfo := &FileInfo{
|
||||||
|
Key: key,
|
||||||
|
Size: aws.ToInt64(output.ContentLength),
|
||||||
|
ETag: strings.Trim(aws.ToString(output.ETag), "\""), // 去除引号
|
||||||
|
ContentType: contentType,
|
||||||
|
LastModified: aws.ToTime(output.LastModified),
|
||||||
|
Metadata: metadata,
|
||||||
|
Exists: true,
|
||||||
|
URL: c.buildAccessURL(bucket, key),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了期望的MD5,进行验证
|
||||||
|
if expectedMD5 != "" {
|
||||||
|
if !c.compareMD5(fileInfo.ETag, expectedMD5) {
|
||||||
|
return fileInfo, fmt.Errorf("文件MD5不匹配,期望: %s, 实际: %s", expectedMD5, fileInfo.ETag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateFileMD5 计算文件MD5(从S3下载并计算)
|
||||||
|
func (c *Client) CalculateFileMD5(ctx context.Context, key string, bucketName ...string) (string, error) {
|
||||||
|
bucket := c.config.BucketName
|
||||||
|
if len(bucketName) > 0 && bucketName[0] != "" {
|
||||||
|
bucket = bucketName[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
output, err := c.client.GetObject(ctx, &s3.GetObjectInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("下载文件失败: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = output.Body.Close() }()
|
||||||
|
|
||||||
|
// 计算MD5
|
||||||
|
hash := md5.New()
|
||||||
|
if _, err := io.Copy(hash, output.Body); err != nil {
|
||||||
|
return "", fmt.Errorf("计算MD5失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileExists 检查文件是否存在
|
||||||
|
func (c *Client) FileExists(ctx context.Context, key string, bucketName ...string) (bool, error) {
|
||||||
|
bucket := c.config.BucketName
|
||||||
|
if len(bucketName) > 0 && bucketName[0] != "" {
|
||||||
|
bucket = bucketName[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := c.client.HeadObject(ctx, &s3.HeadObjectInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// 检查是否是 NoSuchKey 错误
|
||||||
|
if c.isNotFoundError(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("检查文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteFile 删除文件
|
||||||
|
func (c *Client) DeleteFile(ctx context.Context, key string, bucketName ...string) error {
|
||||||
|
bucket := c.config.BucketName
|
||||||
|
if len(bucketName) > 0 && bucketName[0] != "" {
|
||||||
|
bucket = bucketName[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := c.client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("删除文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileInfo 获取文件信息
|
||||||
|
func (c *Client) GetFileInfo(ctx context.Context, key string, bucketName ...string) (*FileInfo, error) {
|
||||||
|
return c.VerifyFile(ctx, key, "", bucketName...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BucketExists 检查桶是否存在
|
||||||
|
func (c *Client) BucketExists(ctx context.Context, bucketName string) (bool, error) {
|
||||||
|
_, err := c.client.HeadBucket(ctx, &s3.HeadBucketInput{
|
||||||
|
Bucket: aws.String(bucketName),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// 检查是否是 NotFound 错误
|
||||||
|
if c.isNotFoundError(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("检查桶失败: %w", err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBucket 创建桶
|
||||||
|
func (c *Client) CreateBucket(ctx context.Context, bucketName string) error {
|
||||||
|
input := &s3.CreateBucketInput{
|
||||||
|
Bucket: aws.String(bucketName),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是 us-east-1 区域,需要设置 LocationConstraint
|
||||||
|
if c.config.Region != "" && c.config.Region != "us-east-1" {
|
||||||
|
input.CreateBucketConfiguration = &types.CreateBucketConfiguration{
|
||||||
|
LocationConstraint: types.BucketLocationConstraint(c.config.Region),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := c.client.CreateBucket(ctx, input)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建桶失败: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBucketPublic 设置桶为公开访问
|
||||||
|
func (c *Client) SetBucketPublic(ctx context.Context, bucketName string) error {
|
||||||
|
policy := fmt.Sprintf(`{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Principal": {"AWS": ["*"]},
|
||||||
|
"Action": ["s3:GetObject"],
|
||||||
|
"Resource": ["arn:aws:s3:::%s/*"]
|
||||||
|
}]
|
||||||
|
}`, bucketName)
|
||||||
|
|
||||||
|
_, err := c.client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
|
||||||
|
Bucket: aws.String(bucketName),
|
||||||
|
Policy: aws.String(policy),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("设置桶策略失败: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildAccessURL 构建访问URL
|
||||||
|
func (c *Client) buildAccessURL(bucket, key string) string {
|
||||||
|
if c.config.CDNDomain != "" {
|
||||||
|
return c.buildCDNURL(bucket, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol := "http"
|
||||||
|
if c.config.UseSSL {
|
||||||
|
protocol = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.config.Endpoint != "" {
|
||||||
|
return fmt.Sprintf("%s://%s/%s/%s", protocol, c.config.Endpoint, bucket, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AWS S3 默认URL格式
|
||||||
|
return fmt.Sprintf("%s://%s.s3.%s.amazonaws.com/%s", protocol, bucket, c.config.Region, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildCDNURL 构建CDN URL
|
||||||
|
func (c *Client) buildCDNURL(bucket, key string) string {
|
||||||
|
return fmt.Sprintf("%s/%s/%s", strings.TrimRight(c.config.CDNDomain, "/"), bucket, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareMD5 比较MD5
|
||||||
|
func (c *Client) compareMD5(etag, md5 string) bool {
|
||||||
|
etag = strings.ToLower(strings.Trim(etag, "\""))
|
||||||
|
md5 = strings.ToLower(strings.Trim(md5, "\""))
|
||||||
|
return etag == md5
|
||||||
|
}
|
||||||
|
|
||||||
|
// isNotFoundError 检查是否为NotFound错误
|
||||||
|
func (c *Client) isNotFoundError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiErr smithy.APIError
|
||||||
|
if errors.As(err, &apiErr) {
|
||||||
|
code := apiErr.ErrorCode()
|
||||||
|
return code == "NotFound" || code == "NoSuchKey" || code == "NoSuchBucket"
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user