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++ } } 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)" // @Produce application/octet-stream // @Success 200 {file} file // @Failure 404 {object} model.Response // @Failure 410 {object} model.Response // @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) 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) }