package logic import ( "context" "errors" "io" "log" "mime/multipart" "os" "path/filepath" "strings" "time" "bsm/full/module/base/fts/internal/config" "bsm/full/module/base/fts/internal/models" "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) // 拼接后校验路径必须位于配置的上传根目录内,防止路径穿越 saveDir, err := safeJoin(config.Spec.Local.UploadDir, bucket, subdirpath) if err != nil { log.Println("存储路径非法:", err) return err } // 创建目录并确保权限正确 if err = os.MkdirAll(saveDir, 0755); err != nil { log.Println("目录创建失败:", err) return errors.New("目录创建失败") } // 保存文件到指定路径 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) return err } defer file.Close() src, err := fh.Open() if err != nil { log.Println("文件打开失败:", err) return err } defer src.Close() if _, err = io.Copy(file, src); err != nil { log.Println("文件保存失败:", err) return err } // 再次确认文件权限 if err = os.Chmod(savePath, 0644); err != nil { log.Printf("警告: 设置文件权限失败: %v", err) return err } 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) { minioClient, err := newMinioClient() 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 } // 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 } func NewSubdir(identity string) string { ym := time.Now().Format("2006-01") return ym + "/" + identity[0:2] }