diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java index 5c81d1d..33cf023 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java @@ -78,6 +78,9 @@ public class Main { return; } + // Проверка обновлений лаунчера + checkAndAutoUpdateLauncher(); + // Запускаем JavaFX окно UIWindow.start(port); } diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/api/install/InstallService.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/api/install/InstallService.java index d7020e2..064c064 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/api/install/InstallService.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/api/install/InstallService.java @@ -13,6 +13,12 @@ import java.util.Map; public class InstallService { + private PackDownloader.ProgressCallback progressCallback; + + public void setProgressCallback(PackDownloader.ProgressCallback callback) { + this.progressCallback = callback; + } + public ApiResponse installZernMCPack(String packName, String instanceName) { try { boolean created = InstanceManager.createInstanceFolder(instanceName); @@ -26,6 +32,9 @@ public class InstallService { } PackDownloader downloader = new PackDownloader(instance); + if (progressCallback != null) { + downloader.setProgressCallback(progressCallback); + } // Получаем список доступных сборок List availablePacks = downloader.getAvailablePacks(); diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java index 9150b8d..5eea0a7 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java @@ -29,12 +29,55 @@ public class PackDownloader { private final Instance instance; private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); private final HttpClient httpClient = HttpClient.newHttpClient(); - //private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + private ProgressCallback progressCallback; + + public interface ProgressCallback { + void onProgress(ProgressInfo info); + } + + public static class ProgressInfo { + private String phase; + private int totalFiles; + private int downloadedFiles; + private String currentFile; + private long fileSize; + private long downloadedBytes; + private int filePercent; + private int totalPercent; + private String eta; + + public ProgressInfo(String phase, int totalFiles, int downloadedFiles, String currentFile, + long fileSize, long downloadedBytes, int filePercent, int totalPercent, String eta) { + this.phase = phase; + this.totalFiles = totalFiles; + this.downloadedFiles = downloadedFiles; + this.currentFile = currentFile; + this.fileSize = fileSize; + this.downloadedBytes = downloadedBytes; + this.filePercent = filePercent; + this.totalPercent = totalPercent; + this.eta = eta; + } + + public String getPhase() { return phase; } + public int getTotalFiles() { return totalFiles; } + public int getDownloadedFiles() { return downloadedFiles; } + public String getCurrentFile() { return currentFile; } + public long getFileSize() { return fileSize; } + public long getDownloadedBytes() { return downloadedBytes; } + public int getFilePercent() { return filePercent; } + public int getTotalPercent() { return totalPercent; } + public String getEta() { return eta; } + } public PackDownloader(Instance instance) { this.instance = instance; } + public void setProgressCallback(ProgressCallback callback) { + this.progressCallback = callback; + } + /** * Получить список доступных паков с сервера */ @@ -398,16 +441,18 @@ public class PackDownloader { System.out.println(ZAnsi.cyan("\nПрименение изменений:")); System.out.println(" Загрузить: " + diff.getToDownload().size() + " файлов"); System.out.println(" Удалить: " + diff.getToDelete().size() + " файлов"); - - // Создаем директории если нужно + + if (progressCallback != null) { + progressCallback.onProgress(new ProgressInfo("starting", diff.getToDownload().size(), 0, "", 0, 0, 0, 0, "")); + } + try { Files.createDirectories(instance.getPath()); } catch (IOException e) { System.err.println(ZAnsi.red("Ошибка создания директорий: " + e.getMessage())); return false; } - - // Удаляем файлы + for (String filePath : diff.getToDelete()) { Path fullPath = instance.getPath().resolve(filePath); try { @@ -418,85 +463,103 @@ public class PackDownloader { System.err.println(ZAnsi.red(" Ошибка удаления " + filePath + ": " + e.getMessage())); } } - - // Скачиваем файлы + AtomicInteger downloaded = new AtomicInteger(0); int total = diff.getToDownload().size(); - + for (FileInfo file : diff.getToDownload()) { String path = file.getPath(); Path fullPath = instance.getPath().resolve(path); - + try { - // Создаем директории Files.createDirectories(fullPath.getParent()); - - // Скачиваем файл - downloadFile(file, fullPath); - - // Проверяем хеш + + downloadFile(file, fullPath, progressCallback, downloaded.get(), total); + String actualHash = calculateHash(fullPath); if (!actualHash.equals(file.getHash())) { - throw new IOException("Хеш не совпадает! Ожидался: " + file.getHash() + + throw new IOException("Хеш не совпадает! Ожидался: " + file.getHash() + ", получен: " + actualHash); } - + downloaded.incrementAndGet(); if (total > 0) { ProgressBar.show("Скачивание", downloaded.get(), total, "файлов"); } - + + if (progressCallback != null) { + progressCallback.onProgress(new ProgressInfo("downloading", total, downloaded.get(), path, + file.getSize(), file.getSize(), 100, (downloaded.get() * 100) / total, "")); + } + } catch (Exception e) { System.err.println("\n" + ZAnsi.red(" Ошибка скачивания " + path + ": " + e.getMessage())); return false; } } - + if (total > 0) { ProgressBar.finish("Скачивание"); } - + + if (progressCallback != null) { + progressCallback.onProgress(new ProgressInfo("complete", total, total, "", 0, 0, 100, 100, "")); + } + return true; } - /** +/** * Скачать один файл с сервера */ private void downloadFile(FileInfo file, Path destination) throws Exception { + downloadFile(file, destination, null, 0, 0); + } + + private void downloadFile(FileInfo file, Path destination, ProgressCallback callback, int downloadedFiles, int totalFiles) throws Exception { String url = ZHttpClient.getBaseUrl() + file.getUrl(); - + HttpRequest request = HttpRequest.newBuilder() .uri(java.net.URI.create(url)) .GET() .build(); - + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); - + if (response.statusCode() != 200) { throw new IOException("HTTP " + response.statusCode()); } - - // Скачиваем с прогрессом + try (InputStream in = response.body(); FileOutputStream out = new FileOutputStream(destination.toFile())) { - + byte[] buffer = new byte[8192]; int bytesRead; long totalRead = 0; long fileSize = file.getSize(); - + long lastCallbackTime = 0; + while ((bytesRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); totalRead += bytesRead; - - if (fileSize > 0 && totalRead % 8192 == 0) { + + if (fileSize > 0) { ProgressBar.showDownload(" " + file.getPath(), totalRead, fileSize); + + long now = System.currentTimeMillis(); + if (callback != null && now - lastCallbackTime > 200) { + int filePercent = (int) ((totalRead * 100) / fileSize); + int totalPercent = totalFiles > 0 ? ((downloadedFiles * 100 + filePercent) / totalFiles) : 0; + callback.onProgress(new ProgressInfo("downloading", totalFiles, downloadedFiles, file.getPath(), + fileSize, totalRead, filePercent, totalPercent, "")); + lastCallbackTime = now; + } } } + + ProgressBar.clearLine(); } - - ProgressBar.clearLine(); } /** diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/web/WebServer.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/web/WebServer.java index 74a2bc3..6cdf8d7 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/web/WebServer.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/web/WebServer.java @@ -5,13 +5,21 @@ import io.javalin.http.staticfiles.Location; import me.sashegdev.zernmc.launcher.api.ApiResponse; import me.sashegdev.zernmc.launcher.api.LauncherAPI; import me.sashegdev.zernmc.launcher.api.instance.InstanceService; +import me.sashegdev.zernmc.launcher.api.install.InstallService; import me.sashegdev.zernmc.launcher.auth.AuthManager; import me.sashegdev.zernmc.launcher.utils.ZAnsi; +import me.sashegdev.zernmc.launcher.utils.ZHttpClient; import java.awt.Desktop; import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.net.ServerSocket; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.List; import java.util.Map; @@ -182,6 +190,48 @@ public class WebServer { } }); + // SSE прогресс установки + app.get("/api/instances/{name}/install/stream", ctx -> { + ctx.header("Content-Type", "text/event-stream"); + ctx.header("Cache-Control", "no-cache"); + ctx.header("Connection", "keep-alive"); + + String instanceName = ctx.pathParam("name"); + var instanceInfo = api.instances().getInstance(instanceName); + + if (!instanceInfo.isSuccess() || instanceInfo.getData() == null) { + ctx.result("data: {\"phase\":\"error\",\"message\":\"Instance not found\"}\n\n"); + return; + } + + var os = ctx.outputStream(); + InstallService service = new InstallService(); + service.setProgressCallback(info -> { + try { + String json = String.format( + "{\"phase\":\"%s\",\"totalFiles\":%d,\"downloadedFiles\":%d,\"currentFile\":\"%s\",\"fileSize\":%d,\"downloadedBytes\":%d,\"filePercent\":%d,\"totalPercent\":%d,\"eta\":\"%s\"}", + info.getPhase(), info.getTotalFiles(), info.getDownloadedFiles(), + info.getCurrentFile() != null ? info.getCurrentFile().replace("\"", "\\\"") : "", + info.getFileSize(), info.getDownloadedBytes(), + info.getFilePercent(), info.getTotalPercent(), + info.getEta() != null ? info.getEta() : "" + ); + os.write(("data: " + json + "\n\n").getBytes()); + os.flush(); + } catch (Exception e) {} + }); + + var result = service.installZernMCPack(instanceInfo.getData().getServerPackName(), instanceName); + try { + if (!result.isSuccess()) { + os.write(("data: {\"phase\":\"error\",\"message\":\"" + result.getError().replace("\"", "\\\"") + "\"}\n\n").getBytes()); + } else { + os.write("data: {\"phase\":\"complete\"}\n\n".getBytes()); + } + os.flush(); + } catch (Exception e) {} + }); + // Проверка обновлений app.get("/api/instances/{name}/updates", ctx -> { String name = ctx.pathParam("name"); @@ -326,4 +376,88 @@ public class WebServer { app.stop(); } } + + // ==================== LAUNCHER AUTO-UPDATE ==================== + public static void checkLauncherUpdate() { + try { + String json = ZHttpClient.getLauncherVersionInfo(); + String serverVersion = extractVersion(json); + String currentVersion = me.sashegdev.zernmc.launcher.utils.Version.getCurrentVersion(); + + if (me.sashegdev.zernmc.launcher.utils.Version.isNewer(currentVersion, serverVersion)) { + System.out.println(ZAnsi.brightYellow("\nДоступна новая версия лаунчера! (" + serverVersion + ")")); + System.out.println(ZAnsi.cyan("Начинается автоматическое обновление...\n")); + performLauncherUpdate(serverVersion); + restartLauncher(); + } else { + System.out.println(ZAnsi.brightGreen("Лаунчер актуален.")); + } + } catch (Exception e) { + System.out.println(ZAnsi.yellow("Не удалось проверить обновления лаунчера.")); + System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage()); + } + } + + private static void performLauncherUpdate(String newVersion) throws Exception { + String downloadUrl = ZHttpClient.getBaseUrl() + "/launcher/download?type=jar"; + Path currentJar = getCurrentJarPath(); + Path tempJar = currentJar.getParent().resolve("zernmc-launcher-new.jar"); + + System.out.println(ZAnsi.cyan("Скачивание версии " + newVersion + "...")); + + HttpClient client = HttpClient.newBuilder().build(); + HttpRequest request = HttpRequest.newBuilder() + .uri(java.net.URI.create(downloadUrl)) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofFile(tempJar)); + + if (response.statusCode() != 200) { + throw new IOException("Сервер вернул код: " + response.statusCode()); + } + + long size = Files.size(tempJar); + System.out.println(ZAnsi.brightGreen("Скачано успешно (" + (size / 1024) + " KB)")); + + Files.move(tempJar, currentJar, StandardCopyOption.REPLACE_EXISTING); + System.out.println(ZAnsi.brightGreen("Обновление успешно установлено!")); + } + + private static void restartLauncher() { + try { + String javaPath = System.getProperty("java.home") + "/bin/java"; + String jarPath = getCurrentJarPath().toAbsolutePath().toString(); + + System.out.println(ZAnsi.brightGreen("Перезапуск лаунчера с новой версией...")); + + new ProcessBuilder(javaPath, "-jar", jarPath) + .inheritIO() + .start(); + + System.exit(0); + } catch (Exception e) { + System.err.println(ZAnsi.brightRed("Не удалось перезапустить лаунчер.")); + System.exit(1); + } + } + + private static String extractVersion(String json) { + try { + return json.replaceAll(".*\"version\"\\s*:\\s*\"([^\"]+)\".*", "$1"); + } catch (Exception e) { + return "unknown"; + } + } + + private static Path getCurrentJarPath() { + try { + return Path.of(me.sashegdev.zernmc.launcher.Main.class.getProtectionDomain() + .getCodeSource() + .getLocation() + .toURI()); + } catch (Exception e) { + return Path.of("zernmc-launcher.jar"); + } + } } \ No newline at end of file