package service import ( "crypto/md5" "encoding/base64" "fmt" "io" "mime/multipart" "os" "path/filepath" "strings" "time" "bugulma/backend/pkg/config" ) type ImageService struct { config *config.Config staticPath string } type UploadedImage struct { URL string `json:"url"` Path string `json:"path"` } func NewImageService(cfg *config.Config) *ImageService { return &ImageService{ config: cfg, staticPath: "static/images", } } // SaveImage saves an uploaded image file and returns the URL and local path func (s *ImageService) SaveImage(file multipart.File, header *multipart.FileHeader, subfolder string) (*UploadedImage, error) { // Create subfolder if it doesn't exist folderPath := filepath.Join(s.staticPath, subfolder) if err := os.MkdirAll(folderPath, 0755); err != nil { return nil, fmt.Errorf("failed to create directory: %w", err) } // Generate unique filename based on content hash and timestamp content, err := io.ReadAll(file) if err != nil { return nil, fmt.Errorf("failed to read file content: %w", err) } hash := fmt.Sprintf("%x", md5.Sum(content)) timestamp := time.Now().Format("20060102_150405") ext := s.getFileExtension(header.Filename) filename := fmt.Sprintf("%s_%s%s", hash[:8], timestamp, ext) filePath := filepath.Join(folderPath, filename) // Write file to disk if err := os.WriteFile(filePath, content, 0644); err != nil { return nil, fmt.Errorf("failed to save file: %w", err) } // Generate URL (assuming static files are served from /static/) url := fmt.Sprintf("/static/images/%s/%s", subfolder, filename) return &UploadedImage{ URL: url, Path: filePath, }, nil } // SaveDataURL saves a base64 data URL as an image file func (s *ImageService) SaveDataURL(dataURL, subfolder string) (*UploadedImage, error) { // Create subfolder if it doesn't exist folderPath := filepath.Join(s.staticPath, subfolder) if err := os.MkdirAll(folderPath, 0755); err != nil { return nil, fmt.Errorf("failed to create directory: %w", err) } // Parse data URL (data:image/png;base64,...) parts := strings.Split(dataURL, ",") if len(parts) != 2 { return nil, fmt.Errorf("invalid data URL format") } header := parts[0] if !strings.HasPrefix(header, "data:image/") { return nil, fmt.Errorf("not an image data URL") } // Extract MIME type and base64 data contentType := strings.TrimSuffix(strings.TrimPrefix(header, "data:"), ";base64") ext := s.getExtensionFromMIME(contentType) // Decode base64 content, err := base64.StdEncoding.DecodeString(parts[1]) if err != nil { return nil, fmt.Errorf("failed to decode base64: %w", err) } // Generate unique filename hash := fmt.Sprintf("%x", md5.Sum(content)) timestamp := time.Now().Format("20060102_150405") filename := fmt.Sprintf("%s_%s%s", hash[:8], timestamp, ext) filePath := filepath.Join(folderPath, filename) // Write file to disk if err := os.WriteFile(filePath, content, 0644); err != nil { return nil, fmt.Errorf("failed to save file: %w", err) } // Generate URL url := fmt.Sprintf("/static/images/%s/%s", subfolder, filename) return &UploadedImage{ URL: url, Path: filePath, }, nil } // DeleteImage removes an image file from disk func (s *ImageService) DeleteImage(imagePath string) error { // Remove the static/images/ prefix to get the actual file path if strings.HasPrefix(imagePath, "/static/images/") { imagePath = strings.TrimPrefix(imagePath, "/static/images/") imagePath = filepath.Join(s.staticPath, imagePath) } if err := os.Remove(imagePath); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to delete image: %w", err) } return nil } // getFileExtension extracts file extension from filename func (s *ImageService) getFileExtension(filename string) string { ext := filepath.Ext(filename) if ext == "" { return ".jpg" // default extension } return strings.ToLower(ext) } // getExtensionFromMIME converts MIME type to file extension func (s *ImageService) getExtensionFromMIME(mimeType string) string { switch mimeType { case "image/jpeg": return ".jpg" case "image/png": return ".png" case "image/gif": return ".gif" case "image/webp": return ".webp" case "image/svg+xml": return ".svg" default: return ".jpg" } }