Bootstrap: incremental update via meta, server: fix file endpoint paths
This commit is contained in:
@@ -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("Проверка обновлений...");
|
||||||
|
|
||||||
|
// Получаем мета с сервера
|
||||||
|
Map<String, FileMeta> serverFiles = fetchServerMeta(newVersion);
|
||||||
|
if (serverFiles.isEmpty()) {
|
||||||
|
log("Не удалось получить мета с сервера");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сканируем локальные файлы
|
||||||
|
Map<String, String> localFiles = scanLocalFiles();
|
||||||
|
log("Локальных файлов: " + localFiles.size());
|
||||||
|
log("Файлов на сервере: " + serverFiles.size());
|
||||||
|
|
||||||
|
// Сравниваем и скачиваем
|
||||||
|
int downloaded = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
|
||||||
|
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();
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
conn.setRequestMethod("GET");
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(10000);
|
||||||
|
|
||||||
if (conn.getResponseCode() == 200) {
|
if (conn.getResponseCode() == 200) {
|
||||||
Path jarFile = getLauncherJar();
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) sb.append(line);
|
||||||
|
|
||||||
// Скачиваем сразу в целевой файл (без tmp)
|
com.google.gson.JsonObject json = com.google.gson.JsonParser.parseString(sb.toString()).getAsJsonObject();
|
||||||
try (InputStream in = conn.getInputStream();
|
com.google.gson.JsonArray filesArray = json.getAsJsonArray("files");
|
||||||
OutputStream out = new FileOutputStream(jarFile.toFile())) {
|
|
||||||
|
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()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log("Ошибка получения мета: " + e.getMessage());
|
||||||
|
}
|
||||||
|
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];
|
byte[] buf = new byte[8192];
|
||||||
int len;
|
int len;
|
||||||
long total = 0;
|
|
||||||
while ((len = in.read(buf)) > 0) {
|
while ((len = in.read(buf)) > 0) {
|
||||||
out.write(buf, 0, len);
|
out.write(buf, 0, len);
|
||||||
total += len;
|
downloaded += len;
|
||||||
System.out.print("\rСкачано: " + (total/1024/1024) + " MB");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log("JAR скачан: " + Files.size(jarFile) + " bytes");
|
|
||||||
log("Обновлено до v" + newVersion);
|
// Проверяем хеш
|
||||||
} else {
|
String actualHash = calculateFileHash(outPath);
|
||||||
throw new IOException("Сервер вернул код: " + conn.getResponseCode());
|
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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user