2026-08-09 10:43:45 +08:00
|
|
|
|
package logic
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"io"
|
|
|
|
|
|
"log"
|
|
|
|
|
|
"mime/multipart"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
2026-09-22 21:15:34 +08:00
|
|
|
|
"strings"
|
2026-08-09 10:43:45 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
|
2026-08-09 12:41:08 +08:00
|
|
|
|
"bsm/full/module/base/fts/internal/config"
|
|
|
|
|
|
"bsm/full/module/base/fts/internal/models"
|
2026-08-09 10:43:45 +08:00
|
|
|
|
"git.apinb.com/bsm-sdk/core/utils"
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
"github.com/minio/minio-go/v7"
|
|
|
|
|
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// LocalUpload 本地上传
|
|
|
|
|
|
func LocalUpload(fh *multipart.FileHeader, record *models.FtsRecord, ctx *gin.Context, bucket string) (err error) {
|
|
|
|
|
|
subdirpath := NewSubdir(record.OwnerIdentity)
|
2026-09-22 21:15:34 +08:00
|
|
|
|
// 拼接后校验路径必须位于配置的上传根目录内,防止路径穿越
|
|
|
|
|
|
saveDir, err := safeJoin(config.Spec.Local.UploadDir, bucket, subdirpath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Println("存储路径非法:", err)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
2026-08-09 10:43:45 +08:00
|
|
|
|
|
|
|
|
|
|
// 创建目录并确保权限正确
|
|
|
|
|
|
if err = os.MkdirAll(saveDir, 0755); err != nil {
|
|
|
|
|
|
log.Println("目录创建失败:", err)
|
2026-09-22 21:15:34 +08:00
|
|
|
|
return errors.New("目录创建失败")
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存文件到指定路径
|
|
|
|
|
|
fileName := utils.ULID() + record.Ext
|
|
|
|
|
|
savePath := filepath.Join(saveDir, fileName)
|
|
|
|
|
|
|
|
|
|
|
|
// 使用自定义方式保存文件,确保权限控制
|
|
|
|
|
|
file, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Println("文件创建失败:", err)
|
2026-09-22 21:15:34 +08:00
|
|
|
|
return err
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
|
|
|
|
src, err := fh.Open()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Println("文件打开失败:", err)
|
2026-09-22 21:15:34 +08:00
|
|
|
|
return err
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
defer src.Close()
|
|
|
|
|
|
|
|
|
|
|
|
if _, err = io.Copy(file, src); err != nil {
|
|
|
|
|
|
log.Println("文件保存失败:", err)
|
2026-09-22 21:15:34 +08:00
|
|
|
|
return err
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 再次确认文件权限
|
|
|
|
|
|
if err = os.Chmod(savePath, 0644); err != nil {
|
|
|
|
|
|
log.Printf("警告: 设置文件权限失败: %v", err)
|
2026-09-22 21:15:34 +08:00
|
|
|
|
return err
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
record.LocalPath = savePath
|
|
|
|
|
|
record.SaveName = fileName
|
|
|
|
|
|
record.ResultUrl = config.Spec.Local.Site + "/" + bucket + "/" + subdirpath + "/" + fileName
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// OssUpload 上传文件到指定的MinIO存储桶
|
|
|
|
|
|
func OssUpload(file *multipart.FileHeader, record *models.FtsRecord, c *gin.Context, bucket string) (err error) {
|
2026-09-22 21:15:34 +08:00
|
|
|
|
minioClient, err := newMinioClient()
|
2026-08-09 10:43:45 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置保存格式为: 年/identity/identity后3位+filename
|
|
|
|
|
|
subdirpath := NewSubdir(record.OwnerIdentity)
|
|
|
|
|
|
fileName := utils.ULID() + record.Ext
|
|
|
|
|
|
savePath := filepath.Join(subdirpath, fileName)
|
|
|
|
|
|
|
|
|
|
|
|
// Open the file
|
|
|
|
|
|
src, err := file.Open()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
defer src.Close()
|
|
|
|
|
|
|
|
|
|
|
|
_, err = minioClient.PutObject(context.Background(), bucket, savePath, src, file.Size, minio.PutObjectOptions{ContentType: "application/octet-stream"})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
record.OssPath = savePath
|
|
|
|
|
|
record.ResultUrl = config.Spec.MinioOss.Site + "/" + bucket + "/" + savePath
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 21:15:34 +08:00
|
|
|
|
// RemoveOssObject 删除已上传到MinIO的对象,用于落库失败后的清理
|
|
|
|
|
|
func RemoveOssObject(bucket, objectName string) error {
|
|
|
|
|
|
if bucket == "" || objectName == "" {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
minioClient, err := newMinioClient()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return minioClient.RemoveObject(context.Background(), bucket, objectName, minio.RemoveObjectOptions{})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// newMinioClient 创建MinIO客户端
|
|
|
|
|
|
func newMinioClient() (*minio.Client, error) {
|
|
|
|
|
|
return minio.New(config.Spec.MinioOss.Endpoint, &minio.Options{
|
|
|
|
|
|
Creds: credentials.NewStaticV4(config.Spec.MinioOss.AccessKeyID, config.Spec.MinioOss.AccessKeySecret, ""),
|
|
|
|
|
|
Secure: config.Spec.MinioOss.UseSSL,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// safeJoin 在根目录下安全拼接子路径,拒绝越出根目录的路径,防止路径穿越
|
|
|
|
|
|
func safeJoin(root string, sub ...string) (string, error) {
|
|
|
|
|
|
root = filepath.Clean(root)
|
|
|
|
|
|
target := filepath.Join(append([]string{root}, sub...)...)
|
|
|
|
|
|
if target != root && !strings.HasPrefix(target, root+string(filepath.Separator)) {
|
|
|
|
|
|
return "", errors.New("存储路径越界")
|
|
|
|
|
|
}
|
|
|
|
|
|
return target, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 10:43:45 +08:00
|
|
|
|
func NewSubdir(identity string) string {
|
|
|
|
|
|
ym := time.Now().Format("2006-01")
|
|
|
|
|
|
return ym + "/" + identity[0:2]
|
|
|
|
|
|
}
|