package handlers import ( "encoding/json" "fmt" "net/http" "path/filepath" "strings" "time" "github.com/google/uuid" "github.com/rede5/gohorsejobs/backend/internal/infrastructure/storage" ) // StorageHandler handles file storage operations type StorageHandler struct { Storage *storage.S3Storage } // NewStorageHandler creates a new storage handler func NewStorageHandler(s *storage.S3Storage) *StorageHandler { return &StorageHandler{Storage: s} } // UploadURLRequest represents a request for a pre-signed upload URL type UploadURLRequest struct { Filename string `json:"filename"` ContentType string `json:"contentType"` Folder string `json:"folder"` // Optional: logos, resumes, documents } // UploadURLResponse represents the response with a pre-signed upload URL type UploadURLResponse struct { UploadURL string `json:"uploadUrl"` Key string `json:"key"` PublicURL string `json:"publicUrl"` ExpiresIn int `json:"expiresIn"` // seconds } // DownloadURLRequest represents a request for a pre-signed download URL type DownloadURLRequest struct { Key string `json:"key"` } // DownloadURLResponse represents the response with a pre-signed download URL type DownloadURLResponse struct { DownloadURL string `json:"downloadUrl"` ExpiresIn int `json:"expiresIn"` // seconds } // GenerateUploadURL handles POST /api/v1/storage/upload-url func (h *StorageHandler) GenerateUploadURL(w http.ResponseWriter, r *http.Request) { var req UploadURLRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.Filename == "" { http.Error(w, "Filename is required", http.StatusBadRequest) return } if req.ContentType == "" { // Try to infer from extension ext := strings.ToLower(filepath.Ext(req.Filename)) switch ext { case ".jpg", ".jpeg": req.ContentType = "image/jpeg" case ".png": req.ContentType = "image/png" case ".gif": req.ContentType = "image/gif" case ".webp": req.ContentType = "image/webp" case ".pdf": req.ContentType = "application/pdf" case ".doc": req.ContentType = "application/msword" case ".docx": req.ContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" default: req.ContentType = "application/octet-stream" } } // Validate folder folder := "uploads" if req.Folder != "" { validFolders := map[string]bool{ "logos": true, "resumes": true, "documents": true, "avatars": true, } if validFolders[req.Folder] { folder = req.Folder } } // Generate unique key ext := filepath.Ext(req.Filename) uniqueID := uuid.New().String() timestamp := time.Now().Format("20060102") key := fmt.Sprintf("%s/%s/%s%s", folder, timestamp, uniqueID, ext) // Generate pre-signed URL (15 minutes expiry) expiryMinutes := 15 uploadURL, err := h.Storage.GenerateUploadURL(key, req.ContentType, expiryMinutes) if err != nil { http.Error(w, fmt.Sprintf("Failed to generate upload URL: %v", err), http.StatusInternalServerError) return } response := UploadURLResponse{ UploadURL: uploadURL, Key: key, PublicURL: h.Storage.GetPublicURL(key), ExpiresIn: expiryMinutes * 60, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // GenerateDownloadURL handles POST /api/v1/storage/download-url func (h *StorageHandler) GenerateDownloadURL(w http.ResponseWriter, r *http.Request) { var req DownloadURLRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.Key == "" { http.Error(w, "Key is required", http.StatusBadRequest) return } // Generate pre-signed URL (1 hour expiry) expiryMinutes := 60 downloadURL, err := h.Storage.GenerateDownloadURL(req.Key, expiryMinutes) if err != nil { http.Error(w, fmt.Sprintf("Failed to generate download URL: %v", err), http.StatusInternalServerError) return } response := DownloadURLResponse{ DownloadURL: downloadURL, ExpiresIn: expiryMinutes * 60, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // DeleteFile handles DELETE /api/v1/storage/files func (h *StorageHandler) DeleteFile(w http.ResponseWriter, r *http.Request) { key := r.URL.Query().Get("key") if key == "" { http.Error(w, "Key query parameter is required", http.StatusBadRequest) return } if err := h.Storage.DeleteObject(key); err != nil { http.Error(w, fmt.Sprintf("Failed to delete file: %v", err), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) }