Files
FileRelay/internal/api/public/pickup.go
2026-01-13 17:07:27 +08:00

167 lines
5.0 KiB
Go

package public
import (
"FileRelay/internal/api/middleware"
"FileRelay/internal/bootstrap"
"FileRelay/internal/model"
"FileRelay/internal/service"
"FileRelay/internal/storage"
"archive/zip"
"fmt"
"io"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
)
type PickupResponse struct {
Remark string `json:"remark"`
ExpireAt *time.Time `json:"expire_at"`
ExpireType string `json:"expire_type"`
DownloadCount int `json:"download_count"`
MaxDownloads int `json:"max_downloads"`
Type string `json:"type"`
Content string `json:"content,omitempty"`
Files []model.FileItem `json:"files,omitempty"`
}
// DownloadBatch 批量下载文件 (ZIP)
// @Summary 批量下载文件
// @Description 根据取件码将批次内的所有文件打包为 ZIP 格式一次性下载
// @Tags Public
// @Param pickup_code path string true "取件码"
// @Produce application/zip
// @Success 200 {file} file
// @Failure 404 {object} model.Response
// @Router /api/download/batch/{pickup_code} [get]
func (h *PickupHandler) DownloadBatch(c *gin.Context) {
code := c.Param("pickup_code")
batch, err := h.batchService.GetBatchByPickupCode(code)
if err != nil {
c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "batch not found or expired"))
return
}
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"batch_%s.zip\"", code))
c.Header("Content-Type", "application/zip")
zw := zip.NewWriter(c.Writer)
defer zw.Close()
for _, item := range batch.FileItems {
reader, err := storage.GlobalStorage.Open(c.Request.Context(), item.StoragePath)
if err != nil {
continue // Skip failed files
}
f, err := zw.Create(item.OriginalName)
if err != nil {
reader.Close()
continue
}
_, _ = io.Copy(f, reader)
reader.Close()
}
// 增加下载次数
h.batchService.IncrementDownloadCount(batch.ID)
}
type PickupHandler struct {
batchService *service.BatchService
}
func NewPickupHandler() *PickupHandler {
return &PickupHandler{
batchService: service.NewBatchService(),
}
}
// Pickup 获取批次信息
// @Summary 获取批次信息
// @Description 根据取件码获取文件批次详详情和文件列表
// @Tags Public
// @Produce json
// @Param pickup_code path string true "取件码"
// @Success 200 {object} model.Response{data=PickupResponse}
// @Failure 404 {object} model.Response
// @Router /api/pickup/{pickup_code} [get]
func (h *PickupHandler) Pickup(c *gin.Context) {
code := c.Param("pickup_code")
if code == "" {
c.JSON(http.StatusBadRequest, model.ErrorResponse(model.CodeBadRequest, "pickup code required"))
return
}
batch, err := h.batchService.GetBatchByPickupCode(code)
if err != nil {
middleware.RecordPickupFailure(c.ClientIP(), code)
c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "batch not found or expired"))
return
}
c.JSON(http.StatusOK, model.SuccessResponse(PickupResponse{
Remark: batch.Remark,
ExpireAt: batch.ExpireAt,
ExpireType: batch.ExpireType,
DownloadCount: batch.DownloadCount,
MaxDownloads: batch.MaxDownloads,
Type: batch.Type,
Content: batch.Content,
Files: batch.FileItems,
}))
}
// DownloadFile 下载单个文件
// @Summary 下载单个文件
// @Description 根据文件 ID 下载单个文件
// @Tags Public
// @Param file_id path int true "文件 ID"
// @Produce application/octet-stream
// @Success 200 {file} file
// @Failure 404 {object} model.Response
// @Failure 410 {object} model.Response
// @Router /api/download/file/{file_id} [get]
func (h *PickupHandler) DownloadFile(c *gin.Context) {
fileIDStr := c.Param("file_id")
fileID, _ := strconv.ParseUint(fileIDStr, 10, 32)
var item model.FileItem
if err := bootstrap.DB.First(&item, fileID).Error; err != nil {
c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "file not found"))
return
}
var batch model.FileBatch
if err := bootstrap.DB.First(&batch, item.BatchID).Error; err != nil {
c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "batch not found"))
return
}
if h.batchService.IsExpired(&batch) {
h.batchService.MarkAsExpired(&batch)
c.JSON(http.StatusGone, model.ErrorResponse(model.CodeGone, "batch expired"))
return
}
// 打开文件
reader, err := storage.GlobalStorage.Open(c.Request.Context(), item.StoragePath)
if err != nil {
c.JSON(http.StatusInternalServerError, model.ErrorResponse(model.CodeInternalError, "failed to open file"))
return
}
defer reader.Close()
// 增加下载次数
h.batchService.IncrementDownloadCount(batch.ID)
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", item.OriginalName))
c.Header("Content-Type", item.MimeType)
c.Header("Content-Length", strconv.FormatInt(item.Size, 10))
io.Copy(c.Writer, reader)
}