Bootstrap: Add incremental update support via meta system

- Get server version from /launcher/meta (new method)
- Scan local files and calculate SHA256 hashes
- POST to /launcher/diff to get what files need update
- Download only changed files via /launcher/file/{version}/{path}
- Delete obsolete files
- Fallback to ZIP/JAR if meta system fails
- Works with legacy method as backup
This commit is contained in:
SashegDev
2026-05-07 18:41:35 +00:00
parent e566703332
commit ce12854e1b
@@ -4,16 +4,16 @@ import java.io.*;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.nio.file.*; import java.nio.file.*;
import java.security.MessageDigest;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.List;
public class Bootstrap { public class Bootstrap {
private static final String VERSION_FILE = "build.version"; private static final String VERSION_FILE = "build.version";
private static final String JAR_NAME = "zernmclauncher.jar"; private static final String JAR_NAME = "zernmclauncher.jar";
private static final String BASE_URL = "http://87.120.187.36:1582"; private static final String BASE_URL = "http://87.120.187.36:1582";
private static final int BUFFER_SIZE = 8192;
private static Path baseDir; private static Path baseDir;
private static Path logDir; private static Path logDir;
@@ -71,7 +71,33 @@ public class Bootstrap {
private static String getServerVersion() { private static String getServerVersion() {
try { try {
URL url = new URL(BASE_URL.replace("download?type=jar", "version")); // Пробуем получить версию из мета-системы
URL url = new URL(BASE_URL + "/launcher/meta");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
if (conn.getResponseCode() == 200) {
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) sb.append(line);
}
String json = sb.toString();
// Ищем первую версию в мета
int vIndex = json.indexOf("\"version\"");
if (vIndex != -1) {
int start = json.indexOf("\"", vIndex + 9) + 1;
int end = json.indexOf("\"", start);
if (start > 0 && end > start) {
return json.substring(start, end);
}
}
}
} catch (Exception ignored) {}
// Fallback на старый метод
try {
URL url = new URL(BASE_URL + "/launcher/version");
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET"); conn.setRequestMethod("GET");
if (conn.getResponseCode() == 200) { if (conn.getResponseCode() == 200) {
@@ -102,6 +128,326 @@ public class Bootstrap {
} }
private static void downloadUpdate(String newVersion) throws Exception { private static void downloadUpdate(String newVersion) throws Exception {
// Пытаемся использовать инкрементное обновление через мета-систему
try {
// 1. Получаем мета с сервера
Map<String, Object> meta = getLauncherMeta();
@SuppressWarnings("unchecked")
List<Map<String, Object>> files = (List<Map<String, Object>>) meta.get("files");
if (files == null || files.isEmpty()) {
// Мета пустая - fallback на старый метод
log("Мета недоступна, используем старый метод");
downloadUpdateLegacy(newVersion);
return;
}
log("Мета получена: " + files.size() + " файлов");
// 2. Сканируем локальные файлы
Map<String, String> localFiles = scanLocalFiles();
log("Локально: " + localFiles.size() + " файлов");
// 3. Получаем diff
Map<String, Object> diff = getLauncherDiff(localFiles);
@SuppressWarnings("unchecked")
List<Map<String, Object>> toDownload = (List<Map<String, Object>>) diff.get("to_download");
@SuppressWarnings("unchecked")
List<String> toDelete = (List<String>) diff.get("to_delete");
log("К скачиванию: " + toDownload.size() + ", к удалению: " + toDelete.size());
// 4. Удаляем лишние файлы
for (String filePath : toDelete) {
Path f = baseDir.resolve(filePath);
if (Files.exists(f)) {
Files.delete(f);
log("Удален: " + filePath);
}
}
// 5. Скачиваем новые/измененные файлы
String serverVersion = (String) diff.get("version");
for (Map<String, Object> file : toDownload) {
String path = (String) file.get("path");
downloadLauncherFile(serverVersion, path);
log("Скачан: " + path);
}
// 6. Записываем новую версию
Files.writeString(baseDir.resolve(VERSION_FILE), serverVersion);
log("Обновлено до v" + serverVersion);
} catch (Exception e) {
log("Ошибка инкрементного обновления: " + e.getMessage());
log("Fallback на старый метод...");
// Fallback на старый метод
downloadUpdateLegacy(newVersion);
}
}
private static Map<String, Object> getLauncherMeta() throws Exception {
URL url = new URL(BASE_URL + "/launcher/meta");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
if (conn.getResponseCode() != 200) {
throw new IOException("Не удалось получить мета");
}
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) sb.append(line);
}
// Парсим JSON вручную
String json = sb.toString();
int versionsStart = json.indexOf("\"versions\"");
if (versionsStart == -1) throw new IOException("Нет versions в ответе");
int arrayStart = json.indexOf("[", versionsStart);
int arrayEnd = json.indexOf("]", arrayStart);
String versionsArray = json.substring(arrayStart + 1, arrayEnd);
// Ищем первую версию
int metaStart = versionsArray.indexOf("\"meta\"");
if (metaStart == -1) throw new IOException("Нет meta в версии");
int objStart = versionsArray.indexOf("{", metaStart);
int objEnd = versionsArray.lastIndexOf("}");
String metaJson = versionsArray.substring(objStart, objEnd + 1);
return parseMetaJson(metaJson);
}
@SuppressWarnings("unchecked")
private static Map<String, Object> parseMetaJson(String json) {
Map<String, Object> result = new HashMap<>();
List<Map<String, Object>> files = new ArrayList<>();
// Простой парсинг
String[] lines = json.split(",");
for (String line : lines) {
line = line.trim();
if (line.contains("\"version\"")) {
String v = line.split(":")[1].replaceAll("[\"\\s]", "");
result.put("version", v);
} else if (line.contains("\"files\"")) {
// Файлы парсим отдельно
} else if (line.contains("\"path\"") && line.contains("\"size\"") && line.contains("\"hash\"")) {
Map<String, Object> file = new HashMap<>();
String[] parts = line.split(":");
for (String part : parts) {
if (part.contains("path")) file.put("path", extractJsonValue(part));
else if (part.contains("size")) file.put("size", Long.parseLong(extractJsonValue(part)));
else if (part.contains("hash")) file.put("hash", extractJsonValue(part));
}
if (!file.isEmpty()) files.add(file);
}
}
result.put("files", files);
return result;
}
private static String extractJsonValue(String part) {
int idx = part.indexOf(":");
if (idx == -1) return "";
String val = part.substring(idx + 1).trim();
if (val.startsWith("\"")) val = val.substring(1);
if (val.endsWith("\"")) val = val.substring(0, val.length() - 1);
return val;
}
private static Map<String, String> scanLocalFiles() throws Exception {
Map<String, String> hashes = new HashMap<>();
// Сканируем основную директорию
try (DirectoryStream<Path> stream = Files.newDirectoryStream(baseDir)) {
for (Path p : stream) {
if (p.getFileName().toString().equals("build.version")) continue;
if (p.getFileName().toString().equals("logs")) continue;
if (p.getFileName().toString().equals("data")) continue;
if (Files.isRegularFile(p)) {
hashes.put(p.getFileName().toString(), calculateSHA256(p));
} else if (Files.isDirectory(p)) {
scanDirectory(p, p.getFileName().toString(), hashes);
}
}
}
return hashes;
}
private static void scanDirectory(Path dir, String basePath, Map<String, String> hashes) throws Exception {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
for (Path p : stream) {
String relPath = basePath + "/" + p.getFileName().toString();
if (Files.isRegularFile(p)) {
hashes.put(relPath, calculateSHA256(p));
} else if (Files.isDirectory(p)) {
scanDirectory(p, relPath, hashes);
}
}
}
}
private static String calculateSHA256(Path file) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
try (InputStream is = Files.newInputStream(file)) {
byte[] buf = new byte[BUFFER_SIZE];
int len;
while ((len = is.read(buf)) != -1) {
md.update(buf, 0, len);
}
}
StringBuilder sb = new StringBuilder();
for (byte b : md.digest()) sb.append(String.format("%02x", b));
return "sha256:" + sb.toString();
}
private static Map<String, Object> getLauncherDiff(Map<String, String> localFiles) throws Exception {
// Создаем JSON {filename: hash, ...}
StringBuilder json = new StringBuilder("{");
int i = 0;
for (Map.Entry<String, String> e : localFiles.entrySet()) {
if (i > 0) json.append(",");
json.append("\"").append(e.getKey()).append("\":\"").append(e.getValue()).append("\"");
i++;
}
json.append("}");
URL url = new URL(BASE_URL + "/launcher/diff");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/json");
try (OutputStream os = conn.getOutputStream()) {
os.write(json.toString().getBytes("UTF-8"));
}
if (conn.getResponseCode() != 200) {
throw new IOException("Diff запрос вернул: " + conn.getResponseCode());
}
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) sb.append(line);
}
return parseDiffJson(sb.toString());
}
@SuppressWarnings("unchecked")
private static Map<String, Object> parseDiffJson(String json) {
Map<String, Object> result = new HashMap<>();
List<Map<String, Object>> toDownload = new ArrayList<>();
List<String> toDelete = new ArrayList<>();
String[] parts = json.split(",");
for (String part : parts) {
part = part.trim();
if (part.contains("\"version\"")) {
result.put("version", extractJsonValue(part));
} else if (part.contains("\"to_download\"")) {
// Парсим файлы
} else if (part.contains("\"to_delete\"")) {
// Парсим удаляемые
} else if (part.contains("\"path\"") && part.contains("\"size\"")) {
Map<String, Object> file = new HashMap<>();
if (part.contains("\"hash\"")) file.put("hash", "need");
file.put("path", extractJsonValue(part));
String sizePart = part.split("size")[1].split(",")[0];
file.put("size", Long.parseLong(extractJsonValue(sizePart.split(":")[1])));
toDownload.add(file);
} else if (part.startsWith("\"")) {
toDelete.add(extractJsonValue(part));
}
}
result.put("to_download", toDownload);
result.put("to_delete", toDelete);
return result;
}
private static void downloadLauncherFile(String version, String filePath) throws Exception {
String encodedPath = filePath.replace(" ", "%20");
URL url = new URL(BASE_URL + "/launcher/file/" + version + "/" + encodedPath);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
if (conn.getResponseCode() != 200) {
throw new IOException("Не удалось скачать файл: " + filePath);
}
Path targetPath = baseDir.resolve(filePath);
Files.createDirectories(targetPath.getParent());
try (InputStream in = conn.getInputStream();
OutputStream out = new FileOutputStream(targetPath.toFile())) {
byte[] buf = new byte[BUFFER_SIZE];
int len;
long total = 0;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
total += len;
}
}
}
// Fallback на старый метод (скачивание ZIP)
private static void downloadUpdateLegacy(String newVersion) throws Exception {
// Пробуем скачать ZIP
String zipUrl = BASE_URL + "/launcher/download/zip/ZernMC-win-" + newVersion + ".zip";
URL url = new URL(zipUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
if (conn.getResponseCode() == 200) {
Path tempZip = baseDir.resolve("update.zip");
try (InputStream in = conn.getInputStream();
OutputStream out = new FileOutputStream(tempZip.toFile())) {
byte[] buf = new byte[BUFFER_SIZE];
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");
}
}
log("ZIP скачан, распаковка...");
// Распаковываем ZIP
java.util.zip.ZipFile zip = new java.util.zip.ZipFile(tempZip.toFile());
java.util.Enumeration<? extends java.util.zip.ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
java.util.zip.ZipEntry entry = entries.nextElement();
Path outPath = baseDir.resolve(entry.getName());
if (entry.isDirectory()) {
Files.createDirectories(outPath);
} else {
Files.createDirectories(outPath.getParent());
try (InputStream is = zip.getInputStream(entry)) {
Files.copy(is, outPath, StandardCopyOption.REPLACE_EXISTING);
}
}
}
zip.close();
Files.delete(tempZip);
Files.writeString(baseDir.resolve(VERSION_FILE), newVersion);
log("Обновлено до v" + newVersion + " (ZIP метод)");
} else {
// Последний fallback - JAR
downloadUpdateJar(newVersion);
}
}
private static void downloadUpdateJar(String newVersion) throws Exception {
URL url = new URL(BASE_URL + "/launcher/download/jar"); URL url = new URL(BASE_URL + "/launcher/download/jar");
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET"); conn.setRequestMethod("GET");
@@ -112,7 +458,7 @@ public class Bootstrap {
try (InputStream in = conn.getInputStream(); try (InputStream in = conn.getInputStream();
OutputStream out = new FileOutputStream(tmp.toFile())) { OutputStream out = new FileOutputStream(tmp.toFile())) {
byte[] buf = new byte[8192]; byte[] buf = new byte[BUFFER_SIZE];
int len; int len;
long total = 0; long total = 0;
while ((len = in.read(buf)) > 0) { while ((len = in.read(buf)) > 0) {
@@ -121,16 +467,15 @@ public class Bootstrap {
System.out.print("\rСкачано: " + (total/1024/1024) + " MB"); System.out.print("\rСкачано: " + (total/1024/1024) + " MB");
} }
} }
log("Скачано"); log("JAR скачан");
Path backup = jarFile.resolveSibling(JAR_NAME + ".old"); Path backup = jarFile.resolveSibling(JAR_NAME + ".old");
if (Files.exists(jarFile)) Files.move(jarFile, backup, StandardCopyOption.REPLACE_EXISTING); if (Files.exists(jarFile)) Files.move(jarFile, backup, StandardCopyOption.REPLACE_EXISTING);
Files.move(tmp, jarFile, StandardCopyOption.REPLACE_EXISTING); Files.move(tmp, jarFile, StandardCopyOption.REPLACE_EXISTING);
if (Files.exists(backup)) Files.delete(backup); if (Files.exists(backup)) Files.delete(backup);
Files.writeString(baseDir.resolve(VERSION_FILE), newVersion); Files.writeString(baseDir.resolve(VERSION_FILE), newVersion);
log("Обновлено до v" + newVersion); log("Обновлено до v" + newVersion + " (JAR метод)");
} else { } else {
throw new IOException("Сервер вернул код: " + conn.getResponseCode()); throw new IOException("Сервер вернул код: " + conn.getResponseCode());
} }