package logic import ( "crypto/sha256" "encoding/hex" "io" "log" "mime/multipart" "os" "path/filepath" "regexp" "strings" "bsm/full/module/base/fts/internal/config" "bsm/full/module/base/fts/internal/impl" "bsm/full/module/base/fts/internal/models" "git.apinb.com/bsm-sdk/core/errcode" "git.apinb.com/bsm-sdk/core/infra" "git.apinb.com/bsm-sdk/core/middleware" "git.apinb.com/bsm-sdk/core/types" "git.apinb.com/bsm-sdk/core/utils" "github.com/gin-gonic/gin" ) // bucketPattern bucket名称只允许字母、数字、下划线和中划线 var bucketPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{1,64}$`) // Handler 上传文件 func Handler(c *gin.Context) { var ( provider = strings.ToLower(c.PostForm("provider")) bucket = strings.ToLower(c.PostForm("bucket")) claims *types.JwtClaims err error ) claims, err = middleware.ParseAuth(c) if err != nil { log.Printf("获取当前登录用户信息失败: %v\n", err) infra.Response.Error(c, err) return } if provider == "" || bucket == "" { infra.Response.Error(c, errcode.NewError(400, "参数错误")) return } // 校验 bucket 名称,避免非法字符导致本地路径穿越或写入任意对象存储桶 if !bucketPattern.MatchString(bucket) { infra.Response.Error(c, errcode.NewError(400, "bucket参数错误")) return } // if !strings.Contains(provider, "local") { // log.Println("provider参数错误") // infra.Response.Error(c, errcode.NewError(400, "provider参数错误")) // return // } fh, err := c.FormFile(config.Spec.FtsConfig.InputKey) if err != nil { infra.Response.Error(c, err) return } // 获取文件信息 fileSize := fh.Size if fileSize > config.Spec.FtsConfig.MaxSize { infra.Response.Error(c, errcode.NewError(400, "文件大小超过限制")) return } // 检测是否是允许上传的文件类型 fileExt := filepath.Ext(fh.Filename) if !isAllow(fileExt) { log.Println("不允许上传的文件类型:", fileExt) infra.Response.Error(c, errcode.NewError(501, "不允许上传的文件类型")) return } fileHash, err := chksum(fh) if err != nil { log.Println("文件校验失败:", err) infra.Response.Error(c, err) return } record := models.FtsRecord{ Identity: utils.UUID(), OwnerID: claims.ID, OwnerIdentity: claims.Identity, Name: fh.Filename, Ext: fileExt, Size: uint64(fileSize), Hash: fileHash, Status: 0, } switch provider { case "local": err = LocalUpload(fh, &record, c, bucket) case "minio": err = OssUpload(fh, &record, c, bucket) default: infra.Response.Error(c, errcode.NewError(400, "provider参数错误")) return } if err != nil { infra.Response.Error(c, err) return } if err := impl.DBService.Create(&record).Error; err != nil { // 落库失败时清理已写入的本地文件或对象存储对象,避免残留孤儿文件 removeStoredFile(&record, bucket) infra.Response.Error(c, err) return } infra.Response.Success(c, record) } // removeStoredFile 清理已落盘/已上传的文件。单条 INSERT 无需事务,落库失败时按记录回滚外部存储 func removeStoredFile(record *models.FtsRecord, bucket string) { if record.LocalPath != "" { if err := os.Remove(record.LocalPath); err != nil { log.Printf("清理本地文件失败: %v\n", err) } } if record.OssPath != "" { if err := RemoveOssObject(bucket, record.OssPath); err != nil { log.Printf("清理对象存储文件失败: %v\n", err) } } } func chksum(fh *multipart.FileHeader) (string, error) { // 2. 打开文件读取内容 file, err := fh.Open() if err != nil { log.Println("multipart err:", err) return "", err } defer file.Close() // 3. 计算文件哈希值(sha256) hash := sha256.New() if _, err = io.Copy(hash, file); err != nil { log.Println("哈希计算失败:", err) return "", err } return hex.EncodeToString(hash.Sum(nil)), nil } func isAllow(extName string) bool { for _, allowExt := range config.Spec.FtsConfig.Allows { if extName == allowExt { return true } } return false }