feat: 优化文件上传服务
- 添加文件大小和类型验证 - 支持更多文件格式 - 优化文件存储逻辑 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,23 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"x-agents/server/internal/repository"
|
||||||
"x-agents/server/internal/service"
|
"x-agents/server/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UploadHandler struct {
|
type UploadHandler struct {
|
||||||
uploadService *service.UploadService
|
uploadService *service.UploadService
|
||||||
|
knowledgeRepo *repository.KnowledgeRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUploadHandler(uploadService *service.UploadService) *UploadHandler {
|
func NewUploadHandler(uploadService *service.UploadService, knowledgeRepo *repository.KnowledgeRepository) *UploadHandler {
|
||||||
return &UploadHandler{uploadService: uploadService}
|
return &UploadHandler{uploadService: uploadService, knowledgeRepo: knowledgeRepo}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload 上传文件
|
// Upload 上传文件
|
||||||
@@ -58,3 +63,71 @@ func (h *UploadHandler) Delete(c *gin.Context) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "File deleted"})
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "File deleted"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProxyFile 代理文件访问(解决 MinIO 内网地址和 HTTPS 问题)
|
||||||
|
func (h *UploadHandler) ProxyFile(c *gin.Context) {
|
||||||
|
fileKey := c.Query("key")
|
||||||
|
kbID := c.Query("kb_id")
|
||||||
|
|
||||||
|
if fileKey == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "File key is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var content io.Reader
|
||||||
|
var contentType string
|
||||||
|
|
||||||
|
// 如果提供了知识库 ID,使用知识库的存储配置
|
||||||
|
if kbID != "" && h.knowledgeRepo != nil {
|
||||||
|
kb, err := h.knowledgeRepo.FindByID(kbID)
|
||||||
|
if err == nil && kb.StorageConfig.Type == "minio" {
|
||||||
|
reader, ct, err := h.uploadService.GetFileContentWithConfig(fileKey, kb.StorageConfig)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content = reader
|
||||||
|
contentType = ct
|
||||||
|
defer reader.Close()
|
||||||
|
} else {
|
||||||
|
// 使用全局配置
|
||||||
|
reader, ct, err := h.uploadService.GetFileContent(fileKey)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content = reader
|
||||||
|
contentType = ct
|
||||||
|
defer reader.Close()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 使用全局配置
|
||||||
|
reader, ct, err := h.uploadService.GetFileContent(fileKey)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content = reader
|
||||||
|
contentType = ct
|
||||||
|
defer reader.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
// 如果 Content-Type 是 application/octet-stream,根据文件扩展名推断
|
||||||
|
if contentType == "application/octet-stream" {
|
||||||
|
ext := filepath.Ext(fileKey)
|
||||||
|
if detected := mime.TypeByExtension(ext); detected != "" {
|
||||||
|
contentType = detected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentType != "" {
|
||||||
|
c.Header("Content-Type", contentType)
|
||||||
|
} else {
|
||||||
|
c.Header("Content-Type", "application/octet-stream")
|
||||||
|
}
|
||||||
|
c.Header("Content-Disposition", "inline")
|
||||||
|
|
||||||
|
// 直接流式输出文件内容
|
||||||
|
c.DataFromReader(http.StatusOK, -1, contentType, content, nil)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -15,8 +17,18 @@ import (
|
|||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"x-agents/server/internal/config"
|
"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 {
|
type UploadService struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
minioClient *minio.Client
|
minioClient *minio.Client
|
||||||
@@ -64,7 +76,7 @@ func NewUploadService(cfg *config.Config) (*UploadService, error) {
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload 上传文件
|
// Upload 上传文件(使用全局配置)
|
||||||
func (s *UploadService) Upload(file *multipart.FileHeader) (*UploadResponse, error) {
|
func (s *UploadService) Upload(file *multipart.FileHeader) (*UploadResponse, error) {
|
||||||
// 打开文件
|
// 打开文件
|
||||||
f, err := file.Open()
|
f, err := file.Open()
|
||||||
@@ -85,10 +97,106 @@ func (s *UploadService) Upload(file *multipart.FileHeader) (*UploadResponse, err
|
|||||||
return s.uploadToLocal(f, filename, fileKey)
|
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
|
// uploadToMinIO 上传到 MinIO
|
||||||
func (s *UploadService) uploadToMinIO(f multipart.File, filename, fileKey string, size int64) (*UploadResponse, error) {
|
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{
|
_, err := s.minioClient.PutObject(context.Background(), s.cfg.MinIOBucket, filename, f, size, minio.PutObjectOptions{
|
||||||
ContentType: "application/octet-stream",
|
ContentType: contentType,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &UploadResponse{Success: false, Message: fmt.Sprintf("MinIO upload failed: %v", err)}, nil
|
return &UploadResponse{Success: false, Message: fmt.Sprintf("MinIO upload failed: %v", err)}, nil
|
||||||
@@ -150,6 +258,27 @@ func (s *UploadService) DeleteFile(filename string) error {
|
|||||||
return os.Remove(path)
|
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(用于已存在的文件)
|
// GetFileURL 获取文件 URL(用于已存在的文件)
|
||||||
func (s *UploadService) GetFileURL(filename string) (string, error) {
|
func (s *UploadService) GetFileURL(filename string) (string, error) {
|
||||||
if s.cfg.UploadMode == "minio" {
|
if s.cfg.UploadMode == "minio" {
|
||||||
@@ -164,3 +293,60 @@ func (s *UploadService) GetFileURL(filename string) (string, error) {
|
|||||||
escaped := url.PathEscape(filename)
|
escaped := url.PathEscape(filename)
|
||||||
return fmt.Sprintf("%s/files/%s", s.cfg.ServerBaseURL, escaped), nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user