package endpoints import ( "rag/file-system/internal/api/handlers" "rag/file-system/internal/api/requests" "rag/file-system/internal/api/validators" "rag/file-system/internal/common" "rag/file-system/internal/domain/repository" "rag/file-system/internal/infrastructure/mediator" "fmt" "io" "net/http" "time" "github.com/gin-gonic/gin" ) type FileEndpoint struct { Mediator *mediator.Mediator FileValidator *validators.FileValidator } func NewFileEndpoint(m *mediator.Mediator, fv *validators.FileValidator) *FileEndpoint { return &FileEndpoint{ Mediator: m, FileValidator: fv, } } // UploadFile godoc // @Summary Upload file // @Description Upload a small file to the specified bucket // @Tags Files // @Accept multipart/form-data // @Produce json // @Security ApiKeyAuth // @Param bucket_name formData string true "Bucket name" // @Param file formData file true "File to upload" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /files/upload [post] func (e *FileEndpoint) UploadFile(c *gin.Context) { var req requests.UploadFileRequest if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := e.FileValidator.ValidateUpload(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } file, err := req.File.Open() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open file"}) return } defer file.Close() cmd := handlers.UploadFileCommand{ BucketName: req.BucketName, FileName: req.File.Filename, Data: file, } result, err := mediator.Send[handlers.UploadFileCommand, string](e.Mediator, c.Request.Context(), cmd) if err != nil { handleError(c, err) return } c.JSON(http.StatusOK, gin.H{"message": result}) } // DownloadFile godoc // @Summary Download file // @Description Download a file from the specified bucket // @Tags Files // @Accept json // @Produce octet-stream // @Security ApiKeyAuth // @Param bucket_name query string true "Bucket name" // @Param object_key query string true "Object key" // @Success 200 {file} file // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /files/download [get] func (e *FileEndpoint) DownloadFile(c *gin.Context) { var req requests.DownloadFileRequest if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := e.FileValidator.ValidateDownload(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } query := handlers.DownloadFileQuery{ BucketName: req.BucketName, ObjectKey: req.ObjectKey, } result, err := mediator.Send[handlers.DownloadFileQuery, io.ReadCloser](e.Mediator, c.Request.Context(), query) if err != nil { handleError(c, err) return } defer result.Close() c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, common.SanitizeFilename(req.ObjectKey))) c.Header("Content-Type", "application/octet-stream") io.Copy(c.Writer, result) } // ListFiles godoc // @Summary List files (paginated) // @Description List files in a bucket with pagination and prefix filtering // @Tags Files // @Accept json // @Produce json // @Security ApiKeyAuth // @Param bucket_name query string true "Bucket name" // @Param prefix query string false "File name prefix filter" // @Param max_keys query int false "Items per page (default 20)" // @Param token query string false "Pagination token" // @Success 200 {object} repository.ListFilesResult // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /files/list [get] func (e *FileEndpoint) ListFiles(c *gin.Context) { var req requests.ListFilesRequest if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := e.FileValidator.ValidateListFiles(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var token *string if req.Token != "" { token = &req.Token } query := handlers.ListFilesQuery{ BucketName: req.BucketName, Prefix: req.Prefix, MaxKeys: req.MaxKeys, Token: token, } result, err := mediator.Send[handlers.ListFilesQuery, *repository.ListFilesResult](e.Mediator, c.Request.Context(), query) if err != nil { handleError(c, err) return } c.JSON(http.StatusOK, result) } // GetPreviewURL godoc // @Summary Get preview URL // @Description Generate a temporary presigned URL for file preview (24h expiry) // @Tags Files // @Accept json // @Produce json // @Security ApiKeyAuth // @Param bucket_name query string true "Bucket name" // @Param object_key query string true "Object key" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /files/preview [get] func (e *FileEndpoint) GetPreviewURL(c *gin.Context) { var req requests.GetFilePreviewRequest if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := e.FileValidator.ValidatePreview(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } query := handlers.GetFilePreviewQuery{ BucketName: req.BucketName, ObjectKey: req.ObjectKey, Expiry: 24 * time.Hour, } result, err := mediator.Send[handlers.GetFilePreviewQuery, string](e.Mediator, c.Request.Context(), query) if err != nil { handleError(c, err) return } c.JSON(http.StatusOK, gin.H{"url": result}) } // GetFileContent godoc // @Summary Get file text content // @Description Retrieve text content of a file for preview (e.g., Markdown) // @Tags Files // @Accept json // @Produce json // @Security ApiKeyAuth // @Param bucket_name query string true "Bucket name" // @Param object_key query string true "Object key" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /files/content [get] func (e *FileEndpoint) GetFileContent(c *gin.Context) { var req requests.GetFileContentRequest if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := e.FileValidator.ValidateGetContent(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } query := handlers.GetFileContentQuery{ BucketName: req.BucketName, ObjectKey: req.ObjectKey, } result, err := mediator.Send[handlers.GetFileContentQuery, string](e.Mediator, c.Request.Context(), query) if err != nil { handleError(c, err) return } c.JSON(http.StatusOK, gin.H{"content": result}) } // InitMultipart godoc // @Summary Initialize multipart upload // @Description Start a new multipart upload session and return upload_id // @Tags Multipart Upload // @Accept json // @Produce json // @Security ApiKeyAuth // @Param request body requests.InitMultipartRequest true "Request parameters" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /files/multipart/init [post] func (e *FileEndpoint) InitMultipart(c *gin.Context) { var req requests.InitMultipartRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := e.FileValidator.ValidateInitMultipart(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } cmd := handlers.InitMultipartCommand{BucketName: req.BucketName, ObjectKey: req.ObjectKey} result, err := mediator.Send[handlers.InitMultipartCommand, string](e.Mediator, c.Request.Context(), cmd) if err != nil { handleError(c, err) return } c.JSON(http.StatusOK, gin.H{"upload_id": result}) } // UploadPart godoc // @Summary Upload a part // @Description Upload a single part of a multipart upload (5MB recommended per part) // @Tags Multipart Upload // @Accept multipart/form-data // @Produce json // @Security ApiKeyAuth // @Param bucket_name formData string true "Bucket name" // @Param object_key formData string true "Object key" // @Param upload_id formData string true "Upload ID" // @Param part_number formData int true "Part number (starting from 1)" // @Param file formData file true "Part data" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /files/multipart/part [put] func (e *FileEndpoint) UploadPart(c *gin.Context) { var req requests.UploadPartRequest if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := e.FileValidator.ValidateUploadPart(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } file, err := req.File.Open() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open part file"}) return } defer file.Close() cmd := handlers.UploadPartCommand{ BucketName: req.BucketName, ObjectKey: req.ObjectKey, UploadId: req.UploadId, PartNumber: req.PartNumber, Data: file, } result, err := mediator.Send[handlers.UploadPartCommand, string](e.Mediator, c.Request.Context(), cmd) if err != nil { handleError(c, err) return } c.JSON(http.StatusOK, gin.H{"etag": result}) } // CompleteMultipart godoc // @Summary Complete multipart upload // @Description Assemble all parts to complete the upload // @Tags Multipart Upload // @Accept json // @Produce json // @Security ApiKeyAuth // @Param request body requests.CompleteMultipartRequest true "Request parameters" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /files/multipart/complete [post] func (e *FileEndpoint) CompleteMultipart(c *gin.Context) { var req requests.CompleteMultipartRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := e.FileValidator.ValidateCompleteMultipart(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } cmd := handlers.CompleteMultipartCommand{ BucketName: req.BucketName, ObjectKey: req.ObjectKey, UploadId: req.UploadId, Parts: req.Parts, } result, err := mediator.Send[handlers.CompleteMultipartCommand, string](e.Mediator, c.Request.Context(), cmd) if err != nil { handleError(c, err) return } c.JSON(http.StatusOK, gin.H{"location": result}) } // AbortMultipart godoc // @Summary Abort multipart upload // @Description Cancel an in-progress multipart upload // @Tags Multipart Upload // @Accept json // @Produce json // @Security ApiKeyAuth // @Param request body requests.AbortMultipartRequest true "Request parameters" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /files/multipart/abort [post] func (e *FileEndpoint) AbortMultipart(c *gin.Context) { var req requests.AbortMultipartRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := e.FileValidator.ValidateAbortMultipart(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } cmd := handlers.AbortMultipartCommand{ BucketName: req.BucketName, ObjectKey: req.ObjectKey, UploadId: req.UploadId, } result, err := mediator.Send[handlers.AbortMultipartCommand, string](e.Mediator, c.Request.Context(), cmd) if err != nil { handleError(c, err) return } c.JSON(http.StatusOK, gin.H{"message": result}) } // DeleteFile godoc // @Summary Delete file // @Description Delete a file from the specified bucket (irreversible) // @Tags Files // @Accept json // @Produce json // @Security ApiKeyAuth // @Param request body requests.DeleteFileRequest true "Request parameters" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /files/delete [delete] func (e *FileEndpoint) DeleteFile(c *gin.Context) { var req requests.DeleteFileRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := e.FileValidator.ValidateDeleteFile(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } cmd := handlers.DeleteFileCommand{ BucketName: req.BucketName, ObjectKey: req.ObjectKey, } result, err := mediator.Send[handlers.DeleteFileCommand, string](e.Mediator, c.Request.Context(), cmd) if err != nil { handleError(c, err) return } c.JSON(http.StatusOK, gin.H{"message": result}) }