package importer import ( "archive/zip" "context" "encoding/json" "fmt" "log" "net/http" "strings" "time" "gitea.mrixs.me/minecraft-platform/backend/internal/models" ) // ModrinthImporter реализует импорт для сборок Modrinth (.mrpack). type ModrinthImporter struct { StoragePath string HTTPClient *http.Client } // NewModrinthImporter создает новый экземпляр импортера. func NewModrinthImporter(storagePath string) *ModrinthImporter { return &ModrinthImporter{ StoragePath: storagePath, HTTPClient: &http.Client{Timeout: 10 * time.Minute}, } } // downloadAndProcessFile скачивает файл и обрабатывает его. func (i *ModrinthImporter) downloadAndProcessFile(url string) (hash string, size int64, err error) { req, err := http.NewRequestWithContext(context.Background(), "GET", url, nil) if err != nil { return "", 0, err } resp, err := i.HTTPClient.Do(req) if err != nil { return "", 0, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", 0, fmt.Errorf("bad status downloading file: %s", resp.Status) } baseImporter := SimpleZipImporter{StoragePath: i.StoragePath} return baseImporter.processFile(resp.Body) } // Import реализует основной метод интерфейса ModpackImporter. func (i *ModrinthImporter) Import(zipPath string) ([]models.ModpackFile, error) { r, err := zip.OpenReader(zipPath) if err != nil { return nil, err } defer r.Close() // 1. Ищем и парсим modrinth.index.json var indexFile *zip.File for _, f := range r.File { if f.Name == "modrinth.index.json" { indexFile = f break } } if indexFile == nil { return nil, fmt.Errorf("modrinth.index.json not found in archive") } idxReader, err := indexFile.Open() if err != nil { return nil, err } defer idxReader.Close() var index ModrinthIndex if err := json.NewDecoder(idxReader).Decode(&index); err != nil { return nil, fmt.Errorf("failed to parse modrinth.index.json: %w", err) } var files []models.ModpackFile // 2. Обрабатываем файлы из индекса (скачиваем их) for _, modFile := range index.Files { // Пропускаем серверные файлы, которые не нужны клиенту? // В ТЗ сказано про "клиент", но обычно лаунчер качает всё, что нужно клиенту. // env.client != "unsupported" (обычно "required" или "optional") if modFile.Env.Client == "unsupported" { continue } if len(modFile.Downloads) == 0 { log.Printf("Modrinth Importer: WARN - No download URLs for file '%s', skipping.", modFile.Path) continue } // Используем первый доступный URL downloadURL := modFile.Downloads[0] log.Printf("Modrinth Importer: Downloading '%s' from %s", modFile.Path, downloadURL) // Скачиваем файл и пересчитываем его хеш/размер (и сохраняем локально, если нужно) // Важно: Modrinth дает хеши в индексе. Мы могли бы использовать их, но наша система // построена на том, что мы храним файлы у себя. Поэтому нам всё равно нужно скачать их // и сохранить в наше хранилище. Метод downloadAndProcessFile делает именно это. hash, size, err := i.downloadAndProcessFile(downloadURL) if err != nil { log.Printf("Modrinth Importer: WARN - Failed to download/process '%s': %v", modFile.Path, err) continue } // Сверка хеша для надежности (опционально, но полезно) if modFile.Hashes.SHA1 != "" && modFile.Hashes.SHA1 != hash { log.Printf("Modrinth Importer: WARN - Hash mismatch for '%s'. Index SHA1: %s, Calculated: %s", modFile.Path, modFile.Hashes.SHA1, hash) // Можно решить: падать с ошибкой или доверять тому, что скачали. // Пока просто предупреждаем. } files = append(files, models.ModpackFile{ RelativePath: modFile.Path, FileHash: hash, FileSize: size, DownloadURL: downloadURL, }) } // 3. Обрабатываем overrides baseImporter := SimpleZipImporter{StoragePath: i.StoragePath} for _, f := range r.File { // В .mrpack файлы для копирования лежат в папке "overrides/" (как правило) // Спецификация говорит, что папка может называться иначе? Нет, обычно overrides. // Но лучше проверить все папки, кроме системных. // Спецификация Modrinth: "Files that are included in the modpack archive are located in the overrides directory." prefix := "overrides/" if strings.HasPrefix(f.Name, prefix) && !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, prefix) files = append(files, models.ModpackFile{ RelativePath: relativePath, FileHash: hash, FileSize: size, }) } } return files, nil }