162 lines
5.4 KiB
Go
162 lines
5.4 KiB
Go
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
|
||
}
|