file_system/internal/api/endpoints/file_endpoint.go
2025-12-18 09:34:49 +08:00

331 lines
10 KiB
Go

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