filestore.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. package filestore
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "path/filepath"
  7. "strings"
  8. "sync"
  9. "time"
  10. "github.com/google/uuid"
  11. "github.com/minio/minio-go/v7"
  12. minioUtil "fwh-toolbox-server/utils/minio"
  13. "go.uber.org/zap"
  14. )
  15. const AIFileBucket = "fwh-toolbox-ai-file"
  16. const AIFileExpireDays = 7
  17. // FileRecord 文件记录
  18. type FileRecord struct {
  19. Key string `json:"key"` // MinIO object key
  20. FileName string `json:"fileName"` // 原始文件名
  21. Path string `json:"path"` // Agent 请求的路径
  22. Size int64 `json:"size"` // 文件大小
  23. CreatedAt int64 `json:"createdAt"` // 创建时间戳
  24. }
  25. // FileStore 会话级别的虚拟文件系统
  26. type FileStore struct {
  27. prefix string // "upload" 或 "download",用于区分文件来源
  28. mu sync.RWMutex
  29. files map[string]*FileRecord // path → FileRecord(同路径覆盖更新)
  30. }
  31. // NewFileStore 创建文件存储
  32. // prefix: "upload" 或 "download",对应 MinIO 中的子目录前缀
  33. func NewFileStore(prefix string) *FileStore {
  34. return &FileStore{
  35. prefix: prefix,
  36. files: make(map[string]*FileRecord),
  37. }
  38. }
  39. // WriteFile 将内容上传到 MinIO,返回文件记录
  40. // objectKey 格式: {prefix}/{userId}/{sessionId}/{uuid}_{fileName}
  41. func (fs *FileStore) WriteFile(userId, sessionId, path, content string) (*FileRecord, error) {
  42. fileName := filepath.Base(path)
  43. uniqueId := uuid.New().String()[:8]
  44. objectKey := fmt.Sprintf("%s/%s/%s/%s_%s", fs.prefix, userId, sessionId, uniqueId, fileName)
  45. // 上传到 MinIO(带7天过期)
  46. reader := strings.NewReader(content)
  47. ctx := context.Background()
  48. _, err := minioUtil.Client.PutObject(ctx, AIFileBucket, objectKey, reader, int64(len(content)), minio.PutObjectOptions{
  49. ContentType: DetectContentType(fileName),
  50. Expires: time.Now().Add(AIFileExpireDays * 24 * time.Hour),
  51. })
  52. if err != nil {
  53. return nil, fmt.Errorf("上传文件到MinIO失败: %w", err)
  54. }
  55. record := &FileRecord{
  56. Key: objectKey,
  57. FileName: fileName,
  58. Path: path,
  59. Size: int64(len(content)),
  60. CreatedAt: time.Now().Unix(),
  61. }
  62. // 存入内存映射(同路径覆盖)
  63. fs.mu.Lock()
  64. fs.files[path] = record
  65. fs.mu.Unlock()
  66. zap.L().Info("[FileStore] 文件已保存到MinIO",
  67. zap.String("prefix", fs.prefix),
  68. zap.String("path", path),
  69. zap.String("key", objectKey),
  70. zap.String("fileName", fileName),
  71. )
  72. return record, nil
  73. }
  74. // WriteBytes 将字节内容上传到 MinIO(支持二进制文件)
  75. // objectKey 格式: {prefix}/{userId}/{timestamp}_{uuid}_{fileName}
  76. func (fs *FileStore) WriteBytes(userId, fileName string, data []byte, contentType string) (*FileRecord, error) {
  77. uniqueId := uuid.New().String()[:8]
  78. timestamp := time.Now().Unix()
  79. objectKey := fmt.Sprintf("%s/%s/%d_%s_%s", fs.prefix, userId, timestamp, uniqueId, fileName)
  80. // 虚拟路径(供 Agent readTextFile 使用)
  81. virtualPath := fmt.Sprintf("/uploads/%s", fileName)
  82. reader := strings.NewReader(string(data))
  83. ctx := context.Background()
  84. if contentType == "" {
  85. contentType = DetectContentType(fileName)
  86. }
  87. _, err := minioUtil.Client.PutObject(ctx, AIFileBucket, objectKey, reader, int64(len(data)), minio.PutObjectOptions{
  88. ContentType: contentType,
  89. Expires: time.Now().Add(AIFileExpireDays * 24 * time.Hour),
  90. })
  91. if err != nil {
  92. return nil, fmt.Errorf("上传文件到MinIO失败: %w", err)
  93. }
  94. record := &FileRecord{
  95. Key: objectKey,
  96. FileName: fileName,
  97. Path: virtualPath,
  98. Size: int64(len(data)),
  99. CreatedAt: time.Now().Unix(),
  100. }
  101. // 存入内存映射
  102. fs.mu.Lock()
  103. fs.files[virtualPath] = record
  104. fs.mu.Unlock()
  105. zap.L().Info("[FileStore] 文件已保存到MinIO",
  106. zap.String("prefix", fs.prefix),
  107. zap.String("path", virtualPath),
  108. zap.String("key", objectKey),
  109. zap.String("fileName", fileName),
  110. )
  111. return record, nil
  112. }
  113. // ReadFile 从 MinIO 读取文件内容
  114. func (fs *FileStore) ReadFile(path string) (string, error) {
  115. fs.mu.RLock()
  116. record, ok := fs.files[path]
  117. fs.mu.RUnlock()
  118. if !ok {
  119. return "", fmt.Errorf("文件不存在: %s", path)
  120. }
  121. ctx := context.Background()
  122. obj, err := minioUtil.Client.GetObject(ctx, AIFileBucket, record.Key, minio.GetObjectOptions{})
  123. if err != nil {
  124. return "", fmt.Errorf("从MinIO读取文件失败: %w", err)
  125. }
  126. defer obj.Close()
  127. data, err := io.ReadAll(obj)
  128. if err != nil {
  129. return "", fmt.Errorf("读取文件内容失败: %w", err)
  130. }
  131. return string(data), nil
  132. }
  133. // HasFile 检查路径是否有文件记录
  134. func (fs *FileStore) HasFile(path string) bool {
  135. fs.mu.RLock()
  136. defer fs.mu.RUnlock()
  137. _, ok := fs.files[path]
  138. return ok
  139. }
  140. // GetFiles 获取所有文件记录
  141. func (fs *FileStore) GetFiles() []FileRecord {
  142. fs.mu.RLock()
  143. defer fs.mu.RUnlock()
  144. result := make([]FileRecord, 0, len(fs.files))
  145. for _, record := range fs.files {
  146. result = append(result, *record)
  147. }
  148. return result
  149. }
  150. // GetFileByKey 按 MinIO key 查找文件记录
  151. func (fs *FileStore) GetFileByKey(key string) *FileRecord {
  152. fs.mu.RLock()
  153. defer fs.mu.RUnlock()
  154. for _, record := range fs.files {
  155. if record.Key == key {
  156. return record
  157. }
  158. }
  159. return nil
  160. }
  161. // Clear 清除所有文件记录(不删除 MinIO 文件)
  162. func (fs *FileStore) Clear() {
  163. fs.mu.Lock()
  164. fs.files = make(map[string]*FileRecord)
  165. fs.mu.Unlock()
  166. }
  167. // DetectContentType 根据文件扩展名推断 Content-Type
  168. func DetectContentType(fileName string) string {
  169. ext := strings.ToLower(filepath.Ext(fileName))
  170. switch ext {
  171. case ".txt":
  172. return "text/plain; charset=utf-8"
  173. case ".json":
  174. return "application/json"
  175. case ".html", ".htm":
  176. return "text/html"
  177. case ".css":
  178. return "text/css"
  179. case ".js":
  180. return "application/javascript"
  181. case ".ts":
  182. return "application/typescript"
  183. case ".go":
  184. return "text/x-go"
  185. case ".py":
  186. return "text/x-python"
  187. case ".md":
  188. return "text/markdown"
  189. case ".csv":
  190. return "text/csv"
  191. case ".xml":
  192. return "application/xml"
  193. case ".yaml", ".yml":
  194. return "text/yaml"
  195. case ".sh":
  196. return "text/x-shellscript"
  197. case ".vue":
  198. return "text/x-vue"
  199. case ".sql":
  200. return "text/x-sql"
  201. case ".log":
  202. return "text/plain; charset=utf-8"
  203. default:
  204. return "application/octet-stream"
  205. }
  206. }