367 lines
12 KiB
Go
367 lines
12 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})
|
|
}
|
|
|
|
// DeleteFile godoc
|
|
// @Summary 删除文件
|
|
// @Description 从指定的存储桶删除文件
|
|
// @Tags 文件操作
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body requests.DeleteFileRequest true "请求参数"
|
|
// @Success 200 {object} map[string]string "删除成功消息"
|
|
// @Failure 400 {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.NewFeaturesValidator.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})
|
|
}
|
|
|
|
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()})
|
|
}
|
|
}
|