file-system/internal/api/endpoints/file_endpoint.go
向宁 b5df6445e5 refactor: commit all pending file_system changes
- Restructure handlers into file_commands/file_queries/file_handlers
- Add gRPC auth client, JWT middleware, rate limiting, request ID
- Add common utilities: logger, sanitizer, s3_errors
- Add unit tests for config, mediator, auth, request_id, sanitize
- Add proto definitions and generated code
- Remove old web UI pages
- Add .dockerignore and .env.example
2026-05-17 22:20:02 +08:00

443 lines
14 KiB
Go

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})
}