|
|
|
@@ -0,0 +1,161 @@
|
|
|
|
|
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
|
|
|
|
|
}
|