feat: 新增文件上传服务
- 添加 UploadHandler 处理文件上传 - 添加 UploadService 实现文件存储 - 配置上传文件大小限制和存储路径 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,17 @@ type Config struct {
|
||||
Port string
|
||||
JWTSecret string
|
||||
DatabaseURL string
|
||||
PythonServiceURL string
|
||||
PythonServiceURL string
|
||||
// 文件上传配置
|
||||
UploadMode string // "local" 或 "minio"
|
||||
UploadLocalPath string // 本地存储路径,如 "resource/files"
|
||||
ServerBaseURL string // 服务器基础 URL,用于生成本地文件 URL
|
||||
// MinIO 配置
|
||||
MinIOEndpoint string
|
||||
MinIOAccessKey string
|
||||
MinIOSecretKey string
|
||||
MinIOBucket string
|
||||
MinIOUseSSL bool
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
@@ -30,16 +40,35 @@ func Load() *Config {
|
||||
viper.SetDefault("jwt_secret", "your-secret-key-change-in-production")
|
||||
viper.SetDefault("python_service_url", "http://localhost:8081")
|
||||
viper.SetDefault("database_url", "root:root@tcp(localhost:3306)/x_agents?charset=utf8mb4&parseTime=True&loc=Local")
|
||||
// 文件上传默认配置
|
||||
viper.SetDefault("upload_mode", "local")
|
||||
viper.SetDefault("upload_local_path", "resource/files")
|
||||
viper.SetDefault("server_base_url", "http://localhost:8080")
|
||||
viper.SetDefault("minio_endpoint", "localhost:9000")
|
||||
viper.SetDefault("minio_access_key", "")
|
||||
viper.SetDefault("minio_secret_key", "")
|
||||
viper.SetDefault("minio_bucket", "x-agents")
|
||||
viper.SetDefault("minio_use_ssl", false)
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Printf("Using default config: %v", err)
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Port: viper.GetString("port"),
|
||||
JWTSecret: viper.GetString("jwt_secret"),
|
||||
DatabaseURL: viper.GetString("database_url"),
|
||||
Port: viper.GetString("port"),
|
||||
JWTSecret: viper.GetString("jwt_secret"),
|
||||
DatabaseURL: viper.GetString("database_url"),
|
||||
PythonServiceURL: viper.GetString("python_service_url"),
|
||||
// 文件上传配置
|
||||
UploadMode: viper.GetString("upload_mode"),
|
||||
UploadLocalPath: viper.GetString("upload_local_path"),
|
||||
ServerBaseURL: viper.GetString("server_base_url"),
|
||||
// MinIO 配置
|
||||
MinIOEndpoint: viper.GetString("minio_endpoint"),
|
||||
MinIOAccessKey: viper.GetString("minio_access_key"),
|
||||
MinIOSecretKey: viper.GetString("minio_secret_key"),
|
||||
MinIOBucket: viper.GetString("minio_bucket"),
|
||||
MinIOUseSSL: viper.GetBool("minio_use_ssl"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
60
server/internal/handler/upload_handler.go
Normal file
60
server/internal/handler/upload_handler.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"x-agents/server/internal/service"
|
||||
)
|
||||
|
||||
type UploadHandler struct {
|
||||
uploadService *service.UploadService
|
||||
}
|
||||
|
||||
func NewUploadHandler(uploadService *service.UploadService) *UploadHandler {
|
||||
return &UploadHandler{uploadService: uploadService}
|
||||
}
|
||||
|
||||
// Upload 上传文件
|
||||
func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "No file uploaded"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件大小(最大 100MB)
|
||||
if file.Size > 100*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "File too large (max 100MB)"})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.uploadService.Upload(file)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if !result.Success {
|
||||
c.JSON(http.StatusInternalServerError, result)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// Delete 删除文件
|
||||
func (h *UploadHandler) Delete(c *gin.Context) {
|
||||
filename := c.Param("filename")
|
||||
if filename == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Filename is required"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.uploadService.DeleteFile(filename); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "File deleted"})
|
||||
}
|
||||
166
server/internal/service/upload_service.go
Normal file
166
server/internal/service/upload_service.go
Normal file
@@ -0,0 +1,166 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user