package storage import ( "context" "fmt" "io" "mime/multipart" "os" "path/filepath" "photography-backend/internal/config" "go.uber.org/zap" ) // UploadedFile 上传后的文件信息 type UploadedFile struct { Filename string `json:"filename"` OriginalURL string `json:"original_url"` ThumbnailURL string `json:"thumbnail_url,omitempty"` Size int64 `json:"size"` MimeType string `json:"mime_type"` } // StorageService 存储服务接口 type StorageService interface { UploadPhoto(ctx context.Context, file multipart.File, filename string) (*UploadedFile, error) DeletePhoto(filename string) error GetPhotoURL(filename string) string GenerateThumbnail(ctx context.Context, filename string) error } // LocalStorageService 本地存储服务实现 type LocalStorageService struct { config *config.Config logger *zap.Logger uploadDir string baseURL string } // NewLocalStorageService 创建本地存储服务 func NewLocalStorageService(config *config.Config, logger *zap.Logger) *LocalStorageService { uploadDir := config.Storage.Local.BasePath if uploadDir == "" { uploadDir = "./uploads" } baseURL := config.Storage.Local.BaseURL if baseURL == "" { baseURL = fmt.Sprintf("http://localhost:%d/uploads", config.App.Port) } // 确保上传目录存在 if err := os.MkdirAll(uploadDir, 0755); err != nil { logger.Error("Failed to create upload directory", zap.Error(err)) } // 创建子目录 dirs := []string{"photos", "thumbnails", "temp"} for _, dir := range dirs { dirPath := filepath.Join(uploadDir, dir) if err := os.MkdirAll(dirPath, 0755); err != nil { logger.Error("Failed to create subdirectory", zap.String("dir", dir), zap.Error(err)) } } return &LocalStorageService{ config: config, logger: logger, uploadDir: uploadDir, baseURL: baseURL, } } // UploadPhoto 上传照片 func (s *LocalStorageService) UploadPhoto(ctx context.Context, file multipart.File, filename string) (*UploadedFile, error) { // 保存原图 photoPath := filepath.Join(s.uploadDir, "photos", filename) out, err := os.Create(photoPath) if err != nil { s.logger.Error("Failed to create file", zap.String("path", photoPath), zap.Error(err)) return nil, err } defer out.Close() // 重置文件指针 file.Seek(0, 0) // 复制文件内容 size, err := io.Copy(out, file) if err != nil { s.logger.Error("Failed to copy file", zap.Error(err)) return nil, err } // 获取文件信息 _, err = out.Stat() if err != nil { s.logger.Error("Failed to get file info", zap.Error(err)) return nil, err } uploadedFile := &UploadedFile{ Filename: filename, OriginalURL: s.GetPhotoURL(filename), Size: size, MimeType: s.getMimeType(filename), } s.logger.Info("Photo uploaded successfully", zap.String("filename", filename), zap.Int64("size", size)) return uploadedFile, nil } // DeletePhoto 删除照片 func (s *LocalStorageService) DeletePhoto(filename string) error { // 删除原图 photoPath := filepath.Join(s.uploadDir, "photos", filename) if err := os.Remove(photoPath); err != nil && !os.IsNotExist(err) { s.logger.Error("Failed to delete photo", zap.String("path", photoPath), zap.Error(err)) return err } // 删除缩略图 thumbnailPath := filepath.Join(s.uploadDir, "thumbnails", filename) if err := os.Remove(thumbnailPath); err != nil && !os.IsNotExist(err) { s.logger.Warn("Failed to delete thumbnail", zap.String("path", thumbnailPath), zap.Error(err)) } s.logger.Info("Photo deleted successfully", zap.String("filename", filename)) return nil } // GetPhotoURL 获取照片 URL func (s *LocalStorageService) GetPhotoURL(filename string) string { return fmt.Sprintf("%s/photos/%s", s.baseURL, filename) } // GetThumbnailURL 获取缩略图 URL func (s *LocalStorageService) GetThumbnailURL(filename string) string { return fmt.Sprintf("%s/thumbnails/%s", s.baseURL, filename) } // GenerateThumbnail 生成缩略图 func (s *LocalStorageService) GenerateThumbnail(ctx context.Context, filename string) error { // TODO: 实现缩略图生成逻辑 // 这里需要使用图像处理库,如 imaging 或 bild s.logger.Info("Generating thumbnail", zap.String("filename", filename)) // 示例实现 - 实际项目中应该使用图像处理库 photoPath := filepath.Join(s.uploadDir, "photos", filename) thumbnailPath := filepath.Join(s.uploadDir, "thumbnails", filename) // 检查原图是否存在 if _, err := os.Stat(photoPath); os.IsNotExist(err) { return fmt.Errorf("original photo not found: %s", filename) } // 这里应该实现实际的缩略图生成逻辑 // 暂时复制原图作为缩略图 sourceFile, err := os.Open(photoPath) if err != nil { return err } defer sourceFile.Close() destFile, err := os.Create(thumbnailPath) if err != nil { return err } defer destFile.Close() _, err = io.Copy(destFile, sourceFile) if err != nil { return err } s.logger.Info("Thumbnail generated successfully", zap.String("filename", filename)) return nil } // getMimeType 根据文件扩展名获取 MIME 类型 func (s *LocalStorageService) getMimeType(filename string) string { ext := filepath.Ext(filename) switch ext { case ".jpg", ".jpeg": return "image/jpeg" case ".png": return "image/png" case ".gif": return "image/gif" case ".webp": return "image/webp" case ".bmp": return "image/bmp" default: return "application/octet-stream" } } // NewStorageService 根据配置创建存储服务 func NewStorageService(config *config.Config, logger *zap.Logger) StorageService { switch config.Storage.Type { case "s3": // TODO: 实现 S3 存储服务 logger.Warn("S3 storage not implemented yet, using local storage") return NewLocalStorageService(config, logger) case "minio": // TODO: 实现 MinIO 存储服务 logger.Warn("MinIO storage not implemented yet, using local storage") return NewLocalStorageService(config, logger) default: return NewLocalStorageService(config, logger) } }