feat(ui): добавляем прогресс-коллбэк и автообновление лаунчера
- добавили ProgressCallback в PackDownloader для отслеживания прогресса
- SSE эндпоинт /instances/{name}/install/stream для стриминга прогресса
- добавили checkLauncherUpdate() в WebServer для обновления самого лаунчера
- теперь при запуске UI проверяются обновления лаунчера автоматически
This commit is contained in:
@@ -78,6 +78,9 @@ public class Main {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверка обновлений лаунчера
|
||||||
|
checkAndAutoUpdateLauncher();
|
||||||
|
|
||||||
// Запускаем JavaFX окно
|
// Запускаем JavaFX окно
|
||||||
UIWindow.start(port);
|
UIWindow.start(port);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ import java.util.Map;
|
|||||||
|
|
||||||
public class InstallService {
|
public class InstallService {
|
||||||
|
|
||||||
|
private PackDownloader.ProgressCallback progressCallback;
|
||||||
|
|
||||||
|
public void setProgressCallback(PackDownloader.ProgressCallback callback) {
|
||||||
|
this.progressCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
public ApiResponse<InstallResult> installZernMCPack(String packName, String instanceName) {
|
public ApiResponse<InstallResult> installZernMCPack(String packName, String instanceName) {
|
||||||
try {
|
try {
|
||||||
boolean created = InstanceManager.createInstanceFolder(instanceName);
|
boolean created = InstanceManager.createInstanceFolder(instanceName);
|
||||||
@@ -26,6 +32,9 @@ public class InstallService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PackDownloader downloader = new PackDownloader(instance);
|
PackDownloader downloader = new PackDownloader(instance);
|
||||||
|
if (progressCallback != null) {
|
||||||
|
downloader.setProgressCallback(progressCallback);
|
||||||
|
}
|
||||||
|
|
||||||
// Получаем список доступных сборок
|
// Получаем список доступных сборок
|
||||||
List<ServerPack> availablePacks = downloader.getAvailablePacks();
|
List<ServerPack> availablePacks = downloader.getAvailablePacks();
|
||||||
|
|||||||
@@ -29,12 +29,55 @@ public class PackDownloader {
|
|||||||
private final Instance instance;
|
private final Instance instance;
|
||||||
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
|
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
|
||||||
private final HttpClient httpClient = HttpClient.newHttpClient();
|
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) {
|
public PackDownloader(Instance instance) {
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setProgressCallback(ProgressCallback callback) {
|
||||||
|
this.progressCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить список доступных паков с сервера
|
* Получить список доступных паков с сервера
|
||||||
*/
|
*/
|
||||||
@@ -399,7 +442,10 @@ public class PackDownloader {
|
|||||||
System.out.println(" Загрузить: " + diff.getToDownload().size() + " файлов");
|
System.out.println(" Загрузить: " + diff.getToDownload().size() + " файлов");
|
||||||
System.out.println(" Удалить: " + diff.getToDelete().size() + " файлов");
|
System.out.println(" Удалить: " + diff.getToDelete().size() + " файлов");
|
||||||
|
|
||||||
// Создаем директории если нужно
|
if (progressCallback != null) {
|
||||||
|
progressCallback.onProgress(new ProgressInfo("starting", diff.getToDownload().size(), 0, "", 0, 0, 0, 0, ""));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(instance.getPath());
|
Files.createDirectories(instance.getPath());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -407,7 +453,6 @@ public class PackDownloader {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем файлы
|
|
||||||
for (String filePath : diff.getToDelete()) {
|
for (String filePath : diff.getToDelete()) {
|
||||||
Path fullPath = instance.getPath().resolve(filePath);
|
Path fullPath = instance.getPath().resolve(filePath);
|
||||||
try {
|
try {
|
||||||
@@ -419,7 +464,6 @@ public class PackDownloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Скачиваем файлы
|
|
||||||
AtomicInteger downloaded = new AtomicInteger(0);
|
AtomicInteger downloaded = new AtomicInteger(0);
|
||||||
int total = diff.getToDownload().size();
|
int total = diff.getToDownload().size();
|
||||||
|
|
||||||
@@ -428,13 +472,10 @@ public class PackDownloader {
|
|||||||
Path fullPath = instance.getPath().resolve(path);
|
Path fullPath = instance.getPath().resolve(path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Создаем директории
|
|
||||||
Files.createDirectories(fullPath.getParent());
|
Files.createDirectories(fullPath.getParent());
|
||||||
|
|
||||||
// Скачиваем файл
|
downloadFile(file, fullPath, progressCallback, downloaded.get(), total);
|
||||||
downloadFile(file, fullPath);
|
|
||||||
|
|
||||||
// Проверяем хеш
|
|
||||||
String actualHash = calculateHash(fullPath);
|
String actualHash = calculateHash(fullPath);
|
||||||
if (!actualHash.equals(file.getHash())) {
|
if (!actualHash.equals(file.getHash())) {
|
||||||
throw new IOException("Хеш не совпадает! Ожидался: " + file.getHash() +
|
throw new IOException("Хеш не совпадает! Ожидался: " + file.getHash() +
|
||||||
@@ -446,6 +487,11 @@ public class PackDownloader {
|
|||||||
ProgressBar.show("Скачивание", downloaded.get(), total, "файлов");
|
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) {
|
} catch (Exception e) {
|
||||||
System.err.println("\n" + ZAnsi.red(" Ошибка скачивания " + path + ": " + e.getMessage()));
|
System.err.println("\n" + ZAnsi.red(" Ошибка скачивания " + path + ": " + e.getMessage()));
|
||||||
return false;
|
return false;
|
||||||
@@ -456,6 +502,10 @@ public class PackDownloader {
|
|||||||
ProgressBar.finish("Скачивание");
|
ProgressBar.finish("Скачивание");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (progressCallback != null) {
|
||||||
|
progressCallback.onProgress(new ProgressInfo("complete", total, total, "", 0, 0, 100, 100, ""));
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,6 +513,10 @@ public class PackDownloader {
|
|||||||
* Скачать один файл с сервера
|
* Скачать один файл с сервера
|
||||||
*/
|
*/
|
||||||
private void downloadFile(FileInfo file, Path destination) throws Exception {
|
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();
|
String url = ZHttpClient.getBaseUrl() + file.getUrl();
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
@@ -477,7 +531,6 @@ public class PackDownloader {
|
|||||||
throw new IOException("HTTP " + response.statusCode());
|
throw new IOException("HTTP " + response.statusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Скачиваем с прогрессом
|
|
||||||
try (InputStream in = response.body();
|
try (InputStream in = response.body();
|
||||||
FileOutputStream out = new FileOutputStream(destination.toFile())) {
|
FileOutputStream out = new FileOutputStream(destination.toFile())) {
|
||||||
|
|
||||||
@@ -485,19 +538,29 @@ public class PackDownloader {
|
|||||||
int bytesRead;
|
int bytesRead;
|
||||||
long totalRead = 0;
|
long totalRead = 0;
|
||||||
long fileSize = file.getSize();
|
long fileSize = file.getSize();
|
||||||
|
long lastCallbackTime = 0;
|
||||||
|
|
||||||
while ((bytesRead = in.read(buffer)) != -1) {
|
while ((bytesRead = in.read(buffer)) != -1) {
|
||||||
out.write(buffer, 0, bytesRead);
|
out.write(buffer, 0, bytesRead);
|
||||||
totalRead += bytesRead;
|
totalRead += bytesRead;
|
||||||
|
|
||||||
if (fileSize > 0 && totalRead % 8192 == 0) {
|
if (fileSize > 0) {
|
||||||
ProgressBar.showDownload(" " + file.getPath(), totalRead, fileSize);
|
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();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Вычисление SHA256 хеша файла
|
* Вычисление SHA256 хеша файла
|
||||||
|
|||||||
@@ -5,13 +5,21 @@ import io.javalin.http.staticfiles.Location;
|
|||||||
import me.sashegdev.zernmc.launcher.api.ApiResponse;
|
import me.sashegdev.zernmc.launcher.api.ApiResponse;
|
||||||
import me.sashegdev.zernmc.launcher.api.LauncherAPI;
|
import me.sashegdev.zernmc.launcher.api.LauncherAPI;
|
||||||
import me.sashegdev.zernmc.launcher.api.instance.InstanceService;
|
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.auth.AuthManager;
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
||||||
|
import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
|
||||||
|
|
||||||
import java.awt.Desktop;
|
import java.awt.Desktop;
|
||||||
import java.io.IOException;
|
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.ServerSocket;
|
||||||
import java.net.URI;
|
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.List;
|
||||||
import java.util.Map;
|
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 -> {
|
app.get("/api/instances/{name}/updates", ctx -> {
|
||||||
String name = ctx.pathParam("name");
|
String name = ctx.pathParam("name");
|
||||||
@@ -326,4 +376,88 @@ public class WebServer {
|
|||||||
app.stop();
|
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<Path> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user