storage.go 5.0 KB

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