feat(modpack): added curseforge import by url
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user