From b9c5a7f73962aee3afda3674b501062768aee246 Mon Sep 17 00:00:00 2001 From: Vladimir Zagainov Date: Thu, 19 Jun 2025 17:16:20 +0300 Subject: [PATCH] fix(modpack): fixed curseforge import by url --- internal/core/importer/curseforge.go | 187 ++++++++++++++------------- 1 file changed, 94 insertions(+), 93 deletions(-) diff --git a/internal/core/importer/curseforge.go b/internal/core/importer/curseforge.go index 59be5e0..94d58e9 100644 --- a/internal/core/importer/curseforge.go +++ b/internal/core/importer/curseforge.go @@ -51,7 +51,6 @@ func (i *CurseForgeImporter) downloadAndProcessFile(url string) (hash string, si return "", 0, fmt.Errorf("bad status downloading file: %s", resp.Status) } - // Используем тот же processFile, что и в SimpleZipImporter baseImporter := SimpleZipImporter{StoragePath: i.StoragePath} return baseImporter.processFile(resp.Body) } @@ -72,101 +71,27 @@ func (i *CurseForgeImporter) getFileDownloadURL(projectID, fileID int) (string, defer resp.Body.Close() if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusMovedPermanently { + location, err := resp.Location() + if err != nil { + return "", fmt.Errorf("failed to get redirect location: %w", err) + } + return location.String(), nil + } return "", fmt.Errorf("bad status getting download URL: %s", resp.Status) } - var apiResp CurseForgeFileResponse - if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { - return "", err - } - - return apiResp.Data.DownloadURL, nil -} - -// Import реализует основной метод для CurseForge. -func (i *CurseForgeImporter) Import(zipPath string) ([]models.ModpackFile, error) { - r, err := zip.OpenReader(zipPath) + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, err - } - defer r.Close() - - var manifestFile *zip.File - for _, f := range r.File { - if f.Name == "manifest.json" { - manifestFile = f - break - } + return "", fmt.Errorf("failed to read download URL response body: %w", err) } - if manifestFile == nil { - return nil, fmt.Errorf("manifest.json not found in archive") + downloadURL := string(bodyBytes) + if downloadURL == "" { + return "", fmt.Errorf("received empty download URL") } - mfReader, err := manifestFile.Open() - if err != nil { - return nil, err - } - defer mfReader.Close() - - var manifest CurseForgeManifest - if err := json.NewDecoder(mfReader).Decode(&manifest); err != nil { - return nil, fmt.Errorf("failed to parse manifest.json: %w", err) - } - - var files []models.ModpackFile - - // 1. Обрабатываем файлы из манифеста (моды) - for _, modFile := range manifest.Files { - downloadURL, err := i.getFileDownloadURL(modFile.ProjectID, modFile.FileID) - if err != nil { - return nil, fmt.Errorf("failed to get download url for fileID %d: %w", modFile.FileID, err) - } - - hash, size, err := i.downloadAndProcessFile(downloadURL) - if err != nil { - return nil, fmt.Errorf("failed to process downloaded file for fileID %d: %w", modFile.FileID, err) - } - - // Путь для модов обычно "mods/filename.jar", но у нас нет имени файла. - // Лаунчеру это не важно, он будет сохранять по хешу. - // Для манифеста мы можем сгенерировать путь. - relativePath := filepath.Join("mods", fmt.Sprintf("%d-%d.jar", modFile.ProjectID, modFile.FileID)) - - files = append(files, models.ModpackFile{ - RelativePath: relativePath, - FileHash: hash, - FileSize: size, - DownloadURL: downloadURL, - }) - } - - // 2. Обрабатываем файлы из папки overrides - baseImporter := SimpleZipImporter{StoragePath: i.StoragePath} - for _, f := range r.File { - if strings.HasPrefix(f.Name, manifest.Overrides+"/") && !f.FileInfo().IsDir() { - fileReader, err := f.Open() - if err != nil { - return nil, err - } - - hash, size, err := baseImporter.processFile(fileReader) - if err != nil { - fileReader.Close() - return nil, err - } - fileReader.Close() - - relativePath := strings.TrimPrefix(f.Name, manifest.Overrides+"/") - files = append(files, models.ModpackFile{ - RelativePath: relativePath, - FileHash: hash, - FileSize: size, - }) - } - } - - return files, nil + return downloadURL, nil } // findModpackBySlug ищет ID проекта по его "слагу" (части URL). @@ -220,14 +145,12 @@ func (i *CurseForgeImporter) getLatestModpackFileURL(projectID int) (string, err 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) @@ -235,21 +158,18 @@ func (i *CurseForgeImporter) DownloadModpackFromURL(pageURL string) (string, 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 @@ -274,3 +194,84 @@ func (i *CurseForgeImporter) DownloadModpackFromURL(pageURL string) (string, err log.Printf("Importer: Successfully downloaded modpack to %s", tempFile.Name()) return tempFile.Name(), nil } + +// Import реализует основной метод для CurseForge. +func (i *CurseForgeImporter) Import(zipPath string) ([]models.ModpackFile, error) { + r, err := zip.OpenReader(zipPath) + if err != nil { + return nil, err + } + defer r.Close() + + var manifestFile *zip.File + for _, f := range r.File { + if f.Name == "manifest.json" { + manifestFile = f + break + } + } + + if manifestFile == nil { + return nil, fmt.Errorf("manifest.json not found in archive") + } + + mfReader, err := manifestFile.Open() + if err != nil { + return nil, err + } + defer mfReader.Close() + + var manifest CurseForgeManifest + if err := json.NewDecoder(mfReader).Decode(&manifest); err != nil { + return nil, fmt.Errorf("failed to parse manifest.json: %w", err) + } + + var files []models.ModpackFile + + for _, modFile := range manifest.Files { + downloadURL, err := i.getFileDownloadURL(modFile.ProjectID, modFile.FileID) + if err != nil { + return nil, fmt.Errorf("failed to get download url for fileID %d: %w", modFile.FileID, err) + } + + hash, size, err := i.downloadAndProcessFile(downloadURL) + if err != nil { + return nil, fmt.Errorf("failed to process downloaded file for fileID %d: %w", modFile.FileID, err) + } + + relativePath := filepath.Join("mods", fmt.Sprintf("%d-%d.jar", modFile.ProjectID, modFile.FileID)) + + files = append(files, models.ModpackFile{ + RelativePath: relativePath, + FileHash: hash, + FileSize: size, + DownloadURL: downloadURL, + }) + } + + baseImporter := SimpleZipImporter{StoragePath: i.StoragePath} + for _, f := range r.File { + if strings.HasPrefix(f.Name, manifest.Overrides+"/") && !f.FileInfo().IsDir() { + fileReader, err := f.Open() + if err != nil { + return nil, err + } + + hash, size, err := baseImporter.processFile(fileReader) + if err != nil { + fileReader.Close() + return nil, err + } + fileReader.Close() + + relativePath := strings.TrimPrefix(f.Name, manifest.Overrides+"/") + files = append(files, models.ModpackFile{ + RelativePath: relativePath, + FileHash: hash, + FileSize: size, + }) + } + } + + return files, nil +}