storage.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  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. // DeleteFile 删除文件
  14. DeleteFile(fileURL string) error
  15. // GetFileURL 获取文件的完整URL
  16. GetFileURL(filePath string) string
  17. }
  18. // LocalStorageService 本地文件存储服务
  19. type LocalStorageService struct {
  20. baseDir string // 基础目录
  21. publicPath string // 公共访问路径
  22. }
  23. // NewLocalStorageService 创建本地存储服务实例
  24. func NewLocalStorageService(baseDir, publicPath string) *LocalStorageService {
  25. // 确保基础目录存在
  26. if err := os.MkdirAll(baseDir, 0755); err != nil {
  27. panic(fmt.Sprintf("创建存储目录失败: %v", err))
  28. }
  29. // 确保头像目录存在
  30. avatarDir := filepath.Join(baseDir, "avatars")
  31. if err := os.MkdirAll(avatarDir, 0755); err != nil {
  32. panic(fmt.Sprintf("创建头像目录失败: %v", err))
  33. }
  34. return &LocalStorageService{
  35. baseDir: baseDir,
  36. publicPath: publicPath,
  37. }
  38. }
  39. // SaveAvatar 保存头像文件
  40. func (s *LocalStorageService) SaveAvatar(userID uint, file io.Reader, filename string) (string, error) {
  41. // 获取文件扩展名
  42. ext := filepath.Ext(filename)
  43. if ext == "" {
  44. ext = ".jpg" // 默认使用 jpg
  45. }
  46. // 生成唯一文件名:user_{userID}_{timestamp}{ext}
  47. timestamp := time.Now().Unix()
  48. newFilename := fmt.Sprintf("user_%d_%d%s", userID, timestamp, ext)
  49. // 保存到 avatars 目录
  50. avatarDir := filepath.Join(s.baseDir, "avatars")
  51. filePath := filepath.Join(avatarDir, newFilename)
  52. // 创建文件
  53. dst, err := os.Create(filePath)
  54. if err != nil {
  55. return "", fmt.Errorf("创建文件失败: %w", err)
  56. }
  57. defer dst.Close()
  58. // 复制文件内容
  59. if _, err := io.Copy(dst, file); err != nil {
  60. return "", fmt.Errorf("保存文件失败: %w", err)
  61. }
  62. // 返回相对路径(用于构建URL)
  63. relativePath := filepath.Join("avatars", newFilename)
  64. return s.GetFileURL(relativePath), nil
  65. }
  66. // DeleteFile 删除文件
  67. func (s *LocalStorageService) DeleteFile(fileURL string) error {
  68. // 从URL中提取文件路径
  69. // 假设URL格式为: /uploads/avatars/filename.jpg
  70. // 需要去掉 /uploads/ 前缀,得到相对路径
  71. relativePath := fileURL
  72. if len(s.publicPath) > 0 && len(fileURL) > len(s.publicPath) {
  73. if fileURL[:len(s.publicPath)] == s.publicPath {
  74. relativePath = fileURL[len(s.publicPath):]
  75. // 去掉开头的 /
  76. if len(relativePath) > 0 && relativePath[0] == '/' {
  77. relativePath = relativePath[1:]
  78. }
  79. }
  80. }
  81. filePath := filepath.Join(s.baseDir, relativePath)
  82. if err := os.Remove(filePath); err != nil {
  83. if os.IsNotExist(err) {
  84. return nil // 文件不存在,认为删除成功
  85. }
  86. return fmt.Errorf("删除文件失败: %w", err)
  87. }
  88. return nil
  89. }
  90. // GetFileURL 获取文件的完整URL
  91. func (s *LocalStorageService) GetFileURL(filePath string) string {
  92. // 确保路径使用正斜杠(用于URL)
  93. urlPath := filepath.ToSlash(filePath)
  94. // 如果 publicPath 为空,返回相对路径
  95. if s.publicPath == "" {
  96. return "/" + urlPath
  97. }
  98. // 确保 publicPath 以 / 结尾
  99. publicPath := s.publicPath
  100. if publicPath[len(publicPath)-1] != '/' {
  101. publicPath += "/"
  102. }
  103. // 确保 urlPath 不以 / 开头
  104. if len(urlPath) > 0 && urlPath[0] == '/' {
  105. urlPath = urlPath[1:]
  106. }
  107. return publicPath + urlPath
  108. }