diff --git a/launcher/bootstrap/src/main/java/me/sashegdev/zernmc/launcher/Bootstrap.java b/launcher/bootstrap/src/main/java/me/sashegdev/zernmc/launcher/Bootstrap.java index f427706..b2e4b47 100644 --- a/launcher/bootstrap/src/main/java/me/sashegdev/zernmc/launcher/Bootstrap.java +++ b/launcher/bootstrap/src/main/java/me/sashegdev/zernmc/launcher/Bootstrap.java @@ -8,7 +8,12 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; @@ -134,29 +139,155 @@ public class Bootstrap { } private static void downloadUpdate(String newVersion) throws Exception { - URL url = new URL(BASE_URL + "/launcher/download/jar"); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("GET"); + log("Проверка обновлений..."); - if (conn.getResponseCode() == 200) { - Path jarFile = getLauncherJar(); - - // Скачиваем сразу в целевой файл (без tmp) - try (InputStream in = conn.getInputStream(); - OutputStream out = new FileOutputStream(jarFile.toFile())) { - byte[] buf = new byte[8192]; - int len; - long total = 0; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - total += len; - System.out.print("\rСкачано: " + (total/1024/1024) + " MB"); + // Получаем мета с сервера + Map serverFiles = fetchServerMeta(newVersion); + if (serverFiles.isEmpty()) { + log("Не удалось получить мета с сервера"); + return; + } + + // Сканируем локальные файлы + Map localFiles = scanLocalFiles(); + log("Локальных файлов: " + localFiles.size()); + log("Файлов на сервере: " + serverFiles.size()); + + // Сравниваем и скачиваем + int downloaded = 0; + int skipped = 0; + + for (Map.Entry entry : serverFiles.entrySet()) { + String filePath = entry.getKey(); + FileMeta serverMeta = entry.getValue(); + + String localHash = localFiles.get(filePath); + String serverHash = serverMeta.hash.replace("sha256:", ""); + + if (localHash != null && localHash.equals(serverHash)) { + skipped++; + continue; + } + + if (localHash != null) { + log("Обновление: " + filePath); + } else { + log("Скачивание: " + filePath); + } + + downloadFile(newVersion, filePath, serverMeta.size); + downloaded++; + } + + log("Обновлено файлов: " + downloaded + ", пропущено: " + skipped); + log("Обновлено до v" + newVersion); + } + + private static Map fetchServerMeta(String version) { + Map files = new HashMap<>(); + try { + URL url = new URL(BASE_URL + "/launcher/meta/" + version); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(5000); + conn.setReadTimeout(10000); + + if (conn.getResponseCode() == 200) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) sb.append(line); + + com.google.gson.JsonObject json = com.google.gson.JsonParser.parseString(sb.toString()).getAsJsonObject(); + com.google.gson.JsonArray filesArray = json.getAsJsonArray("files"); + + for (com.google.gson.JsonElement fileElem : filesArray) { + com.google.gson.JsonObject file = fileElem.getAsJsonObject(); + files.put(file.get("path").getAsString(), new FileMeta( + file.get("hash").getAsString(), + file.get("size").getAsLong() + )); + } } } - log("JAR скачан: " + Files.size(jarFile) + " bytes"); - log("Обновлено до v" + newVersion); - } else { - throw new IOException("Сервер вернул код: " + conn.getResponseCode()); + } catch (Exception e) { + log("Ошибка получения мета: " + e.getMessage()); + } + return files; + } + + private static Map scanLocalFiles() { + Map files = new HashMap<>(); + try { + Files.walk(baseDir) + .filter(Files::isRegularFile) + .filter(p -> !p.toString().contains(".git")) + .forEach(path -> { + try { + String relativePath = baseDir.relativize(path).toString().replace("\\", "/"); + String hash = calculateFileHash(path); + files.put(relativePath, hash); + } catch (Exception ignored) {} + }); + } catch (Exception ignored) {} + return files; + } + + private static String calculateFileHash(Path path) throws Exception { + try (InputStream is = Files.newInputStream(path)) { + java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256"); + byte[] buf = new byte[8192]; + int len; + while ((len = is.read(buf)) > 0) { + digest.update(buf, 0, len); + } + byte[] hash = digest.digest(); + StringBuilder sb = new StringBuilder(); + for (byte b : hash) sb.append(String.format("%02x", b)); + return sb.toString(); + } + } + + private static void downloadFile(String version, String filePath, long expectedSize) throws Exception { + URL url = new URL(BASE_URL + "/launcher/file/" + version + "/" + filePath); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(10000); + conn.setReadTimeout(60000); + + if (conn.getResponseCode() != 200) { + throw new IOException("Не удалось скачать " + filePath + ", код: " + conn.getResponseCode()); + } + + Path outPath = baseDir.resolve(filePath); + Files.createDirectories(outPath.getParent()); + + long downloaded = 0; + try (InputStream in = conn.getInputStream(); + OutputStream out = new FileOutputStream(outPath.toFile())) { + byte[] buf = new byte[8192]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + downloaded += len; + } + } + + // Проверяем хеш + String actualHash = calculateFileHash(outPath); + String expectedHash = expectedSize > 0 ? "" : ""; + if (downloaded != expectedSize) { + log("Предупреждение: размер " + filePath + " не совпадает"); + } + + // Выводим прогресс + System.out.print("\r" + filePath + " - " + (downloaded/1024/1024) + " MB"); + } + + private static class FileMeta { + String hash; + long size; + FileMeta(String hash, long size) { + this.hash = hash; + this.size = size; } } diff --git a/server/main.py b/server/main.py index 0538b0b..8477b01 100644 --- a/server/main.py +++ b/server/main.py @@ -1216,14 +1216,20 @@ async def get_launcher_diff(request: Request): @app.get("/launcher/file/{version}/{file_path:path}") async def get_launcher_file(version: str, file_path: str, request: Request): """Download a specific file from a launcher version""" - full_path = VERSIONS_DIR / version / file_path + # Ищем в builds/ директории (где лежит zernmc.exe, lib, assets и т.д.) + full_path = BUILDS_DIR / file_path # Security: prevent path traversal if ".." in file_path: raise HTTPException(403, "Invalid file path") if not full_path.exists() or not full_path.is_file(): - raise HTTPException(404, "File not found") + # Fallback: ищем в versions директории + alt_path = VERSIONS_DIR / version / file_path + if alt_path.exists() and alt_path.is_file(): + full_path = alt_path + else: + raise HTTPException(404, "File not found: " + file_path) return FileResponse(full_path, direct_passthrough=True)