package http import ( "encoding/json" "fmt" "io" stdhttp "net/http" "strconv" "strings" "time" "backupx/server/internal/apperror" "backupx/server/internal/service" "backupx/server/pkg/response" "github.com/gin-gonic/gin" ) // TaskExportHandler 提供任务配置 JSON 导入/导出。 type TaskExportHandler struct { service *service.TaskExportService auditService *service.AuditService } func NewTaskExportHandler(s *service.TaskExportService, audit *service.AuditService) *TaskExportHandler { return &TaskExportHandler{service: s, auditService: audit} } // Export GET /api/backup/tasks/export?ids=1,2,3 // 无 ids 参数时导出全部任务。返回 application/json + Content-Disposition。 func (h *TaskExportHandler) Export(c *gin.Context) { var taskIDs []uint if v := strings.TrimSpace(c.Query("ids")); v != "" { for _, part := range strings.Split(v, ",") { if id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 32); err == nil { taskIDs = append(taskIDs, uint(id)) } } } payload, err := h.service.Export(c.Request.Context(), taskIDs) if err != nil { response.Error(c, err) return } data, err := json.MarshalIndent(payload, "", " ") if err != nil { response.Error(c, apperror.Internal("TASK_EXPORT_MARSHAL_FAILED", "无法序列化导出内容", err)) return } filename := fmt.Sprintf("backupx-tasks-%s.json", time.Now().UTC().Format("20060102-150405")) c.Header("Content-Type", "application/json; charset=utf-8") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) _, _ = c.Writer.Write(data) recordAudit(c, h.auditService, "backup_task", "export", "backup_task", "", "", fmt.Sprintf("导出 %d 个任务的配置为 JSON", payload.TaskCount)) } // Import POST /api/backup/tasks/import // Body: ExportPayload JSON。返回每个任务的创建/跳过结果。 func (h *TaskExportHandler) Import(c *gin.Context) { body, err := io.ReadAll(c.Request.Body) if err != nil { response.Error(c, apperror.BadRequest("TASK_IMPORT_INVALID", "无法读取请求体", err)) return } if len(body) == 0 { response.Error(c, apperror.BadRequest("TASK_IMPORT_INVALID", "请求体为空", nil)) return } if len(body) > 1024*1024 { // 1MB 上限 c.Writer.WriteHeader(stdhttp.StatusRequestEntityTooLarge) response.Error(c, apperror.BadRequest("TASK_IMPORT_TOO_LARGE", "导入文件过大(上限 1MB)", nil)) return } var payload service.ExportPayload if err := json.Unmarshal(body, &payload); err != nil { response.Error(c, apperror.BadRequest("TASK_IMPORT_INVALID", "JSON 格式不合法", err)) return } if len(payload.Tasks) == 0 { response.Error(c, apperror.BadRequest("TASK_IMPORT_INVALID", "文件中未包含任何任务", nil)) return } results, err := h.service.Import(c.Request.Context(), payload) if err != nil { response.Error(c, err) return } succ := 0 skipped := 0 for _, r := range results { if r.Success && !r.Skipped { succ++ } else if r.Skipped { skipped++ } } recordAudit(c, h.auditService, "backup_task", "import", "backup_task", "", "", fmt.Sprintf("从 JSON 导入任务:创建 %d / 跳过 %d / 失败 %d", succ, skipped, len(results)-succ-skipped)) response.Success(c, results) }