implemented mongo gridFS for file upload
This commit is contained in:
@@ -0,0 +1,452 @@
|
||||
package filestore
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tm/pkg/logger"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// Handler handles HTTP requests for file operations
|
||||
type Handler struct {
|
||||
service FileStoreService
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewHandler creates a new file handler
|
||||
func NewHandler(service FileStoreService, logger logger.Logger) *Handler {
|
||||
return &Handler{
|
||||
service: service,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadResponse represents the response for a successful upload
|
||||
type UploadResponse struct {
|
||||
FileID string `json:"file_id"`
|
||||
Filename string `json:"filename"`
|
||||
ContentType string `json:"content_type"`
|
||||
Size int64 `json:"size"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// FileInfoResponse represents file information in API response
|
||||
type FileInfoResponse struct {
|
||||
FileID string `json:"file_id"`
|
||||
Filename string `json:"filename"`
|
||||
ContentType string `json:"content_type"`
|
||||
Size int64 `json:"size"`
|
||||
UploadedBy string `json:"uploaded_by,omitempty"`
|
||||
UploadedAt time.Time `json:"uploaded_at"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Extra map[string]string `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
// ListFilesResponse represents the response for list files operation
|
||||
type ListFilesResponse struct {
|
||||
Files []FileInfoResponse `json:"files"`
|
||||
Total int64 `json:"total"`
|
||||
Limit int64 `json:"limit"`
|
||||
Skip int64 `json:"skip"`
|
||||
}
|
||||
|
||||
// ErrorResponse represents an error response
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// Upload handles file upload
|
||||
// @Summary Upload a file
|
||||
// @Description Upload a file to GridFS storage
|
||||
// @Tags Files
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param file formData file true "File to upload"
|
||||
// @Param category formData string false "File category"
|
||||
// @Param tags formData []string false "File tags"
|
||||
// @Param description formData string false "File description"
|
||||
// @Success 200 {object} UploadResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 413 {object} ErrorResponse "File too large"
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /api/v1/files/upload [post]
|
||||
func (h *Handler) Upload(c echo.Context) error {
|
||||
// Get file from request
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
h.logger.Warn("No file provided in upload request", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "No file provided",
|
||||
Code: ErrCodeInvalidInput,
|
||||
Message: "Please provide a file to upload",
|
||||
})
|
||||
}
|
||||
|
||||
// Open uploaded file
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to open uploaded file", map[string]interface{}{
|
||||
"filename": file.Filename,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Error: "Failed to open file",
|
||||
Code: ErrCodeUploadFailed,
|
||||
Message: "Unable to read the uploaded file",
|
||||
})
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// Get form data
|
||||
category := c.FormValue("category")
|
||||
tags := c.Request().Form["tags"]
|
||||
description := c.FormValue("description")
|
||||
uploadedBy := h.getUploaderID(c)
|
||||
if uploadedBy == "" {
|
||||
return c.JSON(http.StatusUnauthorized, ErrorResponse{
|
||||
Error: "Unauthorized",
|
||||
Code: ErrCodeAccessDenied,
|
||||
Message: "Authenticated user not found in request context",
|
||||
})
|
||||
}
|
||||
|
||||
// Detect content type if not provided
|
||||
contentType := file.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
ext := filepath.Ext(file.Filename)
|
||||
if detected := mime.TypeByExtension(ext); detected != "" {
|
||||
contentType = detected
|
||||
} else {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
}
|
||||
contentType = strings.TrimSpace(strings.Split(contentType, ";")[0])
|
||||
|
||||
validator := NewFileValidator(100*1024*1024, SupportedMimeTypes)
|
||||
if err := validator.ValidateFile(file.Filename, contentType, file.Size); err != nil {
|
||||
if IsFileTooLarge(err) {
|
||||
return c.JSON(http.StatusRequestEntityTooLarge, ErrorResponse{
|
||||
Error: "File is too large",
|
||||
Code: ErrCodeFileTooLarge,
|
||||
Message: "Maximum file size is 100 MB",
|
||||
})
|
||||
}
|
||||
if IsUnsupportedType(err) {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "Unsupported file type",
|
||||
Code: ErrCodeUnsupported,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "Invalid file",
|
||||
Code: ErrCodeInvalidInput,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Create upload options
|
||||
opts := UploadOptions{
|
||||
Filename: file.Filename,
|
||||
ContentType: contentType,
|
||||
UploadedBy: uploadedBy,
|
||||
Category: category,
|
||||
Tags: tags,
|
||||
Description: description,
|
||||
MaxSize: 100 * 1024 * 1024, // 100 MB max
|
||||
}
|
||||
|
||||
// Upload file
|
||||
fileID, err := h.service.Upload("", file.Filename, src, opts)
|
||||
if err != nil {
|
||||
if IsFileTooLarge(err) {
|
||||
h.logger.Warn("File upload failed: file too large", map[string]interface{}{
|
||||
"filename": file.Filename,
|
||||
"size": file.Size,
|
||||
})
|
||||
return c.JSON(http.StatusRequestEntityTooLarge, ErrorResponse{
|
||||
Error: "File is too large",
|
||||
Code: ErrCodeFileTooLarge,
|
||||
Message: "Maximum file size is 100 MB",
|
||||
})
|
||||
}
|
||||
|
||||
h.logger.Error("File upload failed", map[string]interface{}{
|
||||
"filename": file.Filename,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Error: "Upload failed",
|
||||
Code: ErrCodeUploadFailed,
|
||||
Message: "Failed to upload file to storage",
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, UploadResponse{
|
||||
FileID: fileID,
|
||||
Filename: file.Filename,
|
||||
ContentType: contentType,
|
||||
Size: file.Size,
|
||||
Message: "File uploaded successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// Download handles file download
|
||||
// @Summary Download a file
|
||||
// @Description Download a file by its ID
|
||||
// @Tags Files
|
||||
// @Produce application/octet-stream
|
||||
// @Param file_id path string true "File ID"
|
||||
// @Success 200 {file} bytes
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /api/v1/files/{file_id}/download [get]
|
||||
func (h *Handler) Download(c echo.Context) error {
|
||||
fileID := c.Param("file_id")
|
||||
if fileID == "" {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "File ID is required",
|
||||
Code: ErrCodeInvalidInput,
|
||||
Message: "Please provide a valid file ID",
|
||||
})
|
||||
}
|
||||
|
||||
// Download file
|
||||
reader, fileInfo, err := h.service.Download(fileID)
|
||||
if err != nil {
|
||||
if IsNotFound(err) {
|
||||
h.logger.Warn("File not found", map[string]interface{}{
|
||||
"file_id": fileID,
|
||||
})
|
||||
return c.JSON(http.StatusNotFound, ErrorResponse{
|
||||
Error: "File not found",
|
||||
Code: ErrCodeNotFound,
|
||||
Message: "The requested file does not exist",
|
||||
})
|
||||
}
|
||||
|
||||
h.logger.Error("File download failed", map[string]interface{}{
|
||||
"file_id": fileID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Error: "Download failed",
|
||||
Code: ErrCodeDownloadFailed,
|
||||
Message: "Failed to download file",
|
||||
})
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// Set response headers
|
||||
c.Response().Header().Set("Content-Type", fileInfo.ContentType)
|
||||
c.Response().Header().Set("Content-Length", strconv.FormatInt(fileInfo.Size, 10))
|
||||
c.Response().Header().Set("Content-Disposition", `attachment; filename="`+fileInfo.Filename+`"`)
|
||||
c.Response().Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
|
||||
// Write file to response
|
||||
return c.Stream(http.StatusOK, fileInfo.ContentType, reader)
|
||||
}
|
||||
|
||||
func (h *Handler) getUploaderID(c echo.Context) string {
|
||||
if userID, ok := c.Get("user_id").(string); ok && userID != "" {
|
||||
return userID
|
||||
}
|
||||
if customerID, ok := c.Get("customer_id").(string); ok && customerID != "" {
|
||||
return customerID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetInfo retrieves file information
|
||||
// @Summary Get file information
|
||||
// @Description Get metadata for a file by its ID
|
||||
// @Tags Files
|
||||
// @Produce json
|
||||
// @Param file_id path string true "File ID"
|
||||
// @Success 200 {object} FileInfoResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /api/v1/files/{file_id}/info [get]
|
||||
func (h *Handler) GetInfo(c echo.Context) error {
|
||||
fileID := c.Param("file_id")
|
||||
if fileID == "" {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "File ID is required",
|
||||
Code: ErrCodeInvalidInput,
|
||||
Message: "Please provide a valid file ID",
|
||||
})
|
||||
}
|
||||
|
||||
// Get file info
|
||||
fileInfo, err := h.service.GetFileInfo(fileID)
|
||||
if err != nil {
|
||||
if IsNotFound(err) {
|
||||
return c.JSON(http.StatusNotFound, ErrorResponse{
|
||||
Error: "File not found",
|
||||
Code: ErrCodeNotFound,
|
||||
Message: "The requested file does not exist",
|
||||
})
|
||||
}
|
||||
|
||||
h.logger.Error("Failed to get file info", map[string]interface{}{
|
||||
"file_id": fileID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Error: "Failed to get file info",
|
||||
Code: ErrCodeDownloadFailed,
|
||||
Message: "Unable to retrieve file information",
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, FileInfoResponse{
|
||||
FileID: fileInfo.FileID,
|
||||
Filename: fileInfo.Filename,
|
||||
ContentType: fileInfo.ContentType,
|
||||
Size: fileInfo.Size,
|
||||
UploadedBy: fileInfo.UploadedBy,
|
||||
UploadedAt: fileInfo.UploadedAt,
|
||||
Category: fileInfo.Category,
|
||||
Tags: fileInfo.Tags,
|
||||
Description: fileInfo.Description,
|
||||
Extra: fileInfo.Extra,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete removes a file
|
||||
// @Summary Delete a file
|
||||
// @Description Delete a file by its ID
|
||||
// @Tags Files
|
||||
// @Param file_id path string true "File ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /api/v1/files/{file_id} [delete]
|
||||
func (h *Handler) Delete(c echo.Context) error {
|
||||
fileID := c.Param("file_id")
|
||||
if fileID == "" {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "File ID is required",
|
||||
Code: ErrCodeInvalidInput,
|
||||
Message: "Please provide a valid file ID",
|
||||
})
|
||||
}
|
||||
|
||||
// Delete file
|
||||
err := h.service.Delete(fileID)
|
||||
if err != nil {
|
||||
if IsNotFound(err) {
|
||||
return c.JSON(http.StatusNotFound, ErrorResponse{
|
||||
Error: "File not found",
|
||||
Code: ErrCodeNotFound,
|
||||
Message: "The requested file does not exist",
|
||||
})
|
||||
}
|
||||
|
||||
h.logger.Error("File deletion failed", map[string]interface{}{
|
||||
"file_id": fileID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Error: "Deletion failed",
|
||||
Code: ErrCodeDeleteFailed,
|
||||
Message: "Failed to delete file",
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]string{
|
||||
"message": "File deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ListFiles lists files by category
|
||||
// @Summary List files
|
||||
// @Description List files by category with optional filtering
|
||||
// @Tags Files
|
||||
// @Produce json
|
||||
// @Param category query string false "File category"
|
||||
// @Param tags query []string false "File tags"
|
||||
// @Param limit query int false "Number of files to return (default 50, max 1000)"
|
||||
// @Param skip query int false "Number of files to skip (default 0)"
|
||||
// @Success 200 {object} ListFilesResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /api/v1/files [get]
|
||||
func (h *Handler) ListFiles(c echo.Context) error {
|
||||
category := c.QueryParam("category")
|
||||
tags := c.QueryParams()["tags"]
|
||||
limit := int64(50)
|
||||
skip := int64(0)
|
||||
|
||||
// Parse pagination params
|
||||
if l := c.QueryParam("limit"); l != "" {
|
||||
if val, err := strconv.ParseInt(l, 10, 64); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "Invalid limit parameter",
|
||||
Code: ErrCodeInvalidInput,
|
||||
Message: "Limit must be a valid integer",
|
||||
})
|
||||
} else {
|
||||
limit = val
|
||||
}
|
||||
}
|
||||
if s := c.QueryParam("skip"); s != "" {
|
||||
if val, err := strconv.ParseInt(s, 10, 64); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "Invalid skip parameter",
|
||||
Code: ErrCodeInvalidInput,
|
||||
Message: "Skip must be a valid integer",
|
||||
})
|
||||
} else {
|
||||
skip = val
|
||||
}
|
||||
}
|
||||
|
||||
// List files
|
||||
files, total, err := h.service.ListFiles(category, tags, limit, skip)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to list files", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Error: "Failed to list files",
|
||||
Code: ErrCodeDownloadFailed,
|
||||
Message: "Unable to retrieve file list",
|
||||
})
|
||||
}
|
||||
|
||||
// Convert to response format
|
||||
fileResponses := make([]FileInfoResponse, len(files))
|
||||
for i, f := range files {
|
||||
fileResponses[i] = FileInfoResponse{
|
||||
FileID: f.FileID,
|
||||
Filename: f.Filename,
|
||||
ContentType: f.ContentType,
|
||||
Size: f.Size,
|
||||
UploadedBy: f.UploadedBy,
|
||||
UploadedAt: f.UploadedAt,
|
||||
Category: f.Category,
|
||||
Tags: f.Tags,
|
||||
Description: f.Description,
|
||||
Extra: f.Extra,
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, ListFilesResponse{
|
||||
Files: fileResponses,
|
||||
Total: total,
|
||||
Limit: limit,
|
||||
Skip: skip,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user