Bootstrap: incremental update via meta, server: fix file endpoint paths

This commit is contained in:
SashegDev
2026-05-08 18:45:42 +00:00
parent 099df80cc6
commit 4697b16ab4
2 changed files with 159 additions and 22 deletions
@@ -8,7 +8,12 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.List; 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.Attributes;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import java.util.jar.Manifest; import java.util.jar.Manifest;
@@ -134,29 +139,155 @@ public class Bootstrap {
} }
private static void downloadUpdate(String newVersion) throws Exception { private static void downloadUpdate(String newVersion) throws Exception {
URL url = new URL(BASE_URL + "/launcher/download/jar"); log("Проверка обновлений...");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
if (conn.getResponseCode() == 200) { // Получаем мета с сервера
Path jarFile = getLauncherJar(); Map<String, FileMeta> serverFiles = fetchServerMeta(newVersion);
if (serverFiles.isEmpty()) {
log("Не удалось получить мета с сервера");
return;
}
// Скачиваем сразу в целевой файл (без tmp) // Сканируем локальные файлы
try (InputStream in = conn.getInputStream(); Map<String, String> localFiles = scanLocalFiles();
OutputStream out = new FileOutputStream(jarFile.toFile())) { log("Локальных файлов: " + localFiles.size());
byte[] buf = new byte[8192]; log("Файлов на сервере: " + serverFiles.size());
int len;
long total = 0; // Сравниваем и скачиваем
while ((len = in.read(buf)) > 0) { int downloaded = 0;
out.write(buf, 0, len); int skipped = 0;
total += len;
System.out.print("\rСкачано: " + (total/1024/1024) + " MB"); for (Map.Entry<String, FileMeta> 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<String, FileMeta> fetchServerMeta(String version) {
Map<String, FileMeta> 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"); } catch (Exception e) {
log("Обновлено до v" + newVersion); log("Ошибка получения мета: " + e.getMessage());
} else { }
throw new IOException("Сервер вернул код: " + conn.getResponseCode()); return files;
}
private static Map<String, String> scanLocalFiles() {
Map<String, String> 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;
} }
} }
+8 -2
View File
@@ -1216,14 +1216,20 @@ async def get_launcher_diff(request: Request):
@app.get("/launcher/file/{version}/{file_path:path}") @app.get("/launcher/file/{version}/{file_path:path}")
async def get_launcher_file(version: str, file_path: str, request: Request): async def get_launcher_file(version: str, file_path: str, request: Request):
"""Download a specific file from a launcher version""" """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 # Security: prevent path traversal
if ".." in file_path: if ".." in file_path:
raise HTTPException(403, "Invalid file path") raise HTTPException(403, "Invalid file path")
if not full_path.exists() or not full_path.is_file(): 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) return FileResponse(full_path, direct_passthrough=True)