diff --git a/cmd/server/main.go b/cmd/server/main.go index 8585a7e..fba73a0 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -134,7 +134,9 @@ func main() { }) r.Route("/modpacks", func(r chi.Router) { + r.Get("/", modpackHandler.GetModpacks) r.Post("/import", modpackHandler.ImportModpack) + r.Post("/update", modpackHandler.UpdateModpack) r.Get("/versions", modpackHandler.GetModpackVersions) }) diff --git a/internal/api/modpack_handler.go b/internal/api/modpack_handler.go index 5b2aef4..4ea7c59 100644 --- a/internal/api/modpack_handler.go +++ b/internal/api/modpack_handler.go @@ -282,3 +282,179 @@ func parseModpackURL(rawURL string) (string, error) { } return slug, nil } + +// GetModpacks возвращает список всех модпаков для админки +func (h *ModpackHandler) GetModpacks(w http.ResponseWriter, r *http.Request) { + modpacks, err := h.ModpackRepo.GetAllModpacks(r.Context()) + if err != nil { + slog.Error("Failed to get modpacks", "error", err) + http.Error(w, "Failed to get modpacks", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(modpacks) +} + +// UpdateJobParams содержит параметры для фоновой задачи обновления +type UpdateJobParams struct { + ImporterType string + ImportMethod string + SourceURL string + TempZipPath string + ModpackID int + ModpackName string + MCVersion string +} + +// UpdateModpack обрабатывает запрос на обновление модпака +func (h *ModpackHandler) UpdateModpack(w http.ResponseWriter, r *http.Request) { + if err := r.ParseMultipartForm(512 << 20); err != nil { + http.Error(w, "File too large", http.StatusBadRequest) + return + } + + modpackName := r.FormValue("modpackName") + if modpackName == "" { + http.Error(w, "Missing modpack name", http.StatusBadRequest) + return + } + + // Получаем модпак по имени + modpack, err := h.ModpackRepo.GetModpackByName(r.Context(), modpackName) + if err != nil { + http.Error(w, "Modpack not found", http.StatusNotFound) + return + } + + params := UpdateJobParams{ + ImporterType: r.FormValue("importerType"), + ImportMethod: r.FormValue("importMethod"), + SourceURL: r.FormValue("sourceUrl"), + ModpackID: modpack.ID, + ModpackName: modpackName, + MCVersion: r.FormValue("mcVersion"), + } + + if params.MCVersion == "" { + params.MCVersion = modpack.MinecraftVersion + } + + // Обработка загрузки файла + 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-update-*.zip") + if err != nil { + http.Error(w, "Could not create temp file", http.StatusInternalServerError) + return + } + + 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.processUpdateJob(jobID, params) + + // Отправляем ответ + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(map[string]interface{}{ + "job_id": jobID, + "message": "Update job started", + }) +} + +// processUpdateJob выполняет обновление модпака в фоне +func (h *ModpackHandler) processUpdateJob(jobID int, params UpdateJobParams) { + 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...") + + 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 { + 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, "Updating database...") + + // Обновление в БД + err = h.ModpackRepo.UpdateModpackTx(ctx, params.ModpackID, params.MCVersion, files) + if err != nil { + h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, fmt.Sprintf("Database update failed: %v", err)) + return + } + + h.updateJobStatus(ctx, jobID, models.JobStatusCompleted, 100, "Success") + + // Запуск Janitor-а + go h.JanitorService.CleanOrphanedFiles(context.Background()) +} diff --git a/internal/database/modpack_repository.go b/internal/database/modpack_repository.go index b17a088..b71e7f1 100644 --- a/internal/database/modpack_repository.go +++ b/internal/database/modpack_repository.go @@ -126,3 +126,85 @@ func (r *ModpackRepository) GetModpacksSummary(ctx context.Context) ([]models.Mo return summaries, nil } + +// GetAllModpacks возвращает список всех модпаков для админки. +func (r *ModpackRepository) GetAllModpacks(ctx context.Context) ([]models.Modpack, error) { + query := ` + SELECT id, name, display_name, minecraft_version, is_active, created_at, updated_at + FROM modpacks + ORDER BY name` + + rows, err := r.DB.Query(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var modpacks []models.Modpack + for rows.Next() { + var m models.Modpack + if err := rows.Scan(&m.ID, &m.Name, &m.DisplayName, &m.MinecraftVersion, &m.IsActive, &m.CreatedAt, &m.UpdatedAt); err != nil { + return nil, err + } + modpacks = append(modpacks, m) + } + + return modpacks, nil +} + +// UpdateModpackTx обновляет файлы модпака в транзакции: удаляет старые, добавляет новые. +func (r *ModpackRepository) UpdateModpackTx(ctx context.Context, modpackID int, mcVersion string, files []models.ModpackFile) error { + tx, err := r.DB.Begin(ctx) + if err != nil { + return err + } + defer tx.Rollback(ctx) + + // Обновляем версию Minecraft и updated_at + _, err = tx.Exec(ctx, + "UPDATE modpacks SET minecraft_version = $1, updated_at = NOW() WHERE id = $2", + mcVersion, modpackID) + if err != nil { + return err + } + + // Удаляем старые файлы + _, err = tx.Exec(ctx, "DELETE FROM modpack_files WHERE modpack_id = $1", modpackID) + if err != nil { + return err + } + + // Добавляем новые файлы + rows := make([][]interface{}, len(files)) + for i, f := range files { + rows[i] = []interface{}{modpackID, f.RelativePath, f.FileHash, f.FileSize, f.DownloadURL} + } + + _, err = tx.CopyFrom( + ctx, + pgx.Identifier{"modpack_files"}, + []string{"modpack_id", "relative_path", "file_hash", "file_size", "download_url"}, + pgx.CopyFromRows(rows), + ) + if err != nil { + return err + } + + return tx.Commit(ctx) +} + +// GetModpackByName возвращает модпак по имени. +func (r *ModpackRepository) GetModpackByName(ctx context.Context, name string) (*models.Modpack, error) { + query := ` + SELECT id, name, display_name, minecraft_version, is_active, created_at, updated_at + FROM modpacks + WHERE name = $1` + + var m models.Modpack + err := r.DB.QueryRow(ctx, query, name).Scan(&m.ID, &m.Name, &m.DisplayName, &m.MinecraftVersion, &m.IsActive, &m.CreatedAt, &m.UpdatedAt) + if err != nil { + return nil, err + } + + return &m, nil +}