package api import ( "context" "encoding/json" "fmt" "io" "log/slog" "net/http" "os" "gitea.mrixs.me/minecraft-platform/backend/internal/core" "gitea.mrixs.me/minecraft-platform/backend/internal/core/importer" "gitea.mrixs.me/minecraft-platform/backend/internal/database" "gitea.mrixs.me/minecraft-platform/backend/internal/models" "gitea.mrixs.me/minecraft-platform/backend/internal/ws" ) type ModpackHandler struct { ModpackRepo *database.ModpackRepository JobRepo *database.JobRepository JanitorService *core.FileJanitorService Hub *ws.Hub } // ImportJobParams содержит параметры для фоновой задачи импорта type ImportJobParams struct { ImporterType string ImportMethod string SourceURL string TempZipPath string Name string DisplayName string MCVersion string } // ImportModpack обрабатывает запрос на импорт и запускает асинхронную задачу func (h *ModpackHandler) ImportModpack(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(512 << 20); err != nil { http.Error(w, "File too large", http.StatusBadRequest) return } params := ImportJobParams{ ImporterType: r.FormValue("importerType"), ImportMethod: r.FormValue("importMethod"), SourceURL: r.FormValue("sourceUrl"), Name: r.FormValue("name"), DisplayName: r.FormValue("displayName"), MCVersion: r.FormValue("mcVersion"), } // Валидация if params.Name == "" || params.DisplayName == "" || params.MCVersion == "" { http.Error(w, "Missing required fields", http.StatusBadRequest) return } // Обработка загрузки файла if params.ImportMethod == "file" { file, _, err := r.FormFile("file") if err != nil { http.Error(w, "Invalid file upload", http.StatusBadRequest) return } defer file.Close() tempFile, err := os.CreateTemp("", "modpack-*.zip") if err != nil { http.Error(w, "Could not create temp file", http.StatusInternalServerError) return } // Не удаляем файл здесь, так как он нужен worker-у. Удалим в worker-е. // defer os.Remove(tempFile.Name()) if _, err := io.Copy(tempFile, file); err != nil { tempFile.Close() os.Remove(tempFile.Name()) http.Error(w, "Could not save temp file", http.StatusInternalServerError) return } tempFile.Close() params.TempZipPath = tempFile.Name() } // Создаем задачу в БД jobID, err := h.JobRepo.CreateJob(r.Context()) if err != nil { if params.ImportMethod == "file" { os.Remove(params.TempZipPath) } http.Error(w, fmt.Sprintf("Failed to create job: %v", err), http.StatusInternalServerError) return } // Запускаем worker go h.processImportJob(jobID, params) // Отправляем ответ w.WriteHeader(http.StatusAccepted) json.NewEncoder(w).Encode(map[string]interface{}{ "job_id": jobID, "message": "Import job started", }) } // processImportJob выполняет импорт в фоне func (h *ModpackHandler) processImportJob(jobID int, params ImportJobParams) { ctx := context.Background() h.updateJobStatus(ctx, jobID, models.JobStatusDownloading, 10, "") // Очистка временного файла по завершении if params.TempZipPath != "" { defer os.Remove(params.TempZipPath) } storagePath := os.Getenv("MODPACKS_STORAGE_PATH") var imp importer.ModpackImporter // Настройка импортера switch params.ImporterType { case "simple": imp = &importer.SimpleZipImporter{StoragePath: storagePath} case "curseforge": apiKey := os.Getenv("CURSEFORGE_API_KEY") if apiKey == "" { h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, "CurseForge API key missing") return } imp = importer.NewCurseForgeImporter(storagePath, apiKey) case "modrinth": imp = importer.NewModrinthImporter(storagePath) default: h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, "Invalid importer type") return } // Загрузка по URL если нужно if params.ImportMethod == "url" { h.updateJobStatus(ctx, jobID, models.JobStatusDownloading, 20, "Downloading from URL...") // Логика скачивания зависит от типа импортера, пока поддерживаем CurseForge // TODO: Сделать интерфейс DownloadableImporter if cfImporter, ok := imp.(*importer.CurseForgeImporter); ok { zipPath, err := cfImporter.DownloadModpackFromURL(params.SourceURL) if err != nil { h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, fmt.Sprintf("Download failed: %v", err)) return } params.TempZipPath = zipPath defer os.Remove(zipPath) // Удаляем скачанный файл после обработки } else { // Для других типов пока не поддерживаем URL download внутри импортера h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, "URL import not supported for this type") return } } h.updateJobStatus(ctx, jobID, models.JobStatusProcessing, 40, "Processing modpack files...") // Импорт файлов files, err := imp.Import(params.TempZipPath) if err != nil { h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, fmt.Sprintf("Import failed: %v", err)) return } h.updateJobStatus(ctx, jobID, models.JobStatusProcessing, 80, "Saving to database...") // Сохранение в БД modpack := &models.Modpack{ Name: params.Name, DisplayName: params.DisplayName, MinecraftVersion: params.MCVersion, } err = h.ModpackRepo.CreateModpackTx(ctx, modpack, files) if err != nil { h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, fmt.Sprintf("Database save failed: %v", err)) return } h.updateJobStatus(ctx, jobID, models.JobStatusCompleted, 100, "Success") // Запуск Janitor-а go h.JanitorService.CleanOrphanedFiles(context.Background()) } // updateJobStatus обновляет статус в БД и отправляет уведомление через WebSocket func (h *ModpackHandler) updateJobStatus(ctx context.Context, jobID int, status models.ImportJobStatus, progress int, errMsg string) { // Обновляем БД if err := h.JobRepo.UpdateJobStatus(ctx, jobID, status, progress, errMsg); err != nil { slog.Error("Failed to update job status in DB", "jobID", jobID, "error", err) } // Отправляем в WebSocket update := map[string]interface{}{ "job_id": jobID, "status": status, "progress": progress, "error_message": errMsg, } msg, _ := json.Marshal(update) h.Hub.BroadcastMessage(msg) }