feat: add modpack update functionality
This commit is contained in:
@@ -134,7 +134,9 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/modpacks", func(r chi.Router) {
|
r.Route("/modpacks", func(r chi.Router) {
|
||||||
|
r.Get("/", modpackHandler.GetModpacks)
|
||||||
r.Post("/import", modpackHandler.ImportModpack)
|
r.Post("/import", modpackHandler.ImportModpack)
|
||||||
|
r.Post("/update", modpackHandler.UpdateModpack)
|
||||||
r.Get("/versions", modpackHandler.GetModpackVersions)
|
r.Get("/versions", modpackHandler.GetModpackVersions)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -282,3 +282,179 @@ func parseModpackURL(rawURL string) (string, error) {
|
|||||||
}
|
}
|
||||||
return slug, nil
|
return slug, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetModpacks возвращает список всех модпаков для админки
|
||||||
|
func (h *ModpackHandler) GetModpacks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
modpacks, err := h.ModpackRepo.GetAllModpacks(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get modpacks", "error", err)
|
||||||
|
http.Error(w, "Failed to get modpacks", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(modpacks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateJobParams содержит параметры для фоновой задачи обновления
|
||||||
|
type UpdateJobParams struct {
|
||||||
|
ImporterType string
|
||||||
|
ImportMethod string
|
||||||
|
SourceURL string
|
||||||
|
TempZipPath string
|
||||||
|
ModpackID int
|
||||||
|
ModpackName string
|
||||||
|
MCVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateModpack обрабатывает запрос на обновление модпака
|
||||||
|
func (h *ModpackHandler) UpdateModpack(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := r.ParseMultipartForm(512 << 20); err != nil {
|
||||||
|
http.Error(w, "File too large", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
modpackName := r.FormValue("modpackName")
|
||||||
|
if modpackName == "" {
|
||||||
|
http.Error(w, "Missing modpack name", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем модпак по имени
|
||||||
|
modpack, err := h.ModpackRepo.GetModpackByName(r.Context(), modpackName)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Modpack not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params := UpdateJobParams{
|
||||||
|
ImporterType: r.FormValue("importerType"),
|
||||||
|
ImportMethod: r.FormValue("importMethod"),
|
||||||
|
SourceURL: r.FormValue("sourceUrl"),
|
||||||
|
ModpackID: modpack.ID,
|
||||||
|
ModpackName: modpackName,
|
||||||
|
MCVersion: r.FormValue("mcVersion"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.MCVersion == "" {
|
||||||
|
params.MCVersion = modpack.MinecraftVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка загрузки файла
|
||||||
|
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-update-*.zip")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Could not create temp file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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.processUpdateJob(jobID, params)
|
||||||
|
|
||||||
|
// Отправляем ответ
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"job_id": jobID,
|
||||||
|
"message": "Update job started",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// processUpdateJob выполняет обновление модпака в фоне
|
||||||
|
func (h *ModpackHandler) processUpdateJob(jobID int, params UpdateJobParams) {
|
||||||
|
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...")
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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, "Updating database...")
|
||||||
|
|
||||||
|
// Обновление в БД
|
||||||
|
err = h.ModpackRepo.UpdateModpackTx(ctx, params.ModpackID, params.MCVersion, files)
|
||||||
|
if err != nil {
|
||||||
|
h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, fmt.Sprintf("Database update failed: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.updateJobStatus(ctx, jobID, models.JobStatusCompleted, 100, "Success")
|
||||||
|
|
||||||
|
// Запуск Janitor-а
|
||||||
|
go h.JanitorService.CleanOrphanedFiles(context.Background())
|
||||||
|
}
|
||||||
|
|||||||
@@ -126,3 +126,85 @@ func (r *ModpackRepository) GetModpacksSummary(ctx context.Context) ([]models.Mo
|
|||||||
|
|
||||||
return summaries, nil
|
return summaries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllModpacks возвращает список всех модпаков для админки.
|
||||||
|
func (r *ModpackRepository) GetAllModpacks(ctx context.Context) ([]models.Modpack, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, name, display_name, minecraft_version, is_active, created_at, updated_at
|
||||||
|
FROM modpacks
|
||||||
|
ORDER BY name`
|
||||||
|
|
||||||
|
rows, err := r.DB.Query(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var modpacks []models.Modpack
|
||||||
|
for rows.Next() {
|
||||||
|
var m models.Modpack
|
||||||
|
if err := rows.Scan(&m.ID, &m.Name, &m.DisplayName, &m.MinecraftVersion, &m.IsActive, &m.CreatedAt, &m.UpdatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
modpacks = append(modpacks, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return modpacks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateModpackTx обновляет файлы модпака в транзакции: удаляет старые, добавляет новые.
|
||||||
|
func (r *ModpackRepository) UpdateModpackTx(ctx context.Context, modpackID int, mcVersion string, files []models.ModpackFile) error {
|
||||||
|
tx, err := r.DB.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
// Обновляем версию Minecraft и updated_at
|
||||||
|
_, err = tx.Exec(ctx,
|
||||||
|
"UPDATE modpacks SET minecraft_version = $1, updated_at = NOW() WHERE id = $2",
|
||||||
|
mcVersion, modpackID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем старые файлы
|
||||||
|
_, err = tx.Exec(ctx, "DELETE FROM modpack_files WHERE modpack_id = $1", modpackID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем новые файлы
|
||||||
|
rows := make([][]interface{}, len(files))
|
||||||
|
for i, f := range files {
|
||||||
|
rows[i] = []interface{}{modpackID, f.RelativePath, f.FileHash, f.FileSize, f.DownloadURL}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.CopyFrom(
|
||||||
|
ctx,
|
||||||
|
pgx.Identifier{"modpack_files"},
|
||||||
|
[]string{"modpack_id", "relative_path", "file_hash", "file_size", "download_url"},
|
||||||
|
pgx.CopyFromRows(rows),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModpackByName возвращает модпак по имени.
|
||||||
|
func (r *ModpackRepository) GetModpackByName(ctx context.Context, name string) (*models.Modpack, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, name, display_name, minecraft_version, is_active, created_at, updated_at
|
||||||
|
FROM modpacks
|
||||||
|
WHERE name = $1`
|
||||||
|
|
||||||
|
var m models.Modpack
|
||||||
|
err := r.DB.QueryRow(ctx, query, name).Scan(&m.ID, &m.Name, &m.DisplayName, &m.MinecraftVersion, &m.IsActive, &m.CreatedAt, &m.UpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &m, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user