storage.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. package infra
  2. import (
  3. "fmt"
  4. "io"
  5. "net/url"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "time"
  10. )
  11. // StorageService 文件存储服务接口(可扩展为云存储)
  12. type StorageService interface {
  13. // SaveAvatar 保存头像文件,返回文件URL
  14. SaveAvatar(userID uint, file io.Reader, filename string) (string, error)
  15. // SaveMessageFile 保存消息文件,返回文件URL
  16. // conversationID: 对话ID,用于组织文件目录
  17. // file: 文件内容
  18. // filename: 原始文件名
  19. SaveMessageFile(conversationID uint, file io.Reader, filename string) (string, error)
  20. // ReadMessageFile 根据消息文件的 URL 或路径读取文件内容(用于多模态:识图等)
  21. // fileURLOrPath 为创建消息时返回的 file_url,可为相对路径如 /uploads/messages/1/xxx.jpg 或完整 URL
  22. ReadMessageFile(fileURLOrPath string) ([]byte, error)
  23. // DeleteFile 删除文件
  24. DeleteFile(fileURL string) error
  25. // GetFileURL 获取文件的完整URL
  26. GetFileURL(filePath string) string
  27. }
  28. // LocalStorageService 本地文件存储服务
  29. type LocalStorageService struct {
  30. baseDir string // 基础目录
  31. publicPath string // 公共访问路径
  32. }
  33. // NewLocalStorageService 创建本地存储服务实例
  34. func NewLocalStorageService(baseDir, publicPath string) *LocalStorageService {
  35. // 确保基础目录存在
  36. if err := os.MkdirAll(baseDir, 0755); err != nil {
  37. panic(fmt.Sprintf("创建存储目录失败: %v", err))
  38. }
  39. // 确保头像目录存在
  40. avatarDir := filepath.Join(baseDir, "avatars")
  41. if err := os.MkdirAll(avatarDir, 0755); err != nil {
  42. panic(fmt.Sprintf("创建头像目录失败: %v", err))
  43. }
  44. return &LocalStorageService{
  45. baseDir: baseDir,
  46. publicPath: publicPath,
  47. }
  48. }
  49. // SaveAvatar 保存头像文件
  50. func (s *LocalStorageService) SaveAvatar(userID uint, file io.Reader, filename string) (string, error) {
  51. // 获取文件扩展名
  52. ext := filepath.Ext(filename)
  53. if ext == "" {
  54. ext = ".jpg" // 默认使用 jpg
  55. }
  56. // 生成唯一文件名:user_{userID}_{timestamp}{ext}
  57. timestamp := time.Now().Unix()
  58. newFilename := fmt.Sprintf("user_%d_%d%s", userID, timestamp, ext)
  59. // 保存到 avatars 目录
  60. avatarDir := filepath.Join(s.baseDir, "avatars")
  61. filePath := filepath.Join(avatarDir, newFilename)
  62. // 创建文件
  63. dst, err := os.Create(filePath)
  64. if err != nil {
  65. return "", fmt.Errorf("创建文件失败: %w", err)
  66. }
  67. defer dst.Close()
  68. // 复制文件内容
  69. if _, err := io.Copy(dst, file); err != nil {
  70. return "", fmt.Errorf("保存文件失败: %w", err)
  71. }
  72. // 返回相对路径(用于构建URL)
  73. relativePath := filepath.Join("avatars", newFilename)
  74. return s.GetFileURL(relativePath), nil
  75. }
  76. // ReadMessageFile 根据消息文件的 URL 或路径读取文件内容。
  77. func (s *LocalStorageService) ReadMessageFile(fileURLOrPath string) ([]byte, error) {
  78. pathPart := fileURLOrPath
  79. if strings.Contains(fileURLOrPath, "://") {
  80. u, err := url.Parse(fileURLOrPath)
  81. if err != nil {
  82. return nil, fmt.Errorf("解析文件 URL 失败: %w", err)
  83. }
  84. pathPart = u.Path
  85. }
  86. pathPart = strings.TrimPrefix(pathPart, "/")
  87. publicPathTrimmed := strings.TrimPrefix(strings.TrimSuffix(s.publicPath, "/"), "/")
  88. if publicPathTrimmed != "" && strings.HasPrefix(pathPart, publicPathTrimmed+"/") {
  89. pathPart = strings.TrimPrefix(pathPart, publicPathTrimmed+"/")
  90. } else if publicPathTrimmed != "" && pathPart == publicPathTrimmed {
  91. pathPart = ""
  92. }
  93. if pathPart == "" {
  94. return nil, fmt.Errorf("无法从 URL 解析出相对路径: %s", fileURLOrPath)
  95. }
  96. fullPath := filepath.Join(s.baseDir, pathPart)
  97. return os.ReadFile(fullPath)
  98. }
  99. // DeleteFile 删除文件
  100. func (s *LocalStorageService) DeleteFile(fileURL string) error {
  101. // 从URL中提取文件路径
  102. // 假设URL格式为: /uploads/avatars/filename.jpg
  103. // 需要去掉 /uploads/ 前缀,得到相对路径
  104. relativePath := fileURL
  105. if len(s.publicPath) > 0 && len(fileURL) > len(s.publicPath) {
  106. if fileURL[:len(s.publicPath)] == s.publicPath {
  107. relativePath = fileURL[len(s.publicPath):]
  108. // 去掉开头的 /
  109. if len(relativePath) > 0 && relativePath[0] == '/' {
  110. relativePath = relativePath[1:]
  111. }
  112. }
  113. }
  114. filePath := filepath.Join(s.baseDir, relativePath)
  115. if err := os.Remove(filePath); err != nil {
  116. if os.IsNotExist(err) {
  117. return nil // 文件不存在,认为删除成功
  118. }
  119. return fmt.Errorf("删除文件失败: %w", err)
  120. }
  121. return nil
  122. }
  123. // SaveMessageFile 保存消息文件
  124. func (s *LocalStorageService) SaveMessageFile(conversationID uint, file io.Reader, filename string) (string, error) {
  125. // 获取文件扩展名
  126. ext := filepath.Ext(filename)
  127. if ext == "" {
  128. ext = ".bin" // 默认扩展名
  129. }
  130. // 生成唯一文件名:{timestamp}_{原始文件名}
  131. timestamp := time.Now().Unix()
  132. // 清理文件名,移除特殊字符
  133. safeFilename := filepath.Base(filename)
  134. if len(safeFilename) > 100 {
  135. // 文件名过长,截断
  136. safeFilename = safeFilename[:100]
  137. }
  138. newFilename := fmt.Sprintf("%d_%s", timestamp, safeFilename)
  139. // 按对话ID组织目录:messages/{conversationID}/
  140. messageDir := filepath.Join(s.baseDir, "messages", fmt.Sprintf("%d", conversationID))
  141. if err := os.MkdirAll(messageDir, 0755); err != nil {
  142. return "", fmt.Errorf("创建消息文件目录失败: %w", err)
  143. }
  144. filePath := filepath.Join(messageDir, newFilename)
  145. // 创建文件
  146. dst, err := os.Create(filePath)
  147. if err != nil {
  148. return "", fmt.Errorf("创建文件失败: %w", err)
  149. }
  150. defer dst.Close()
  151. // 复制文件内容
  152. if _, err := io.Copy(dst, file); err != nil {
  153. return "", fmt.Errorf("保存文件失败: %w", err)
  154. }
  155. // 返回相对路径(用于构建URL)
  156. relativePath := filepath.Join("messages", fmt.Sprintf("%d", conversationID), newFilename)
  157. return s.GetFileURL(relativePath), nil
  158. }
  159. // GetFileURL 获取文件的完整URL
  160. func (s *LocalStorageService) GetFileURL(filePath string) string {
  161. // 确保路径使用正斜杠(用于URL)
  162. urlPath := filepath.ToSlash(filePath)
  163. // 如果 publicPath 为空,返回相对路径
  164. if s.publicPath == "" {
  165. return "/" + urlPath
  166. }
  167. // 确保 publicPath 以 / 结尾
  168. publicPath := s.publicPath
  169. if publicPath[len(publicPath)-1] != '/' {
  170. publicPath += "/"
  171. }
  172. // 确保 urlPath 不以 / 开头
  173. if len(urlPath) > 0 && urlPath[0] == '/' {
  174. urlPath = urlPath[1:]
  175. }
  176. return publicPath + urlPath
  177. }