minor fixes
This commit is contained in:
@@ -69,7 +69,7 @@ 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/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) {
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ public class LauncherAPI {
|
|||||||
}
|
}
|
||||||
idx = end;
|
idx = end;
|
||||||
}
|
}
|
||||||
versions.sort((a, b) -> b.compareTo(a));
|
versions.sort(LauncherAPI::compareVersions);
|
||||||
break;
|
break;
|
||||||
case "neoforge":
|
case "neoforge":
|
||||||
String neoforgeXml = ZHttpClient.downloadString("https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml");
|
String neoforgeXml = ZHttpClient.downloadString("https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml");
|
||||||
@@ -129,7 +129,7 @@ public class LauncherAPI {
|
|||||||
}
|
}
|
||||||
neoidx = end;
|
neoidx = end;
|
||||||
}
|
}
|
||||||
versions.sort((a, b) -> b.compareTo(a));
|
versions.sort(LauncherAPI::compareVersions);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -141,6 +141,23 @@ public class LauncherAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int compareVersions(String a, String b) {
|
||||||
|
String[] partsA = a.split("\\.");
|
||||||
|
String[] partsB = b.split("\\.");
|
||||||
|
int len = Math.min(partsA.length, partsB.length);
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
try {
|
||||||
|
int numA = Integer.parseInt(partsA[i]);
|
||||||
|
int numB = Integer.parseInt(partsB[i]);
|
||||||
|
if (numA != numB) return Integer.compare(numB, numA);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
int cmp = partsA[i].compareTo(partsB[i]);
|
||||||
|
if (cmp != 0) return cmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Integer.compare(partsB.length, partsA.length);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isNeoForgeCompatible(String version, String mcVersion) {
|
private boolean isNeoForgeCompatible(String version, String mcVersion) {
|
||||||
if (mcVersion.startsWith("1.21")) {
|
if (mcVersion.startsWith("1.21")) {
|
||||||
return version.contains("1.21") && !version.contains("1.20");
|
return version.contains("1.21") && !version.contains("1.20");
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ public class AuthService {
|
|||||||
|
|
||||||
public ApiResponse<LoginResult> register(String username, String password) {
|
public ApiResponse<LoginResult> register(String username, String password) {
|
||||||
try {
|
try {
|
||||||
String response = post("/auth/register",
|
JsonObject json = new JsonObject();
|
||||||
"{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}");
|
json.addProperty("username", username);
|
||||||
|
json.addProperty("password", password);
|
||||||
|
String response = post("/auth/register", json.toString());
|
||||||
|
|
||||||
// If registration succeeds, auto-login
|
// If registration succeeds, auto-login
|
||||||
AuthManager.AuthResult result = AuthManager.login(username, password);
|
AuthManager.AuthResult result = AuthManager.login(username, password);
|
||||||
@@ -74,8 +76,9 @@ public class AuthService {
|
|||||||
|
|
||||||
public ApiResponse<Boolean> activatePass(String passCode) {
|
public ApiResponse<Boolean> activatePass(String passCode) {
|
||||||
try {
|
try {
|
||||||
String response = post("/auth/pass/activate",
|
JsonObject json = new JsonObject();
|
||||||
"{\"pass_code\":\"" + passCode + "\"}");
|
json.addProperty("pass_code", passCode);
|
||||||
|
String response = post("/auth/pass/activate", json.toString());
|
||||||
AuthManager.refreshUserInfo();
|
AuthManager.refreshUserInfo();
|
||||||
return ApiResponse.success(true);
|
return ApiResponse.success(true);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
+1
-1
@@ -186,7 +186,7 @@ public class LaunchService {
|
|||||||
}
|
}
|
||||||
String args = Config.getExtraJvmArgs();
|
String args = Config.getExtraJvmArgs();
|
||||||
if (args != null && !args.isEmpty()) {
|
if (args != null && !args.isEmpty()) {
|
||||||
for (String arg : args.split("\\s+")) {
|
for (String arg : args.split("\n")) {
|
||||||
arg = arg.trim();
|
arg = arg.trim();
|
||||||
if (!arg.isEmpty()) extraArgs.add(arg);
|
if (!arg.isEmpty()) extraArgs.add(arg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,12 +123,18 @@ public class AuthManager {
|
|||||||
public static void logout() {
|
public static void logout() {
|
||||||
if (session != null && session.refreshToken != null) {
|
if (session != null && session.refreshToken != null) {
|
||||||
try {
|
try {
|
||||||
post("/auth/logout", "{\"refresh_token\":\"" + session.refreshToken + "\"}");
|
JsonObject json = new JsonObject();
|
||||||
} catch (Exception ignored) {}
|
json.addProperty("refresh_token", session.refreshToken);
|
||||||
|
post("/auth/logout", json.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
LauncherLogger.warn("Logout error: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
session = null;
|
session = null;
|
||||||
userInfo = null;
|
userInfo = null;
|
||||||
try { Files.deleteIfExists(AUTH_FILE); } catch (Exception ignored) {}
|
try { Files.deleteIfExists(AUTH_FILE); } catch (Exception e) {
|
||||||
|
LauncherLogger.warn("Failed to delete auth.json: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isLoggedIn() {
|
public static boolean isLoggedIn() {
|
||||||
@@ -140,23 +146,28 @@ public class AuthManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getUsername() {
|
public static String getUsername() {
|
||||||
return session != null ? session.username : "Player";
|
AuthSession localSession = session;
|
||||||
|
return localSession != null ? localSession.username : "Player";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getUuid() {
|
public static String getUuid() {
|
||||||
return session != null ? session.uuid : "00000000-0000-0000-0000-000000000000";
|
AuthSession localSession = session;
|
||||||
|
return localSession != null ? localSession.uuid : "00000000-0000-0000-0000-000000000000";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getAccessToken() {
|
public static String getAccessToken() {
|
||||||
if (session == null) return "0";
|
AuthSession localSession = session;
|
||||||
|
if (localSession == null) return "0";
|
||||||
if (isAccessTokenExpired()) {
|
if (isAccessTokenExpired()) {
|
||||||
boolean refreshed = tryRefresh();
|
boolean refreshed = tryRefresh();
|
||||||
if (!refreshed) {
|
if (!refreshed) {
|
||||||
if (session == null) return "0";
|
localSession = session;
|
||||||
return session.accessToken != null ? session.accessToken : "0";
|
if (localSession == null) return "0";
|
||||||
|
return localSession.accessToken != null ? localSession.accessToken : "0";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return session != null && session.accessToken != null ? session.accessToken : "0";
|
localSession = session;
|
||||||
|
return localSession != null && localSession.accessToken != null ? localSession.accessToken : "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isAccessTokenExpired() {
|
private static boolean isAccessTokenExpired() {
|
||||||
@@ -174,8 +185,9 @@ public class AuthManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
String body = "{\"refresh_token\":\"" + session.refreshToken + "\"}";
|
JsonObject json = new JsonObject();
|
||||||
SimpleHttpResponse resp = post("/auth/refresh", body);
|
json.addProperty("refresh_token", session.refreshToken);
|
||||||
|
SimpleHttpResponse resp = post("/auth/refresh", json.toString());
|
||||||
|
|
||||||
if (resp.statusCode() == 200) {
|
if (resp.statusCode() == 200) {
|
||||||
AuthSession newSession = GSON.fromJson(resp.body(), AuthSession.class);
|
AuthSession newSession = GSON.fromJson(resp.body(), AuthSession.class);
|
||||||
|
|||||||
+10
-8
@@ -8,6 +8,7 @@ import me.sashegdev.zernmc.launcher.minecraft.launch.LaunchCommandBuilder;
|
|||||||
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
|
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
|
||||||
import me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher;
|
import me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher;
|
||||||
import me.sashegdev.zernmc.launcher.utils.ConsoleUtils;
|
import me.sashegdev.zernmc.launcher.utils.ConsoleUtils;
|
||||||
|
import me.sashegdev.zernmc.launcher.utils.LauncherLogger;
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
@@ -160,14 +161,15 @@ public class MinecraftLib {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void safeDeleteDirectory(Path dir) {
|
private void safeDeleteDirectory(Path dir) {
|
||||||
try {
|
try (var stream = Files.walk(dir)) {
|
||||||
Files.walk(dir)
|
stream.sorted((a, b) -> b.compareTo(a))
|
||||||
.sorted((a, b) -> b.compareTo(a))
|
.forEach(p -> {
|
||||||
.forEach(p -> {
|
try { Files.deleteIfExists(p); }
|
||||||
try { Files.deleteIfExists(p); }
|
catch (IOException e) { /* ignore */ }
|
||||||
catch (IOException ignored) {}
|
});
|
||||||
});
|
} catch (IOException e) {
|
||||||
} catch (IOException ignored) {}
|
LauncherLogger.warn("safeDeleteDirectory: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteOldVersionDirs(Path versionsDir, String keepVersion) throws IOException {
|
private void deleteOldVersionDirs(Path versionsDir, String keepVersion) throws IOException {
|
||||||
|
|||||||
+11
-3
@@ -20,6 +20,7 @@ import java.net.http.HttpResponse;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -464,12 +465,19 @@ public class PackDownloader {
|
|||||||
*/
|
*/
|
||||||
private void downloadFile(FileInfo file, Path destination) throws Exception {
|
private void downloadFile(FileInfo file, Path destination) throws Exception {
|
||||||
String url = ZHttpClient.getBaseUrl() + file.getUrl();
|
String url = ZHttpClient.getBaseUrl() + file.getUrl();
|
||||||
|
String accessToken = AuthManager.getAccessToken();
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
||||||
.uri(java.net.URI.create(url))
|
.uri(java.net.URI.create(url))
|
||||||
.GET()
|
.timeout(Duration.ofSeconds(60))
|
||||||
.build();
|
.header("User-Agent", "ZernMC-Launcher/1.0")
|
||||||
|
.GET();
|
||||||
|
|
||||||
|
if (accessToken != null && !accessToken.equals("0")) {
|
||||||
|
builder.header("Authorization", "Bearer " + accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpRequest request = builder.build();
|
||||||
HttpResponse<InputStream> response = httpClient.send(request,
|
HttpResponse<InputStream> response = httpClient.send(request,
|
||||||
HttpResponse.BodyHandlers.ofInputStream());
|
HttpResponse.BodyHandlers.ofInputStream());
|
||||||
|
|
||||||
|
|||||||
+23
-15
@@ -11,7 +11,9 @@ import java.net.http.HttpResponse;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class ForgeInstaller {
|
public class ForgeInstaller {
|
||||||
@@ -240,29 +242,35 @@ public class ForgeInstaller {
|
|||||||
System.out.println(ZAnsi.cyan("Checking and downloading missing libraries..."));
|
System.out.println(ZAnsi.cyan("Checking and downloading missing libraries..."));
|
||||||
|
|
||||||
// List of problematic libraries and their alternate URLs
|
// List of problematic libraries and their alternate URLs
|
||||||
Map<String, String> alternativeUrls = new HashMap<>();
|
|
||||||
alternativeUrls.put("org/ow2/asm/asm/9.6/asm-9.6.jar",
|
|
||||||
"https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar");
|
|
||||||
alternativeUrls.put("org/ow2/asm/asm/9.6/asm-9.6.jar",
|
|
||||||
"https://mirrors.huaweicloud.com/repository/maven/org/ow2/asm/asm/9.6/asm-9.6.jar");
|
|
||||||
|
|
||||||
Path librariesDir = instance.getPath().resolve("libraries");
|
Path librariesDir = instance.getPath().resolve("libraries");
|
||||||
|
|
||||||
for (Map.Entry<String, String> entry : alternativeUrls.entrySet()) {
|
// Map from maven path to list of mirror URLs (tried in order)
|
||||||
|
Map<String, List<String>> alternativeUrls = new HashMap<>();
|
||||||
|
alternativeUrls.put("org/ow2/asm/asm/9.6/asm-9.6.jar", Arrays.asList(
|
||||||
|
"https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar",
|
||||||
|
"https://mirrors.huaweicloud.com/repository/maven/org/ow2/asm/asm/9.6/asm-9.6.jar"
|
||||||
|
));
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<String>> entry : alternativeUrls.entrySet()) {
|
||||||
Path target = librariesDir.resolve(entry.getKey());
|
Path target = librariesDir.resolve(entry.getKey());
|
||||||
if (!Files.exists(target)) {
|
if (!Files.exists(target)) {
|
||||||
Files.createDirectories(target.getParent());
|
Files.createDirectories(target.getParent());
|
||||||
System.out.println(ZAnsi.yellow("Downloading: " + target.getFileName()));
|
System.out.println(ZAnsi.yellow("Downloading: " + target.getFileName()));
|
||||||
|
|
||||||
for (int attempt = 1; attempt <= 3; attempt++) {
|
boolean downloaded = false;
|
||||||
try {
|
for (String mirrorUrl : entry.getValue()) {
|
||||||
downloadFileWithProgress(entry.getValue(), target);
|
for (int attempt = 1; attempt <= 3; attempt++) {
|
||||||
break;
|
try {
|
||||||
} catch (Exception e) {
|
downloadFileWithProgress(mirrorUrl, target);
|
||||||
if (attempt == 3) throw e;
|
downloaded = true;
|
||||||
System.out.println(ZAnsi.yellow("Повторная попытка " + attempt + "/3..."));
|
break;
|
||||||
Thread.sleep(2000);
|
} catch (Exception e) {
|
||||||
|
if (attempt == 3 && mirrorUrl.equals(entry.getValue().get(entry.getValue().size() - 1))) throw e;
|
||||||
|
System.out.println(ZAnsi.yellow("Retry " + attempt + "/3..."));
|
||||||
|
try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (downloaded) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -183,6 +183,7 @@ public class VersionInstaller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||||
|
executor.shutdown();
|
||||||
|
|
||||||
ProgressBar.finish("Assets downloaded (" + success[0] + " ok, " + failed[0] + " skipped)");
|
ProgressBar.finish("Assets downloaded (" + success[0] + " ok, " + failed[0] + " skipped)");
|
||||||
|
|
||||||
|
|||||||
@@ -41,12 +41,24 @@ public class Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
maxMemory = Integer.parseInt(props.getProperty("maxMemory", "4096"));
|
try {
|
||||||
|
maxMemory = Integer.parseInt(props.getProperty("maxMemory", "4096"));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
System.err.println(ZAnsi.yellow("Config: invalid maxMemory value, using default"));
|
||||||
|
}
|
||||||
ramManuallySet = Boolean.parseBoolean(props.getProperty("ramManuallySet", "false"));
|
ramManuallySet = Boolean.parseBoolean(props.getProperty("ramManuallySet", "false"));
|
||||||
serverUrl = props.getProperty("serverUrl", serverUrl);
|
serverUrl = props.getProperty("serverUrl", serverUrl);
|
||||||
lastUsername = props.getProperty("lastUsername", lastUsername);
|
lastUsername = props.getProperty("lastUsername", lastUsername);
|
||||||
windowWidth = Integer.parseInt(props.getProperty("windowWidth", "1280"));
|
try {
|
||||||
windowHeight = Integer.parseInt(props.getProperty("windowHeight", "720"));
|
windowWidth = Integer.parseInt(props.getProperty("windowWidth", "1280"));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
System.err.println(ZAnsi.yellow("Config: invalid windowWidth value, using default"));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
windowHeight = Integer.parseInt(props.getProperty("windowHeight", "720"));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
System.err.println(ZAnsi.yellow("Config: invalid windowHeight value, using default"));
|
||||||
|
}
|
||||||
extraJvmArgs = props.getProperty("extraJvmArgs", "");
|
extraJvmArgs = props.getProperty("extraJvmArgs", "");
|
||||||
javaPath = props.getProperty("javaPath", "java");
|
javaPath = props.getProperty("javaPath", "java");
|
||||||
locale = props.getProperty("locale", "en");
|
locale = props.getProperty("locale", "en");
|
||||||
@@ -235,7 +247,6 @@ public class Config {
|
|||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("-XX:ParallelGCThreads=").append(Math.max(1, cores));
|
sb.append("-XX:ParallelGCThreads=").append(Math.max(1, cores));
|
||||||
sb.append(" -XX:ConcGCThreads=").append(Math.max(1, cores / 2));
|
sb.append(" -XX:ConcGCThreads=").append(Math.max(1, cores / 2));
|
||||||
sb.append(" -XX:+UseContainerSupport");
|
|
||||||
sb.append(" -XX:+AlwaysPreTouch");
|
sb.append(" -XX:+AlwaysPreTouch");
|
||||||
if (ramMB >= 8192) {
|
if (ramMB >= 8192) {
|
||||||
sb.append(" -XX:+UseZGC");
|
sb.append(" -XX:+UseZGC");
|
||||||
|
|||||||
+3
-10
@@ -60,8 +60,8 @@ async def list_users(
|
|||||||
query += " FROM users"
|
query += " FROM users"
|
||||||
|
|
||||||
if search:
|
if search:
|
||||||
query += " AND (username LIKE ? OR email LIKE ?)"
|
query += " AND username LIKE ?"
|
||||||
params.extend([f"%{search}%", f"%{search}%"])
|
params.append(f"%{search}%")
|
||||||
|
|
||||||
query += " ORDER BY role DESC, username"
|
query += " ORDER BY role DESC, username"
|
||||||
|
|
||||||
@@ -108,19 +108,13 @@ async def get_user_detail(
|
|||||||
"""Детальная информация о пользователе"""
|
"""Детальная информация о пользователе"""
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
row = conn.execute("""
|
row = conn.execute("""
|
||||||
SELECT id, username, email, uuid, role, created_at, last_login, is_active, banned_until
|
SELECT id, username, uuid, role, created_at, last_login, is_active, banned_until
|
||||||
FROM users WHERE id = ?
|
FROM users WHERE id = ?
|
||||||
""", (user_id,)).fetchone()
|
""", (user_id,)).fetchone()
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
raise HTTPException(404, "Пользователь не найден")
|
raise HTTPException(404, "Пользователь не найден")
|
||||||
|
|
||||||
# Модераторы не видят email обычных пользователей
|
|
||||||
if current_user["role"] < ROLE_ELDER and row["role"] < ROLE_MODERATOR:
|
|
||||||
email = None
|
|
||||||
else:
|
|
||||||
email = row["email"]
|
|
||||||
|
|
||||||
# Получаем активную проходку
|
# Получаем активную проходку
|
||||||
pass_info = None
|
pass_info = None
|
||||||
if row["role"] >= ROLE_PASS_HOLDER or current_user["role"] >= ROLE_ELDER:
|
if row["role"] >= ROLE_PASS_HOLDER or current_user["role"] >= ROLE_ELDER:
|
||||||
@@ -151,7 +145,6 @@ async def get_user_detail(
|
|||||||
return {
|
return {
|
||||||
"id": row["id"],
|
"id": row["id"],
|
||||||
"username": row["username"],
|
"username": row["username"],
|
||||||
"email": email,
|
|
||||||
"uuid": row["uuid"],
|
"uuid": row["uuid"],
|
||||||
"role": row["role"],
|
"role": row["role"],
|
||||||
"role_name": ROLE_NAMES.get(row["role"], "Неизвестно"),
|
"role_name": ROLE_NAMES.get(row["role"], "Неизвестно"),
|
||||||
|
|||||||
+2
-2
@@ -135,7 +135,7 @@ async def list_friends(current_user: dict = Depends(get_current_user)):
|
|||||||
"role": row[2],
|
"role": row[2],
|
||||||
"online": bool(row[3]),
|
"online": bool(row[3]),
|
||||||
"current_pack": row[4],
|
"current_pack": row[4],
|
||||||
"last_seen": row[5].isoformat() if row[5] else None
|
"last_seen": row[5] if row[5] else None
|
||||||
})
|
})
|
||||||
|
|
||||||
return {"friends": friends}
|
return {"friends": friends}
|
||||||
@@ -155,7 +155,7 @@ async def list_friend_requests(current_user: dict = Depends(get_current_user)):
|
|||||||
"id": row[0],
|
"id": row[0],
|
||||||
"username": row[1],
|
"username": row[1],
|
||||||
"role": row[2],
|
"role": row[2],
|
||||||
"created_at": row[3].isoformat() if row[3] else None
|
"created_at": row[3] if row[3] else None
|
||||||
})
|
})
|
||||||
return {"requests": requests}
|
return {"requests": requests}
|
||||||
|
|
||||||
|
|||||||
+9
-32
@@ -647,18 +647,12 @@ class CacheControlMiddleware:
|
|||||||
await self.app(scope, receive, send)
|
await self.app(scope, receive, send)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Add caching headers for static files
|
|
||||||
async def send_wrapper(status, headers, *args, **kwargs):
|
async def send_wrapper(status, headers, *args, **kwargs):
|
||||||
# Add cache headers for static files
|
cache_headers = [(b"cache-control", b"public, max-age=86400")]
|
||||||
cache_headers = [
|
|
||||||
(b"cache-control", b"public, max-age=86400"), # 24 hours
|
|
||||||
(b"etag", b'"file-etag"'),
|
|
||||||
]
|
|
||||||
headers = list(headers) + cache_headers
|
headers = list(headers) + cache_headers
|
||||||
await send(status, headers, *args, **kwargs)
|
await send(status, headers, *args, **kwargs)
|
||||||
|
|
||||||
# Use original send
|
await self.app(scope, receive, send_wrapper)
|
||||||
await self.app(scope, receive, send)
|
|
||||||
|
|
||||||
|
|
||||||
app.add_middleware(CacheControlMiddleware)
|
app.add_middleware(CacheControlMiddleware)
|
||||||
@@ -962,7 +956,7 @@ async def get_pack_diff(
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/pack/{pack_name}")
|
@app.get("/pack/{pack_name}")
|
||||||
async def get_pack_manifest(pack_name: str, request: Request):
|
async def get_pack_manifest(pack_name: str, request: Request, current_user: dict = Depends(get_current_user)):
|
||||||
"""Get pack manifest with caching"""
|
"""Get pack manifest with caching"""
|
||||||
client_ip = request.client.host if request.client else "unknown"
|
client_ip = request.client.host if request.client else "unknown"
|
||||||
|
|
||||||
@@ -1009,7 +1003,12 @@ async def get_pack_file(pack_name: str, file_path: str, request: Request):
|
|||||||
client_ip = request.client.host if request.client else None
|
client_ip = request.client.host if request.client else None
|
||||||
|
|
||||||
# Security: prevent path traversal
|
# Security: prevent path traversal
|
||||||
if ".." in file_path:
|
try:
|
||||||
|
full_path = full_path.resolve()
|
||||||
|
pack_root = (PACKS_DIR / pack_name).resolve()
|
||||||
|
if not str(full_path).startswith(str(pack_root)):
|
||||||
|
raise HTTPException(403, "Invalid file path")
|
||||||
|
except (ValueError, OSError):
|
||||||
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():
|
||||||
@@ -1461,28 +1460,6 @@ async def download_legacy_launcher():
|
|||||||
raise HTTPException(404, "No legacy launcher files available")
|
raise HTTPException(404, "No legacy launcher files available")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/launcher/download/zip/{filename}")
|
|
||||||
async def download_launcher_zip(filename: str):
|
|
||||||
"""Download specific launcher ZIP archive"""
|
|
||||||
if ".." in filename:
|
|
||||||
raise HTTPException(400, "Invalid filename")
|
|
||||||
|
|
||||||
valid_patterns = ["ZernMCLauncher-", "ZernMC-win-"]
|
|
||||||
if not any(filename.startswith(p) for p in valid_patterns) or not filename.endswith(".zip"):
|
|
||||||
raise HTTPException(400, "Invalid filename")
|
|
||||||
|
|
||||||
file_path = BUILDS_DIR / filename
|
|
||||||
|
|
||||||
if not file_path.exists():
|
|
||||||
raise HTTPException(404, "ZIP file not found")
|
|
||||||
|
|
||||||
return FileResponse(
|
|
||||||
path=file_path,
|
|
||||||
filename=filename,
|
|
||||||
media_type="application/zip"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ====================== ЛАУНЧЕР МЕТА ЭНДПОИНТЫ ======================
|
# ====================== ЛАУНЧЕР МЕТА ЭНДПОИНТЫ ======================
|
||||||
|
|
||||||
@app.get("/launcher/meta")
|
@app.get("/launcher/meta")
|
||||||
|
|||||||
+12
-12
@@ -5,6 +5,8 @@ from pathlib import Path
|
|||||||
import json
|
import json
|
||||||
from typing import Optional, Dict
|
from typing import Optional, Dict
|
||||||
import structlog
|
import structlog
|
||||||
|
import asyncio
|
||||||
|
import aiofiles
|
||||||
|
|
||||||
from models import PackMeta, FileEntry
|
from models import PackMeta, FileEntry
|
||||||
|
|
||||||
@@ -33,9 +35,9 @@ def calculate_sha256_sync(file_path: Path) -> str:
|
|||||||
return hash_sha.hexdigest()
|
return hash_sha.hexdigest()
|
||||||
|
|
||||||
async def calculate_sha256(file_path: Path) -> str:
|
async def calculate_sha256(file_path: Path) -> str:
|
||||||
"""Calculate SHA256 hash of a file (async wrapper)"""
|
"""Calculate SHA256 hash of a file (async)"""
|
||||||
# Используем синхронную версию для простоты
|
loop = asyncio.get_running_loop()
|
||||||
return calculate_sha256_sync(file_path)
|
return await loop.run_in_executor(None, calculate_sha256_sync, file_path)
|
||||||
|
|
||||||
async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
|
async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
|
||||||
"""Scan pack directory and update manifest if needed"""
|
"""Scan pack directory and update manifest if needed"""
|
||||||
@@ -51,11 +53,11 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
|
|||||||
if not force_rescan and pack_name in _manifest_cache:
|
if not force_rescan and pack_name in _manifest_cache:
|
||||||
return _manifest_cache[pack_name]
|
return _manifest_cache[pack_name]
|
||||||
|
|
||||||
# Load existing meta if available (синхронно)
|
# Load existing meta if available
|
||||||
if meta_path.exists():
|
if meta_path.exists():
|
||||||
try:
|
try:
|
||||||
with open(meta_path, 'r', encoding='utf-8') as f:
|
async with aiofiles.open(meta_path, 'r', encoding='utf-8') as f:
|
||||||
data = json.load(f)
|
data = json.loads(await f.read())
|
||||||
current_meta = PackMeta.model_validate(data)
|
current_meta = PackMeta.model_validate(data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to load existing meta for pack {pack_name}: {e}")
|
logger.warning(f"Failed to load existing meta for pack {pack_name}: {e}")
|
||||||
@@ -114,9 +116,8 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
|
|||||||
pack_config_path = pack_path / "instance.json"
|
pack_config_path = pack_path / "instance.json"
|
||||||
if pack_config_path.exists():
|
if pack_config_path.exists():
|
||||||
try:
|
try:
|
||||||
# Синхронное чтение конфига
|
async with aiofiles.open(pack_config_path, 'r', encoding='utf-8') as f:
|
||||||
with open(pack_config_path, 'r', encoding='utf-8') as f:
|
config = json.loads(await f.read())
|
||||||
config = json.load(f)
|
|
||||||
minecraft_version = config.get("minecraftVersion", minecraft_version)
|
minecraft_version = config.get("minecraftVersion", minecraft_version)
|
||||||
loader_type = config.get("loaderType", loader_type)
|
loader_type = config.get("loaderType", loader_type)
|
||||||
loader_version = config.get("loaderVersion")
|
loader_version = config.get("loaderVersion")
|
||||||
@@ -137,9 +138,8 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
|
|||||||
asset_index=asset_index
|
asset_index=asset_index
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save to disk (синхронно)
|
async with aiofiles.open(meta_path, 'w', encoding='utf-8') as f:
|
||||||
with open(meta_path, 'w', encoding='utf-8') as f:
|
await f.write(new_meta.model_dump_json(indent=2))
|
||||||
f.write(new_meta.model_dump_json(indent=2))
|
|
||||||
|
|
||||||
# Update cache
|
# Update cache
|
||||||
_manifest_cache[pack_name] = new_meta
|
_manifest_cache[pack_name] = new_meta
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ async def sync_playtime(
|
|||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(
|
||||||
"SELECT id, minutes FROM playtime WHERE user_id = ? AND pack_name = ?",
|
"SELECT id, minutes FROM playtime WHERE user_id = ? AND pack_name = ?",
|
||||||
(current_user["user_id"], req.pack_name)
|
(current_user["id"], req.pack_name)
|
||||||
)
|
)
|
||||||
existing = cursor.fetchone()
|
existing = cursor.fetchone()
|
||||||
if existing:
|
if existing:
|
||||||
|
|||||||
Reference in New Issue
Block a user