208 lines
6.7 KiB
Go
208 lines
6.7 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 格式一次性下载。可选提供带 pickup scope 的 API Token。
|
|
// @Tags Public
|
|
// @Security APITokenAuth
|
|
// @Param pickup_code path string true "取件码"
|
|
// @Produce application/zip
|
|
// @Success 200 {file} file
|
|
// @Failure 404 {object} model.Response
|
|
// @Router /api/batches/{pickup_code}/download [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()
|
|
}
|
|
|
|
// 增加下载次数
|
|
if err := h.batchService.IncrementDownloadCount(batch.ID); err != nil {
|
|
fmt.Printf("[DownloadBatch] Failed to increment download count for batch %s: %v\n", batch.ID, err)
|
|
}
|
|
}
|
|
|
|
type PickupHandler struct {
|
|
batchService *service.BatchService
|
|
}
|
|
|
|
func NewPickupHandler() *PickupHandler {
|
|
return &PickupHandler{
|
|
batchService: service.NewBatchService(),
|
|
}
|
|
}
|
|
|
|
// Pickup 获取批次信息
|
|
// @Summary 获取批次信息
|
|
// @Description 根据取件码获取文件批次详细信息和文件列表。可选提供带 pickup scope 的 API Token。
|
|
// @Tags Public
|
|
// @Security APITokenAuth
|
|
// @Produce json
|
|
// @Param pickup_code path string true "取件码"
|
|
// @Success 200 {object} model.Response{data=PickupResponse}
|
|
// @Failure 404 {object} model.Response
|
|
// @Router /api/batches/{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
|
|
}
|
|
|
|
if batch.Type == "text" {
|
|
if err := h.batchService.IncrementDownloadCount(batch.ID); err != nil {
|
|
fmt.Printf("[Pickup] Failed to increment download count for batch %s: %v\n", batch.ID, err)
|
|
} else {
|
|
batch.DownloadCount++
|
|
}
|
|
}
|
|
|
|
// 生成文件下载绝对路径直链
|
|
scheme := "http"
|
|
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
|
|
scheme = "https"
|
|
}
|
|
host := c.Request.Host
|
|
if forwardedHost := c.GetHeader("X-Forwarded-Host"); forwardedHost != "" {
|
|
host = forwardedHost
|
|
}
|
|
|
|
for i := range batch.FileItems {
|
|
batch.FileItems[i].DownloadURL = fmt.Sprintf("%s://%s/api/files/%s/%s", scheme, host, batch.FileItems[i].ID, batch.FileItems[i].OriginalName)
|
|
}
|
|
|
|
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 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。
|
|
// @Tags Public
|
|
// @Security APITokenAuth
|
|
// @Param file_id path string true "文件 ID (UUID)"
|
|
// @Param filename path string false "文件名"
|
|
// @Produce application/octet-stream
|
|
// @Success 200 {file} file
|
|
// @Failure 404 {object} model.Response
|
|
// @Failure 410 {object} model.Response
|
|
// @Router /api/files/{file_id}/{filename} [get]
|
|
// @Router /api/files/{file_id}/download [get]
|
|
func (h *PickupHandler) DownloadFile(c *gin.Context) {
|
|
fileID := c.Param("file_id")
|
|
|
|
var item model.FileItem
|
|
if err := bootstrap.DB.First(&item, "id = ?", 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, "id = ?", 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)
|
|
// 按照需求,如果不存在(已在上面处理)或达到上限,返回 404
|
|
if batch.ExpireType == "download" && batch.DownloadCount >= batch.MaxDownloads {
|
|
c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "file not found or download limit reached"))
|
|
} else {
|
|
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()
|
|
|
|
// 增加下载次数
|
|
if err := h.batchService.IncrementDownloadCount(batch.ID); err != nil {
|
|
// 记录错误但不中断下载过程
|
|
fmt.Printf("[Download] Failed to increment download count for batch %s: %v\n", batch.ID, err)
|
|
}
|
|
|
|
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))
|
|
|
|
// 如果是 HEAD 请求,只返回 Header
|
|
if c.Request.Method == http.MethodHead {
|
|
return
|
|
}
|
|
|
|
io.Copy(c.Writer, reader)
|
|
}
|