feat: 添加环境变量管理工具和部署配置改版
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m33s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m33s
主要改动: - 新增交互式环境配置脚本 (scripts/setup-env.sh) - 新增本地启动快捷脚本 (scripts/run-local.sh) - 新增环境变量模板文件 (.env.example) - 部署模式改版:使用嵌入式配置 + 环境变量覆盖 - 添加对象存储功能支持 - 改进 IoT 卡片导入任务 - 优化 OpenAPI 文档生成 - 删除旧的配置文件,改用嵌入式默认配置
This commit is contained in:
184
pkg/storage/s3.go
Normal file
184
pkg/storage/s3.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||
)
|
||||
|
||||
type S3Provider struct {
|
||||
client *s3.S3
|
||||
uploader *s3manager.Uploader
|
||||
bucket string
|
||||
tempDir string
|
||||
}
|
||||
|
||||
func NewS3Provider(cfg *config.StorageConfig) (*S3Provider, error) {
|
||||
if cfg.S3.Endpoint == "" || cfg.S3.Bucket == "" {
|
||||
return nil, fmt.Errorf("S3 配置不完整:endpoint 和 bucket 必填")
|
||||
}
|
||||
|
||||
if cfg.S3.AccessKeyID == "" || cfg.S3.SecretAccessKey == "" {
|
||||
return nil, fmt.Errorf("S3 凭证未配置:access_key_id 和 secret_access_key 必填")
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Endpoint: aws.String(cfg.S3.Endpoint),
|
||||
Region: aws.String(cfg.S3.Region),
|
||||
Credentials: credentials.NewStaticCredentials(cfg.S3.AccessKeyID, cfg.S3.SecretAccessKey, ""),
|
||||
DisableSSL: aws.Bool(!cfg.S3.UseSSL),
|
||||
S3ForcePathStyle: aws.Bool(cfg.S3.PathStyle),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建 S3 session 失败: %w", err)
|
||||
}
|
||||
|
||||
tempDir := cfg.TempDir
|
||||
if tempDir == "" {
|
||||
tempDir = "/tmp/junhong-storage"
|
||||
}
|
||||
|
||||
return &S3Provider{
|
||||
client: s3.New(sess),
|
||||
uploader: s3manager.NewUploader(sess),
|
||||
bucket: cfg.S3.Bucket,
|
||||
tempDir: tempDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *S3Provider) Upload(ctx context.Context, key string, reader io.Reader, contentType string) error {
|
||||
input := &s3manager.UploadInput{
|
||||
Bucket: aws.String(p.bucket),
|
||||
Key: aws.String(key),
|
||||
Body: reader,
|
||||
ContentType: aws.String(contentType),
|
||||
}
|
||||
|
||||
_, err := p.uploader.UploadWithContext(ctx, input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("上传文件失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *S3Provider) Download(ctx context.Context, key string, writer io.Writer) error {
|
||||
input := &s3.GetObjectInput{
|
||||
Bucket: aws.String(p.bucket),
|
||||
Key: aws.String(key),
|
||||
}
|
||||
|
||||
result, err := p.client.GetObjectWithContext(ctx, input)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "NoSuchKey") {
|
||||
return fmt.Errorf("文件不存在: %s", key)
|
||||
}
|
||||
return fmt.Errorf("下载文件失败: %w", err)
|
||||
}
|
||||
defer result.Body.Close()
|
||||
|
||||
_, err = io.Copy(writer, result.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入文件内容失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *S3Provider) DownloadToTemp(ctx context.Context, key string) (string, func(), error) {
|
||||
ext := filepath.Ext(key)
|
||||
if ext == "" {
|
||||
ext = ".tmp"
|
||||
}
|
||||
|
||||
tempFile, err := os.CreateTemp(p.tempDir, "download-*"+ext)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("创建临时文件失败: %w", err)
|
||||
}
|
||||
tempPath := tempFile.Name()
|
||||
|
||||
cleanup := func() {
|
||||
_ = os.Remove(tempPath)
|
||||
}
|
||||
|
||||
if err := p.Download(ctx, key, tempFile); err != nil {
|
||||
tempFile.Close()
|
||||
cleanup()
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if err := tempFile.Close(); err != nil {
|
||||
cleanup()
|
||||
return "", nil, fmt.Errorf("关闭临时文件失败: %w", err)
|
||||
}
|
||||
|
||||
return tempPath, cleanup, nil
|
||||
}
|
||||
|
||||
func (p *S3Provider) Delete(ctx context.Context, key string) error {
|
||||
input := &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(p.bucket),
|
||||
Key: aws.String(key),
|
||||
}
|
||||
|
||||
_, err := p.client.DeleteObjectWithContext(ctx, input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除文件失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *S3Provider) Exists(ctx context.Context, key string) (bool, error) {
|
||||
input := &s3.HeadObjectInput{
|
||||
Bucket: aws.String(p.bucket),
|
||||
Key: aws.String(key),
|
||||
}
|
||||
|
||||
_, err := p.client.HeadObjectWithContext(ctx, input)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "NotFound") || strings.Contains(err.Error(), "404") {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("检查文件存在性失败: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (p *S3Provider) GetUploadURL(ctx context.Context, key string, contentType string, expires time.Duration) (string, error) {
|
||||
input := &s3.PutObjectInput{
|
||||
Bucket: aws.String(p.bucket),
|
||||
Key: aws.String(key),
|
||||
ContentType: aws.String(contentType),
|
||||
}
|
||||
|
||||
req, _ := p.client.PutObjectRequest(input)
|
||||
url, err := req.Presign(expires)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("生成上传预签名 URL 失败: %w", err)
|
||||
}
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func (p *S3Provider) GetDownloadURL(ctx context.Context, key string, expires time.Duration) (string, error) {
|
||||
input := &s3.GetObjectInput{
|
||||
Bucket: aws.String(p.bucket),
|
||||
Key: aws.String(key),
|
||||
}
|
||||
|
||||
req, _ := p.client.GetObjectRequest(input)
|
||||
url, err := req.Presign(expires)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("生成下载预签名 URL 失败: %w", err)
|
||||
}
|
||||
return url, nil
|
||||
}
|
||||
Reference in New Issue
Block a user