mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
Repository Structure:
- Move files from cluttered root directory into organized structure
- Create archive/ for archived data and scraper results
- Create bugulma/ for the complete application (frontend + backend)
- Create data/ for sample datasets and reference materials
- Create docs/ for comprehensive documentation structure
- Create scripts/ for utility scripts and API tools
Backend Implementation:
- Implement 3 missing backend endpoints identified in gap analysis:
* GET /api/v1/organizations/{id}/matching/direct - Direct symbiosis matches
* GET /api/v1/users/me/organizations - User organizations
* POST /api/v1/proposals/{id}/status - Update proposal status
- Add complete proposal domain model, repository, and service layers
- Create database migration for proposals table
- Fix CLI server command registration issue
API Documentation:
- Add comprehensive proposals.md API documentation
- Update README.md with Users and Proposals API sections
- Document all request/response formats, error codes, and business rules
Code Quality:
- Follow existing Go backend architecture patterns
- Add proper error handling and validation
- Match frontend expected response schemas
- Maintain clean separation of concerns (handler -> service -> repository)
160 lines
4.2 KiB
Go
160 lines
4.2 KiB
Go
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"
|
|
}
|
|
}
|