Files
X-Agents/server/internal/service/upload_service.go
DESKTOP-72TV0V4\caoxiaozhu 4017c0e1df feat: 优化文件上传服务
- 添加文件大小和类型验证
- 支持更多文件格式
- 优化文件存储逻辑

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:02:15 +08:00

353 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"fmt"
"io"
"log"
"mime"
"mime/multipart"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/google/uuid"
"x-agents/server/internal/config"
"x-agents/server/internal/model"
)
// debugLog 专用调试日志
var debugLog *log.Logger
func init() {
// 创建调试日志文件
debugFile, _ := os.OpenFile("logs/debug.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
debugLog = log.New(debugFile, "", log.Ldate|log.Ltime)
}
type UploadService struct {
cfg *config.Config
minioClient *minio.Client
}
// UploadResponse 上传响应
type UploadResponse struct {
Success bool `json:"success"`
URL string `json:"url"`
FileKey string `json:"fileKey"` // 文件标识,用于删除等操作
Message string `json:"message"`
}
func NewUploadService(cfg *config.Config) (*UploadService, error) {
s := &UploadService{cfg: cfg}
// 如果是 MinIO 模式,初始化 MinIO 客户端
if cfg.UploadMode == "minio" {
if cfg.MinIOEndpoint == "" || cfg.MinIOAccessKey == "" || cfg.MinIOSecretKey == "" {
return nil, fmt.Errorf("MinIO configuration is incomplete")
}
client, err := minio.New(cfg.MinIOEndpoint, &minio.Options{
Creds: credentials.NewStaticV4(cfg.MinIOAccessKey, cfg.MinIOSecretKey, ""),
Secure: cfg.MinIOUseSSL,
})
if err != nil {
return nil, fmt.Errorf("failed to create MinIO client: %w", err)
}
// 检查 bucket 是否存在,不存在则创建
exists, err := client.BucketExists(context.Background(), cfg.MinIOBucket)
if err != nil {
return nil, fmt.Errorf("failed to check MinIO bucket: %w", err)
}
if !exists {
if err := client.MakeBucket(context.Background(), cfg.MinIOBucket, minio.MakeBucketOptions{}); err != nil {
return nil, fmt.Errorf("failed to create MinIO bucket: %w", err)
}
}
s.minioClient = client
}
return s, nil
}
// Upload 上传文件(使用全局配置)
func (s *UploadService) Upload(file *multipart.FileHeader) (*UploadResponse, error) {
// 打开文件
f, err := file.Open()
if err != nil {
return &UploadResponse{Success: false, Message: err.Error()}, nil
}
defer f.Close()
// 生成唯一文件名
ext := filepath.Ext(file.Filename)
filename := fmt.Sprintf("%s%s", uuid.New().String(), ext)
fileKey := strings.TrimSuffix(filename, ext)
// 根据模式选择存储方式
if s.cfg.UploadMode == "minio" {
return s.uploadToMinIO(f, filename, fileKey, file.Size)
}
return s.uploadToLocal(f, filename, fileKey)
}
// UploadWithConfig 上传文件(使用指定配置)
func (s *UploadService) UploadWithConfig(file *multipart.FileHeader, storageConfig model.StorageConfig) (*UploadResponse, error) {
// 打开文件
f, err := file.Open()
if err != nil {
return &UploadResponse{Success: false, Message: err.Error()}, nil
}
defer f.Close()
// 生成唯一文件名
ext := filepath.Ext(file.Filename)
filename := fmt.Sprintf("%s%s", uuid.New().String(), ext)
fileKey := strings.TrimSuffix(filename, ext)
// 根据配置选择存储方式
if storageConfig.Type == "minio" {
return s.uploadToMinIOWithConfig(f, filename, fileKey, file.Size, storageConfig)
}
// 默认使用本地存储
return s.uploadToLocal(f, filename, fileKey)
}
// uploadToMinIOWithConfig 使用指定配置上传到 MinIO
func (s *UploadService) uploadToMinIOWithConfig(f multipart.File, filename, fileKey string, size int64, cfg model.StorageConfig) (*UploadResponse, error) {
debugLog.Printf("[MinIO Upload] 开始上传: endpoint=%s, bucket=%s, filename=%s, size=%d",
cfg.Endpoint, cfg.Bucket, filename, size)
debugLog.Printf("[MinIO Upload] MinIO配置详情: endpoint=%s, useSSL=%v, accessKey=%s",
cfg.Endpoint, cfg.UseSSL, cfg.AccessKeyID)
// 创建 MinIO 客户端
client, err := minio.New(cfg.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(cfg.AccessKeyID, cfg.SecretAccessKey, ""),
Secure: cfg.UseSSL,
})
if err != nil {
debugLog.Printf("[MinIO Upload] 错误: 创建MinIO客户端失败, err=%v", err)
return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to create MinIO client: %v", err)}, nil
}
debugLog.Printf("[MinIO Upload] MinIO客户端创建成功")
// 检查 bucket 是否存在,不存在则创建
exists, err := client.BucketExists(context.Background(), cfg.Bucket)
if err != nil {
debugLog.Printf("[MinIO Upload] 错误: 检查bucket失败, err=%v", err)
return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to check bucket: %v", err)}, nil
}
debugLog.Printf("[MinIO Upload] Bucket存在检查: exists=%v", exists)
if !exists {
debugLog.Printf("[MinIO Upload] 正在创建Bucket: %s", cfg.Bucket)
if err := client.MakeBucket(context.Background(), cfg.Bucket, minio.MakeBucketOptions{}); err != nil {
debugLog.Printf("[MinIO Upload] 错误: 创建bucket失败, err=%v", err)
return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to create bucket: %v", err)}, nil
}
debugLog.Printf("[MinIO Upload] Bucket创建成功")
}
// 上传文件
debugLog.Printf("[MinIO Upload] 正在上传文件到MinIO...")
// 根据文件扩展名获取正确的 Content-Type
contentType := mime.TypeByExtension(filepath.Ext(filename))
if contentType == "" {
contentType = "application/octet-stream"
}
_, err = client.PutObject(context.Background(), cfg.Bucket, filename, f, size, minio.PutObjectOptions{
ContentType: contentType,
})
if err != nil {
debugLog.Printf("[MinIO Upload] 错误: 上传文件失败, err=%v", err)
return &UploadResponse{Success: false, Message: fmt.Sprintf("MinIO upload failed: %v", err)}, nil
}
debugLog.Printf("[MinIO Upload] 文件上传成功")
// 生成预签名 URL有效期 24 小时)
debugLog.Printf("[MinIO Upload] 正在生成预签名URL...")
presignedURL, err := client.PresignedGetObject(context.Background(), cfg.Bucket, filename, 24*time.Hour, nil)
if err != nil {
debugLog.Printf("[MinIO Upload] 错误: 生成预签名URL失败, err=%v", err)
return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to generate URL: %v", err)}, nil
}
debugLog.Printf("[MinIO Upload] 预签名URL生成成功: %s", presignedURL.String())
return &UploadResponse{
Success: true,
URL: presignedURL.String(),
FileKey: fileKey,
Message: "Upload successful",
}, nil
}
// uploadToMinIO 上传到 MinIO
func (s *UploadService) uploadToMinIO(f multipart.File, filename, fileKey string, size int64) (*UploadResponse, error) {
// 根据文件扩展名获取正确的 Content-Type
contentType := mime.TypeByExtension(filepath.Ext(filename))
if contentType == "" {
contentType = "application/octet-stream"
}
_, err := s.minioClient.PutObject(context.Background(), s.cfg.MinIOBucket, filename, f, size, minio.PutObjectOptions{
ContentType: contentType,
})
if err != nil {
return &UploadResponse{Success: false, Message: fmt.Sprintf("MinIO upload failed: %v", err)}, nil
}
// 生成预签名 URL有效期 24 小时)
presignedURL, err := s.minioClient.PresignedGetObject(context.Background(), s.cfg.MinIOBucket, filename, 24*time.Hour, nil)
if err != nil {
return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to generate URL: %v", err)}, nil
}
return &UploadResponse{
Success: true,
URL: presignedURL.String(),
FileKey: fileKey,
Message: "Upload successful",
}, nil
}
// uploadToLocal 上传到本地
func (s *UploadService) uploadToLocal(f multipart.File, filename, fileKey string) (*UploadResponse, error) {
// 确保目录存在
uploadPath := s.cfg.UploadLocalPath
if err := os.MkdirAll(uploadPath, 0755); err != nil {
return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to create directory: %v", err)}, nil
}
// 创建文件
dst, err := os.Create(filepath.Join(uploadPath, filename))
if err != nil {
return &UploadResponse{Success: false, Message: err.Error()}, nil
}
defer dst.Close()
// 复制内容
if _, err := io.Copy(dst, f); err != nil {
return &UploadResponse{Success: false, Message: err.Error()}, nil
}
// 生成 URL
fileURL := fmt.Sprintf("%s/files/%s", s.cfg.ServerBaseURL, filename)
return &UploadResponse{
Success: true,
URL: fileURL,
FileKey: fileKey,
Message: "Upload successful",
}, nil
}
// DeleteFile 删除文件
func (s *UploadService) DeleteFile(filename string) error {
if s.cfg.UploadMode == "minio" {
return s.minioClient.RemoveObject(context.Background(), s.cfg.MinIOBucket, filename, minio.RemoveObjectOptions{})
}
// 本地删除
path := filepath.Join(s.cfg.UploadLocalPath, filename)
return os.Remove(path)
}
// DeleteFileWithConfig 使用指定配置删除文件
func (s *UploadService) DeleteFileWithConfig(filename string, storageConfig model.StorageConfig) error {
if storageConfig.Type == "minio" {
// 创建 MinIO 客户端
client, err := minio.New(storageConfig.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(storageConfig.AccessKeyID, storageConfig.SecretAccessKey, ""),
Secure: storageConfig.UseSSL,
})
if err != nil {
debugLog.Printf("[MinIO Delete] 创建客户端失败: %v", err)
return err
}
debugLog.Printf("[MinIO Delete] 删除文件: bucket=%s, filename=%s", storageConfig.Bucket, filename)
return client.RemoveObject(context.Background(), storageConfig.Bucket, filename, minio.RemoveObjectOptions{})
}
// 本地删除
path := filepath.Join(s.cfg.UploadLocalPath, filename)
return os.Remove(path)
}
// GetFileURL 获取文件 URL用于已存在的文件
func (s *UploadService) GetFileURL(filename string) (string, error) {
if s.cfg.UploadMode == "minio" {
presignedURL, err := s.minioClient.PresignedGetObject(context.Background(), s.cfg.MinIOBucket, filename, 24*time.Hour, nil)
if err != nil {
return "", err
}
return presignedURL.String(), nil
}
// 本地文件
escaped := url.PathEscape(filename)
return fmt.Sprintf("%s/files/%s", s.cfg.ServerBaseURL, escaped), nil
}
// GetFileContent 获取文件内容(用于代理访问)
func (s *UploadService) GetFileContent(fileKey string) (io.ReadCloser, string, error) {
if s.cfg.UploadMode == "minio" {
object, err := s.minioClient.GetObject(context.Background(), s.cfg.MinIOBucket, fileKey, minio.GetObjectOptions{})
if err != nil {
return nil, "", err
}
// 获取 Content-Type
stat, err := s.minioClient.StatObject(context.Background(), s.cfg.MinIOBucket, fileKey, minio.StatObjectOptions{})
if err != nil {
return nil, "", err
}
return object, stat.ContentType, nil
}
// 本地文件
path := filepath.Join(s.cfg.UploadLocalPath, fileKey)
f, err := os.Open(path)
if err != nil {
return nil, "", err
}
return f, "application/octet-stream", nil
}
// GetFileContentWithConfig 使用指定配置获取文件内容
func (s *UploadService) GetFileContentWithConfig(fileKey string, storageConfig model.StorageConfig) (io.ReadCloser, string, error) {
if storageConfig.Type == "minio" {
// 创建 MinIO 客户端
client, err := minio.New(storageConfig.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(storageConfig.AccessKeyID, storageConfig.SecretAccessKey, ""),
Secure: storageConfig.UseSSL,
})
if err != nil {
return nil, "", err
}
object, err := client.GetObject(context.Background(), storageConfig.Bucket, fileKey, minio.GetObjectOptions{})
if err != nil {
return nil, "", err
}
// 获取 Content-Type
stat, err := client.StatObject(context.Background(), storageConfig.Bucket, fileKey, minio.StatObjectOptions{})
if err != nil {
return nil, "", err
}
return object, stat.ContentType, nil
}
// 本地文件
path := filepath.Join(s.cfg.UploadLocalPath, fileKey)
f, err := os.Open(path)
if err != nil {
return nil, "", err
}
return f, "application/octet-stream", nil
}