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 }