diff --git a/internal/api/modpack_handler.go b/internal/api/modpack_handler.go index ae4fbdd..0bb839c 100644 --- a/internal/api/modpack_handler.go +++ b/internal/api/modpack_handler.go @@ -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 diff --git a/internal/core/importer/modrinth.go b/internal/core/importer/modrinth.go new file mode 100644 index 0000000..8811068 --- /dev/null +++ b/internal/core/importer/modrinth.go @@ -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 +} diff --git a/internal/core/importer/modrinth_types.go b/internal/core/importer/modrinth_types.go new file mode 100644 index 0000000..1fc87c8 --- /dev/null +++ b/internal/core/importer/modrinth_types.go @@ -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"` +}