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
|
return
|
||||||
}
|
}
|
||||||
imp = importer.NewCurseForgeImporter(storagePath, apiKey)
|
imp = importer.NewCurseForgeImporter(storagePath, apiKey)
|
||||||
|
case "modrinth":
|
||||||
|
imp = importer.NewModrinthImporter(storagePath)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Invalid importer type", http.StatusBadRequest)
|
http.Error(w, "Invalid importer type", http.StatusBadRequest)
|
||||||
return
|
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