206 lines
6.6 KiB
Go
206 lines
6.6 KiB
Go
package api
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"log/slog"
|
||
"net/http"
|
||
"os"
|
||
|
||
"gitea.mrixs.me/minecraft-platform/backend/internal/core"
|
||
"gitea.mrixs.me/minecraft-platform/backend/internal/core/importer"
|
||
"gitea.mrixs.me/minecraft-platform/backend/internal/database"
|
||
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
|
||
"gitea.mrixs.me/minecraft-platform/backend/internal/ws"
|
||
)
|
||
|
||
type ModpackHandler struct {
|
||
ModpackRepo *database.ModpackRepository
|
||
JobRepo *database.JobRepository
|
||
JanitorService *core.FileJanitorService
|
||
Hub *ws.Hub
|
||
}
|
||
|
||
// ImportJobParams содержит параметры для фоновой задачи импорта
|
||
type ImportJobParams struct {
|
||
ImporterType string
|
||
ImportMethod string
|
||
SourceURL string
|
||
TempZipPath string
|
||
Name string
|
||
DisplayName string
|
||
MCVersion string
|
||
}
|
||
|
||
// ImportModpack обрабатывает запрос на импорт и запускает асинхронную задачу
|
||
func (h *ModpackHandler) ImportModpack(w http.ResponseWriter, r *http.Request) {
|
||
if err := r.ParseMultipartForm(512 << 20); err != nil {
|
||
http.Error(w, "File too large", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
params := ImportJobParams{
|
||
ImporterType: r.FormValue("importerType"),
|
||
ImportMethod: r.FormValue("importMethod"),
|
||
SourceURL: r.FormValue("sourceUrl"),
|
||
Name: r.FormValue("name"),
|
||
DisplayName: r.FormValue("displayName"),
|
||
MCVersion: r.FormValue("mcVersion"),
|
||
}
|
||
|
||
// Валидация
|
||
if params.Name == "" || params.DisplayName == "" || params.MCVersion == "" {
|
||
http.Error(w, "Missing required fields", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
// Обработка загрузки файла
|
||
if params.ImportMethod == "file" {
|
||
file, _, err := r.FormFile("file")
|
||
if err != nil {
|
||
http.Error(w, "Invalid file upload", http.StatusBadRequest)
|
||
return
|
||
}
|
||
defer file.Close()
|
||
|
||
tempFile, err := os.CreateTemp("", "modpack-*.zip")
|
||
if err != nil {
|
||
http.Error(w, "Could not create temp file", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
// Не удаляем файл здесь, так как он нужен worker-у. Удалим в worker-е.
|
||
// defer os.Remove(tempFile.Name())
|
||
|
||
if _, err := io.Copy(tempFile, file); err != nil {
|
||
tempFile.Close()
|
||
os.Remove(tempFile.Name())
|
||
http.Error(w, "Could not save temp file", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
tempFile.Close()
|
||
params.TempZipPath = tempFile.Name()
|
||
}
|
||
|
||
// Создаем задачу в БД
|
||
jobID, err := h.JobRepo.CreateJob(r.Context())
|
||
if err != nil {
|
||
if params.ImportMethod == "file" {
|
||
os.Remove(params.TempZipPath)
|
||
}
|
||
http.Error(w, fmt.Sprintf("Failed to create job: %v", err), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
// Запускаем worker
|
||
go h.processImportJob(jobID, params)
|
||
|
||
// Отправляем ответ
|
||
w.WriteHeader(http.StatusAccepted)
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"job_id": jobID,
|
||
"message": "Import job started",
|
||
})
|
||
}
|
||
|
||
// processImportJob выполняет импорт в фоне
|
||
func (h *ModpackHandler) processImportJob(jobID int, params ImportJobParams) {
|
||
ctx := context.Background()
|
||
h.updateJobStatus(ctx, jobID, models.JobStatusDownloading, 10, "")
|
||
|
||
// Очистка временного файла по завершении
|
||
if params.TempZipPath != "" {
|
||
defer os.Remove(params.TempZipPath)
|
||
}
|
||
|
||
storagePath := os.Getenv("MODPACKS_STORAGE_PATH")
|
||
var imp importer.ModpackImporter
|
||
|
||
// Настройка импортера
|
||
switch params.ImporterType {
|
||
case "simple":
|
||
imp = &importer.SimpleZipImporter{StoragePath: storagePath}
|
||
case "curseforge":
|
||
apiKey := os.Getenv("CURSEFORGE_API_KEY")
|
||
if apiKey == "" {
|
||
h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, "CurseForge API key missing")
|
||
return
|
||
}
|
||
imp = importer.NewCurseForgeImporter(storagePath, apiKey)
|
||
case "modrinth":
|
||
imp = importer.NewModrinthImporter(storagePath)
|
||
default:
|
||
h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, "Invalid importer type")
|
||
return
|
||
}
|
||
|
||
// Загрузка по URL если нужно
|
||
if params.ImportMethod == "url" {
|
||
h.updateJobStatus(ctx, jobID, models.JobStatusDownloading, 20, "Downloading from URL...")
|
||
|
||
// Логика скачивания зависит от типа импортера, пока поддерживаем CurseForge
|
||
// TODO: Сделать интерфейс DownloadableImporter
|
||
if cfImporter, ok := imp.(*importer.CurseForgeImporter); ok {
|
||
zipPath, err := cfImporter.DownloadModpackFromURL(params.SourceURL)
|
||
if err != nil {
|
||
h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, fmt.Sprintf("Download failed: %v", err))
|
||
return
|
||
}
|
||
params.TempZipPath = zipPath
|
||
defer os.Remove(zipPath) // Удаляем скачанный файл после обработки
|
||
} else {
|
||
// Для других типов пока не поддерживаем URL download внутри импортера
|
||
h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, "URL import not supported for this type")
|
||
return
|
||
}
|
||
}
|
||
|
||
h.updateJobStatus(ctx, jobID, models.JobStatusProcessing, 40, "Processing modpack files...")
|
||
|
||
// Импорт файлов
|
||
files, err := imp.Import(params.TempZipPath)
|
||
if err != nil {
|
||
h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, fmt.Sprintf("Import failed: %v", err))
|
||
return
|
||
}
|
||
|
||
h.updateJobStatus(ctx, jobID, models.JobStatusProcessing, 80, "Saving to database...")
|
||
|
||
// Сохранение в БД
|
||
modpack := &models.Modpack{
|
||
Name: params.Name,
|
||
DisplayName: params.DisplayName,
|
||
MinecraftVersion: params.MCVersion,
|
||
}
|
||
|
||
err = h.ModpackRepo.CreateModpackTx(ctx, modpack, files)
|
||
if err != nil {
|
||
h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, fmt.Sprintf("Database save failed: %v", err))
|
||
return
|
||
}
|
||
|
||
h.updateJobStatus(ctx, jobID, models.JobStatusCompleted, 100, "Success")
|
||
|
||
// Запуск Janitor-а
|
||
go h.JanitorService.CleanOrphanedFiles(context.Background())
|
||
}
|
||
|
||
// updateJobStatus обновляет статус в БД и отправляет уведомление через WebSocket
|
||
func (h *ModpackHandler) updateJobStatus(ctx context.Context, jobID int, status models.ImportJobStatus, progress int, errMsg string) {
|
||
// Обновляем БД
|
||
if err := h.JobRepo.UpdateJobStatus(ctx, jobID, status, progress, errMsg); err != nil {
|
||
slog.Error("Failed to update job status in DB", "jobID", jobID, "error", err)
|
||
}
|
||
|
||
// Отправляем в WebSocket
|
||
update := map[string]interface{}{
|
||
"job_id": jobID,
|
||
"status": status,
|
||
"progress": progress,
|
||
"error_message": errMsg,
|
||
}
|
||
msg, _ := json.Marshal(update)
|
||
h.Hub.BroadcastMessage(msg)
|
||
}
|