package handlers import ( "encoding/json" "fmt" "net/http" "path/filepath" "strings" "time" "github.com/rede5/gohorsejobs/backend/internal/utils/uuid" ) // StorageServiceInterface defines the contract for storage operations type StorageServiceInterface interface { GenerateUploadURL(key string, contentType string, expiryMinutes int) (string, error) GenerateDownloadURL(key string, expiryMinutes int) (string, error) GetPublicURL(key string) string DeleteObject(key string) error } // StorageHandler handles file storage operations type StorageHandler struct { Storage StorageServiceInterface } // NewStorageHandler creates a new storage handler func NewStorageHandler(s StorageServiceInterface) *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 creates a pre-signed S3 URL for uploads // @Summary Generate upload URL // @Description Generate a pre-signed URL to upload a file to S3 // @Tags Storage // @Accept json // @Produce json // @Security BearerAuth // @Param request body UploadURLRequest true "Upload request" // @Success 200 {object} UploadURLResponse // @Failure 400 {string} string "Bad Request" // @Failure 500 {string} string "Internal Server Error" // @Router /api/v1/storage/upload-url [post] 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.V7() 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 creates a pre-signed S3 URL for downloads // @Summary Generate download URL // @Description Generate a pre-signed URL to download a private file from S3 // @Tags Storage // @Accept json // @Produce json // @Security BearerAuth // @Param request body DownloadURLRequest true "Download request" // @Success 200 {object} DownloadURLResponse // @Failure 400 {string} string "Bad Request" // @Failure 500 {string} string "Internal Server Error" // @Router /api/v1/storage/download-url [post] 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 removes an object from S3 // @Summary Delete file // @Description Delete a stored file by key // @Tags Storage // @Accept json // @Produce json // @Security BearerAuth // @Param key query string true "File key" // @Success 204 // @Failure 400 {string} string "Bad Request" // @Failure 500 {string} string "Internal Server Error" // @Router /api/v1/storage/files [delete] 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) }