package service import ( "context" "fmt" "io" "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" ) 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) } // uploadToMinIO 上传到 MinIO func (s *UploadService) uploadToMinIO(f multipart.File, filename, fileKey string, size int64) (*UploadResponse, error) { _, err := s.minioClient.PutObject(context.Background(), s.cfg.MinIOBucket, filename, f, size, minio.PutObjectOptions{ ContentType: "application/octet-stream", }) 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) } // 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 }