package endpoints import ( "file-system/internal/api/handlers" "file-system/internal/api/requests" "file-system/internal/api/validators" "file-system/internal/common" "file-system/internal/domain/repository" "file-system/internal/infrastructure/mediator" "io" "net/http" "time" "github.com/gin-gonic/gin" ) type FileEndpoint struct { Mediator *mediator.Mediator UploadValidator *validators.UploadFileValidator DownloadValidator *validators.DownloadFileValidator NewFeaturesValidator *validators.NewFeaturesValidator } func NewFileEndpoint(m *mediator.Mediator, uv *validators.UploadFileValidator, dv *validators.DownloadFileValidator, nfv *validators.NewFeaturesValidator) *FileEndpoint { return &FileEndpoint{ Mediator: m, UploadValidator: uv, DownloadValidator: dv, NewFeaturesValidator: nfv, } } // UploadFile godoc // @Summary 上传文件 (简单上传) // @Description 上传小文件到指定的存储桶 // @Tags 文件操作 // @Accept multipart/form-data // @Produce json // @Param bucket_name formData string true "存储桶名称" // @Param file formData file true "要上传的文件" // @Success 200 {object} map[string]string "上传成功消息" // @Failure 400 {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.UploadValidator.Validate(&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() // 构建 Command cmd := handlers.UploadFileCommand{ BucketName: req.BucketName, FileName: req.File.Filename, Data: file, } // 调用 Mediator 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 下载文件 // @Description 从指定的存储桶下载文件 // @Tags 文件操作 // @Accept json // @Produce octet-stream // @Param bucket_name query string true "存储桶名称" // @Param object_key query string true "对象键(文件名)" // @Success 200 {file} file "文件流" // @Failure 400 {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.DownloadValidator.Validate(&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", "attachment; filename="+req.ObjectKey) c.Header("Content-Type", "application/octet-stream") io.Copy(c.Writer, result) } // ListFiles godoc // @Summary 文件列表 (分页) // @Description 分页查询存储桶中的文件 // @Tags 文件操作 // @Accept json // @Produce json // @Param bucket_name query string true "存储桶名称" // @Param prefix query string false "文件名前缀筛选" // @Param max_keys query int false "每页数量" // @Param token query string false "分页Token" // @Success 200 {object} repository.ListFilesResult // @Failure 400 {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.NewFeaturesValidator.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 获取预览链接 // @Description 生成文件的临时预览链接 (24小时有效) // @Tags 文件操作 // @Accept json // @Produce json // @Param bucket_name query string true "存储桶名称" // @Param object_key query string true "对象键" // @Success 200 {object} map[string]string // @Failure 400 {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.NewFeaturesValidator.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}) } // InitMultipart godoc // @Summary 初始化分片上传 // @Description 开始一个新的大文件分片上传任务 // @Tags 大文件上传 // @Accept json // @Produce json // @Param request body requests.InitMultipartRequest true "请求参数" // @Success 200 {object} map[string]string "返回 upload_id" // @Failure 400 {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.NewFeaturesValidator.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 上传分片 // @Description 上传单个文件分片 // @Tags 大文件上传 // @Accept multipart/form-data // @Produce json // @Param bucket_name formData string true "存储桶名称" // @Param object_key formData string true "对象键" // @Param upload_id formData string true "上传ID" // @Param part_number formData int true "分片序号 (从1开始)" // @Param file formData file true "分片文件数据" // @Success 200 {object} map[string]string "返回 ETag" // @Failure 400 {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.NewFeaturesValidator.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 完成分片上传 // @Description 合并所有分片完成上传 // @Tags 大文件上传 // @Accept json // @Produce json // @Param request body requests.CompleteMultipartRequest true "请求参数" // @Success 200 {object} map[string]string "返回文件位置" // @Failure 400 {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.NewFeaturesValidator.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}) } func handleError(c *gin.Context, err error) { if be, ok := err.(*common.BusinessException); ok { c.JSON(be.Code, gin.H{"error": be.Message}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } }