[Go]GopherAI项目学习记录:文件上传功能拓展

一个网页AI聊天,图像识别项目,使用到的技术栈有,Gin框架,GORM,rabbitmq,redis,eino框架,Vue框架等

项目地址:https://github.com/youngyangyang04/GopherAI


GopherAI第二版在第一版的基础上扩展了RAG服务,文件上传服务,MCP服务调用工具,TTS语言识别功能,下面进行一一梳理

由于引入了RAG,需要用户上传文件以作为检索和生成的知识来源,文件服务正式为此设计

路由层

新增一个api用于文件上传,也需要jwt鉴权

{
        FileGroup := enterRouter.Group("/file")
        FileGroup.Use(jwt.Auth())
        FileRouter(FileGroup)
}
func FileRouter(r *gin.RouterGroup) {
    r.POST("/upload", file.UploadRagFile)
}

控制器层

首先定义了api的响应结构体i,便于参数绑定,相比基础的api响应多了一个字符串用于表示文件上传到服务器后存放的路径

type (
    UploadFileResponse struct {
        FilePath string `json:"file_path,omitempty"`
        controller.Response
    }
)

然后就是handler函数了,从前端发来的请求中读取文件之后,调用服务层的file.UploadRagFile函数把文件上传到服务器

func UploadRagFile(c *gin.Context) {
    res := new(UploadFileResponse)
    uploadedFile, err := c.FormFile("file")     // 从form-data里取文件
    if err != nil {
        log.Println("FormFile fail ", err)
        c.JSON(http.StatusOK, res.CodeOf(code.CodeInvalidParams))
        return
    }

    username := c.GetString("userName")         // from jwt
    if username == "" {
        log.Println("Username not found in context")
        c.JSON(http.StatusOK, res.CodeOf(code.CodeInvalidToken))
        return
    }

    // 存储文件,indexer 会在 service 层根据实际文件名创建
    filePath, err := file.UploadRagFile(username, uploadedFile)
    if err != nil {
        log.Println("UploadFile fail ", err)
        c.JSON(http.StatusOK, res.CodeOf(code.CodeServerBusy))
        return
    }

    res.Success()
    res.FilePath = filePath
    c.JSON(http.StatusOK, res)
}

服务层

文件上传服务只有一个核心函数UploadRagFile,首先是校验文件名和类型,随后为用户创建一个用户文件夹,用于存放用户的RAG文件,随后清空用户文件夹以及RedisSearch中的索引

生成最终存储的RAG文件名,命名规则:uuid+原始后缀,存放在用户文件夹

打开file,拷贝到用户RAG文件中

最后创建RAG索引器并对文件向量化,读取文件内容并创建向量索引

// 上传rag相关文件(这里只允许文本文件)
// 其实可以直接将其向量化进行保存,但这边依旧存储到服务器上以便后续可以在服务器上查看历史RAG文件
func UploadRagFile(username string, file *multipart.FileHeader) (string, error) {
    // 校验文件类型和文件名,只允许文本文件
    if err := utils.ValidateFile(file); err != nil {
        log.Printf("File validation failed: %v", err)
        return "", err
    }

    // 创建用户目录,存放该用户上传的RAG文件
    userDir := filepath.Join("uploads", username)
    if err := os.MkdirAll(userDir, 0755); err != nil {
        log.Printf("Failed to create user directory %s: %v", userDir, err)
        return "", err
    }

    // 删除用户目录中的所有现有文件及其索引(每个用户只能有一个文件)
    files, err := os.ReadDir(userDir)
    if err == nil {
        for _, f := range files {
            if !f.IsDir() {
                filename := f.Name()
                // 删除该文件对应的 RedisSearch 索引
                if err := rag.DeleteIndex(context.Background(), filename); err != nil {
                    log.Printf("Failed to delete index for %s: %v", filename, err)
                    // 继续执行,不因为索引删除失败而中断文件上传
                }
            }
        }
    }
    // 删除用户目录中的所有文件
    if err := utils.RemoveAllFilesInDir(userDir); err != nil {
        log.Printf("Failed to clean user directory %s: %v", userDir, err)
        return "", err
    }

    // 生成UUID作为唯一文件名
    uuid := utils.GenerateUUID()

    ext := filepath.Ext(file.Filename)              // 获取原文件后缀
    filename := uuid + ext                          // RAG文件命名为uuid+源文件后缀
    filePath := filepath.Join(userDir, filename)    // 获取RAG文件路径

    // 打开上传的文件
    src, err := file.Open()
    if err != nil {
        log.Printf("Failed to open uploaded file: %v", err)
        return "", err
    }
    defer src.Close()

    // 创建用户的RAG文件
    dst, err := os.Create(filePath)
    if err != nil {
        log.Printf("Failed to create destination file %s: %v", filePath, err)
        return "", err
    }
    defer dst.Close()
    // 拷贝数据
    if _, err := io.Copy(dst, src); err != nil {
        log.Printf("Failed to copy file content: %v", err)
        return "", err
    }

    log.Printf("File uploaded successfully: %s", filePath)

    // ==============================================
    // 创建 RAG 索引器并对文件进行向量化
    indexer, err := rag.NewRAGIndexer(filename, config.GetConfig().RagModelConfig.RagEmbeddingModel)
    if err != nil {
        log.Printf("Failed to create RAG indexer: %v", err)
        // 删除已上传的文件
        os.Remove(filePath)
        return "", err
    }

    // 读取文件内容并创建向量索引
    if err := indexer.IndexFile(context.Background(), filePath); err != nil {
        log.Printf("Failed to index file: %v", err)
        // 删除已上传的文件和索引
        os.Remove(filePath)
        rag.DeleteIndex(context.Background(), filename)
        return "", err
    }

    log.Printf("File indexed successfully: %s", filename)
    return filePath, nil
}

使用到的Utils

封装了一个删除文件夹所有文件的工具函数

// RemoveAllFilesInDir 删除目录中的所有文件(不删除子目录)
func RemoveAllFilesInDir(dir string) error {
    entries, err := os.ReadDir(dir)
    if err != nil {
        if os.IsNotExist(err) {
            return nil // 目录不存在就算了
        }
        return err
    }

    for _, entry := range entries {
        if !entry.IsDir() {     // 只删除文件,目录忽略
            filePath := filepath.Join(dir, entry.Name())
            if err := os.Remove(filePath); err != nil {
                return err
            }
        }
    }
    return nil
}

检测RAG文件合法性的工具函数

// ValidateFile 校验文件是否为允许的文本文件(.md 或 .txt)
func ValidateFile(file *multipart.FileHeader) error {
    // 校验文件扩展名
    ext := strings.ToLower(filepath.Ext(file.Filename))
    if ext != ".md" && ext != ".txt" {
        return fmt.Errorf("文件类型不正确,只允许 .md 或 .txt 文件,当前扩展名: %s", ext)
    }

    return nil
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇