diff --git a/internal/api/modpack_handler.go b/internal/api/modpack_handler.go index 108d415..ae4fbdd 100644 --- a/internal/api/modpack_handler.go +++ b/internal/api/modpack_handler.go @@ -25,40 +25,14 @@ func (h *ModpackHandler) ImportModpack(w http.ResponseWriter, r *http.Request) { return } - // Получаем тип импортера и метод из формы importerType := r.FormValue("importerType") importMethod := r.FormValue("importMethod") - // sourceURL := r.FormValue("sourceUrl") + sourceURL := r.FormValue("sourceUrl") - // --- Получаем zip-файл --- - tempFile, err := os.CreateTemp("", "modpack-*.zip") - if err != nil { - http.Error(w, "Could not create temp file", http.StatusInternalServerError) - return - } - defer os.Remove(tempFile.Name()) - defer tempFile.Close() + var tempZipPath string + var err error - if importMethod == "file" { - file, _, err := r.FormFile("file") - if err != nil { - http.Error(w, "Invalid file upload", http.StatusBadRequest) - return - } - defer file.Close() - if _, err := io.Copy(tempFile, file); err != nil { - http.Error(w, "Could not save temp file", http.StatusInternalServerError) - return - } - } else if importMethod == "url" { - http.Error(w, "Import by URL is not implemented yet", http.StatusNotImplemented) - return - } else { - http.Error(w, "Invalid import method", http.StatusBadRequest) - return - } - - // --- Выбираем и запускаем импортер --- + // --- Выбираем импортер --- var imp importer.ModpackImporter storagePath := os.Getenv("MODPACKS_STORAGE_PATH") @@ -77,7 +51,49 @@ func (h *ModpackHandler) ImportModpack(w http.ResponseWriter, r *http.Request) { return } - files, err := imp.Import(tempFile.Name()) + // --- Получаем zip-файл --- + if 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 + } + defer tempFile.Close() + defer os.Remove(tempFile.Name()) + + if _, err := io.Copy(tempFile, file); err != nil { + http.Error(w, "Could not save temp file", http.StatusInternalServerError) + return + } + tempZipPath = tempFile.Name() + + } else if importMethod == "url" { + cfImporter, ok := imp.(*importer.CurseForgeImporter) + if !ok { + http.Error(w, "Importer type does not support URL import", http.StatusBadRequest) + return + } + tempZipPath, err = cfImporter.DownloadModpackFromURL(sourceURL) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to download from URL: %v", err), http.StatusInternalServerError) + return + } + defer os.Remove(tempZipPath) + + } else { + http.Error(w, "Invalid import method", http.StatusBadRequest) + return + } + + // --- Запускаем импорт --- + files, err := imp.Import(tempZipPath) if err != nil { http.Error(w, fmt.Sprintf("Import failed: %v", err), http.StatusInternalServerError) return diff --git a/internal/core/importer/curseforge.go b/internal/core/importer/curseforge.go index e84b8fc..59be5e0 100644 --- a/internal/core/importer/curseforge.go +++ b/internal/core/importer/curseforge.go @@ -5,7 +5,12 @@ import ( "context" "encoding/json" "fmt" + "io" + "log" "net/http" + "net/url" + "os" + "path" "path/filepath" "strings" "time" @@ -163,3 +168,109 @@ func (i *CurseForgeImporter) Import(zipPath string) ([]models.ModpackFile, error return files, nil } + +// findModpackBySlug ищет ID проекта по его "слагу" (части URL). +func (i *CurseForgeImporter) findModpackBySlug(slug string) (int, error) { + apiURL := fmt.Sprintf("https://api.curseforge.com/v1/mods/search?gameId=432&slug=%s", url.QueryEscape(slug)) + req, err := http.NewRequestWithContext(context.Background(), "GET", apiURL, nil) + if err != nil { + return 0, err + } + req.Header.Set("x-api-key", i.APIKey) + + resp, err := i.HTTPClient.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + var searchResp CurseForgeSearchResponse + if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { + return 0, err + } + + if len(searchResp.Data) == 0 { + return 0, fmt.Errorf("modpack with slug '%s' not found", slug) + } + + return searchResp.Data[0].ID, nil +} + +// getLatestModpackFileURL находит URL для скачивания последнего файла проекта. +func (i *CurseForgeImporter) getLatestModpackFileURL(projectID int) (string, error) { + apiURL := fmt.Sprintf("https://api.curseforge.com/v1/mods/%d/files", projectID) + req, err := http.NewRequestWithContext(context.Background(), "GET", apiURL, nil) + if err != nil { + return "", err + } + req.Header.Set("x-api-key", i.APIKey) + + resp, err := i.HTTPClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + var filesResp CurseForgeFilesResponse + if err := json.NewDecoder(resp.Body).Decode(&filesResp); err != nil { + return "", err + } + + if len(filesResp.Data) == 0 { + return "", fmt.Errorf("no files found for projectID %d", projectID) + } + + // Последний загруженный файл обычно идет первым в списке + latestFile := filesResp.Data[0] + return latestFile.DownloadURL, nil +} + +// DownloadModpackFromURL скачивает zip-архив модпака по URL страницы CurseForge. +func (i *CurseForgeImporter) DownloadModpackFromURL(pageURL string) (string, error) { + // 1. Парсим URL, чтобы извлечь slug + parsedURL, err := url.Parse(pageURL) + if err != nil { + return "", fmt.Errorf("invalid url: %w", err) + } + slug := path.Base(parsedURL.Path) + log.Printf("Importer: Extracted slug '%s' from URL", slug) + + // 2. Находим ID проекта по slug + projectID, err := i.findModpackBySlug(slug) + if err != nil { + return "", err + } + log.Printf("Importer: Found projectID %d for slug '%s'", projectID, slug) + + // 3. Получаем URL для скачивания последнего файла + downloadURL, err := i.getLatestModpackFileURL(projectID) + if err != nil { + return "", err + } + log.Printf("Importer: Found download URL: %s", downloadURL) + + // 4. Скачиваем zip-архив во временный файл + tempFile, err := os.CreateTemp("", "modpack-*.zip") + if err != nil { + return "", err + } + defer tempFile.Close() + + req, err := http.NewRequestWithContext(context.Background(), "GET", downloadURL, nil) + if err != nil { + return "", err + } + + resp, err := i.HTTPClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if _, err := io.Copy(tempFile, resp.Body); err != nil { + return "", err + } + + log.Printf("Importer: Successfully downloaded modpack to %s", tempFile.Name()) + return tempFile.Name(), nil +} diff --git a/internal/core/importer/curseforge_types.go b/internal/core/importer/curseforge_types.go index 924fdd9..5c95256 100644 --- a/internal/core/importer/curseforge_types.go +++ b/internal/core/importer/curseforge_types.go @@ -21,6 +21,26 @@ type CurseForgeManifest struct { Overrides string `json:"overrides"` } +// CurseForgeSearchResponse - ответ от эндпоинта поиска +type CurseForgeSearchResponse struct { + Data []struct { + ID int `json:"id"` + Slug string `json:"slug"` + } `json:"data"` +} + +// CurseForgeFilesResponse - ответ от эндпоинта получения файлов проекта +type CurseForgeFilesResponse struct { + Data []struct { + ID int `json:"id"` + FileName string `json:"fileName"` + DownloadURL string `json:"downloadUrl"` + } `json:"data"` + Pagination struct { + TotalCount int `json:"totalCount"` + } `json:"pagination"` +} + type CurseForgeFileResponse struct { Data struct { DownloadURL string `json:"downloadUrl"`