feat: implement Modrinth modpack importer (.mrpack)
This commit is contained in:
@@ -46,6 +46,8 @@ func (h *ModpackHandler) ImportModpack(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
imp = importer.NewCurseForgeImporter(storagePath, apiKey)
|
||||
case "modrinth":
|
||||
imp = importer.NewModrinthImporter(storagePath)
|
||||
default:
|
||||
http.Error(w, "Invalid importer type", http.StatusBadRequest)
|
||||
return
|
||||
|
||||
161
internal/core/importer/modrinth.go
Normal file
161
internal/core/importer/modrinth.go
Normal file
@@ -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
|
||||
}
|
||||
24
internal/core/importer/modrinth_types.go
Normal file
24
internal/core/importer/modrinth_types.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package importer
|
||||
|
||||
// ModrinthIndex - структура modrinth.index.json
|
||||
type ModrinthIndex struct {
|
||||
FormatVersion int `json:"formatVersion"`
|
||||
Game string `json:"game"`
|
||||
VersionID string `json:"versionId"`
|
||||
Name string `json:"name"`
|
||||
Summary string `json:"summary"`
|
||||
Files []struct {
|
||||
Path string `json:"path"`
|
||||
Hashes struct {
|
||||
SHA1 string `json:"sha1"`
|
||||
SHA512 string `json:"sha512"`
|
||||
} `json:"hashes"`
|
||||
Env struct {
|
||||
Client string `json:"client"`
|
||||
Server string `json:"server"`
|
||||
} `json:"env"`
|
||||
Downloads []string `json:"downloads"`
|
||||
FileSize int64 `json:"fileSize"` // В спецификации это поле может быть uint64, но int64 удобнее
|
||||
} `json:"files"`
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
}
|
||||
Reference in New Issue
Block a user