Implement multi-file upload functionality for companies and enhance file upload routes
This commit is contained in:
+142
-91
@@ -1,11 +1,9 @@
|
||||
package filestore
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"errors"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tm/pkg/logger"
|
||||
@@ -36,6 +34,13 @@ type UploadResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// BatchUploadResponse represents the response for a multi-file upload
|
||||
type BatchUploadResponse struct {
|
||||
Uploaded []UploadResponse `json:"uploaded"`
|
||||
Failed []FailedUploadResult `json:"failed,omitempty"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// FileInfoResponse represents file information in API response
|
||||
type FileInfoResponse struct {
|
||||
FileID string `json:"file_id"`
|
||||
@@ -81,7 +86,6 @@ type ErrorResponse struct {
|
||||
// @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{}{
|
||||
@@ -94,25 +98,6 @@ func (h *Handler) Upload(c echo.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
// 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{
|
||||
@@ -122,34 +107,135 @@ func (h *Handler) Upload(c echo.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
// 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"
|
||||
opts := h.buildUploadOptions(c, uploadedBy)
|
||||
result, err := UploadFromMultipartFile(h.service, file, opts)
|
||||
if err != nil {
|
||||
return h.writeUploadError(c, file.Filename, err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, UploadResponse{
|
||||
FileID: result.FileID,
|
||||
Filename: result.Filename,
|
||||
ContentType: result.ContentType,
|
||||
Size: result.Size,
|
||||
Message: "File uploaded successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// UploadBatch handles multi-file upload in a single request
|
||||
// @Summary Upload multiple files
|
||||
// @Description Upload up to 20 files to GridFS storage in one request. Use form field "files" (repeat for each file) or a single "file".
|
||||
// @Tags Files
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param files formData file true "Files 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} BatchUploadResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 401 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /api/v1/files/upload/batch [post]
|
||||
func (h *Handler) UploadBatch(c echo.Context) error {
|
||||
uploadedBy := h.getUploaderID(c)
|
||||
if uploadedBy == "" {
|
||||
return c.JSON(http.StatusUnauthorized, ErrorResponse{
|
||||
Error: "Unauthorized",
|
||||
Code: ErrCodeAccessDenied,
|
||||
Message: "Authenticated user not found in request context",
|
||||
})
|
||||
}
|
||||
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "Invalid multipart form",
|
||||
Code: ErrCodeInvalidInput,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
files := CollectMultipartFiles(form)
|
||||
if len(files) == 0 {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "No files provided",
|
||||
Code: ErrCodeInvalidInput,
|
||||
Message: "Use form field \"files\" with one or more files",
|
||||
})
|
||||
}
|
||||
if len(files) > MaxBatchUploadFiles {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "Too many files",
|
||||
Code: ErrCodeInvalidInput,
|
||||
Message: "Maximum 20 files per request",
|
||||
})
|
||||
}
|
||||
|
||||
opts := h.buildUploadOptions(c, uploadedBy)
|
||||
results, failed := UploadManyFromMultipart(h.service, files, opts)
|
||||
if len(results) == 0 {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "All uploads failed",
|
||||
Code: ErrCodeUploadFailed,
|
||||
Message: "No files were uploaded successfully",
|
||||
})
|
||||
}
|
||||
|
||||
uploaded := make([]UploadResponse, len(results))
|
||||
for i, r := range results {
|
||||
uploaded[i] = UploadResponse{
|
||||
FileID: r.FileID,
|
||||
Filename: r.Filename,
|
||||
ContentType: r.ContentType,
|
||||
Size: r.Size,
|
||||
Message: "File uploaded successfully",
|
||||
}
|
||||
}
|
||||
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(),
|
||||
})
|
||||
}
|
||||
message := "Files uploaded successfully"
|
||||
if len(failed) > 0 {
|
||||
message = "Some files uploaded successfully"
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, BatchUploadResponse{
|
||||
Uploaded: uploaded,
|
||||
Failed: failed,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) buildUploadOptions(c echo.Context, uploadedBy string) UploadOptions {
|
||||
return UploadOptions{
|
||||
UploadedBy: uploadedBy,
|
||||
Category: c.FormValue("category"),
|
||||
Tags: c.Request().Form["tags"],
|
||||
Description: c.FormValue("description"),
|
||||
MaxSize: MaxUploadSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) writeUploadError(c echo.Context, filename string, err error) error {
|
||||
if IsFileTooLarge(err) {
|
||||
h.logger.Warn("File upload failed: file too large", map[string]interface{}{
|
||||
"filename": filename,
|
||||
})
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
var fsErr *FileStoreError
|
||||
if errors.As(err, &fsErr) && fsErr.Code == ErrCodeInvalidInput {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "Invalid file",
|
||||
Code: ErrCodeInvalidInput,
|
||||
@@ -157,49 +243,14 @@ func (h *Handler) Upload(c echo.Context) 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",
|
||||
h.logger.Error("File upload failed", map[string]interface{}{
|
||||
"filename": filename,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Error: "Upload failed",
|
||||
Code: ErrCodeUploadFailed,
|
||||
Message: "Failed to upload file to storage",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user