Compare commits

..

2 Commits

3 changed files with 188 additions and 40 deletions

View File

@@ -25,40 +25,14 @@ func (h *ModpackHandler) ImportModpack(w http.ResponseWriter, r *http.Request) {
return return
} }
// Получаем тип импортера и метод из формы
importerType := r.FormValue("importerType") importerType := r.FormValue("importerType")
importMethod := r.FormValue("importMethod") importMethod := r.FormValue("importMethod")
// sourceURL := r.FormValue("sourceUrl") sourceURL := r.FormValue("sourceUrl")
// --- Получаем zip-файл --- var tempZipPath string
tempFile, err := os.CreateTemp("", "modpack-*.zip") var err error
if err != nil {
http.Error(w, "Could not create temp file", http.StatusInternalServerError)
return
}
defer os.Remove(tempFile.Name())
defer tempFile.Close()
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 var imp importer.ModpackImporter
storagePath := os.Getenv("MODPACKS_STORAGE_PATH") storagePath := os.Getenv("MODPACKS_STORAGE_PATH")
@@ -77,7 +51,49 @@ func (h *ModpackHandler) ImportModpack(w http.ResponseWriter, r *http.Request) {
return 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 { if err != nil {
http.Error(w, fmt.Sprintf("Import failed: %v", err), http.StatusInternalServerError) http.Error(w, fmt.Sprintf("Import failed: %v", err), http.StatusInternalServerError)
return return

View File

@@ -5,7 +5,12 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"log"
"net/http" "net/http"
"net/url"
"os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
@@ -46,7 +51,6 @@ func (i *CurseForgeImporter) downloadAndProcessFile(url string) (hash string, si
return "", 0, fmt.Errorf("bad status downloading file: %s", resp.Status) return "", 0, fmt.Errorf("bad status downloading file: %s", resp.Status)
} }
// Используем тот же processFile, что и в SimpleZipImporter
baseImporter := SimpleZipImporter{StoragePath: i.StoragePath} baseImporter := SimpleZipImporter{StoragePath: i.StoragePath}
return baseImporter.processFile(resp.Body) return baseImporter.processFile(resp.Body)
} }
@@ -67,15 +71,128 @@ func (i *CurseForgeImporter) getFileDownloadURL(projectID, fileID int) (string,
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { 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) return "", fmt.Errorf("bad status getting download URL: %s", resp.Status)
} }
var apiResp CurseForgeFileResponse bodyBytes, err := io.ReadAll(resp.Body)
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { if err != nil {
return "", fmt.Errorf("failed to read download URL response body: %w", err)
}
downloadURL := string(bodyBytes)
if downloadURL == "" {
return "", fmt.Errorf("received empty download URL")
}
return downloadURL, 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 return "", err
} }
return apiResp.Data.DownloadURL, nil 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) {
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)
projectID, err := i.findModpackBySlug(slug)
if err != nil {
return "", err
}
log.Printf("Importer: Found projectID %d for slug '%s'", projectID, slug)
downloadURL, err := i.getLatestModpackFileURL(projectID)
if err != nil {
return "", err
}
log.Printf("Importer: Found download URL: %s", downloadURL)
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
} }
// Import реализует основной метод для CurseForge. // Import реализует основной метод для CurseForge.
@@ -111,7 +228,6 @@ func (i *CurseForgeImporter) Import(zipPath string) ([]models.ModpackFile, error
var files []models.ModpackFile var files []models.ModpackFile
// 1. Обрабатываем файлы из манифеста (моды)
for _, modFile := range manifest.Files { for _, modFile := range manifest.Files {
downloadURL, err := i.getFileDownloadURL(modFile.ProjectID, modFile.FileID) downloadURL, err := i.getFileDownloadURL(modFile.ProjectID, modFile.FileID)
if err != nil { if err != nil {
@@ -123,9 +239,6 @@ func (i *CurseForgeImporter) Import(zipPath string) ([]models.ModpackFile, error
return nil, fmt.Errorf("failed to process downloaded file for fileID %d: %w", modFile.FileID, err) 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)) relativePath := filepath.Join("mods", fmt.Sprintf("%d-%d.jar", modFile.ProjectID, modFile.FileID))
files = append(files, models.ModpackFile{ files = append(files, models.ModpackFile{
@@ -136,7 +249,6 @@ func (i *CurseForgeImporter) Import(zipPath string) ([]models.ModpackFile, error
}) })
} }
// 2. Обрабатываем файлы из папки overrides
baseImporter := SimpleZipImporter{StoragePath: i.StoragePath} baseImporter := SimpleZipImporter{StoragePath: i.StoragePath}
for _, f := range r.File { for _, f := range r.File {
if strings.HasPrefix(f.Name, manifest.Overrides+"/") && !f.FileInfo().IsDir() { if strings.HasPrefix(f.Name, manifest.Overrides+"/") && !f.FileInfo().IsDir() {

View File

@@ -21,6 +21,26 @@ type CurseForgeManifest struct {
Overrides string `json:"overrides"` 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 { type CurseForgeFileResponse struct {
Data struct { Data struct {
DownloadURL string `json:"downloadUrl"` DownloadURL string `json:"downloadUrl"`