mirror of
https://github.com/Xiao-MoMi/craft-engine.git
synced 2026-01-04 15:41:38 +00:00
feat(core): 添加 Alist 托管支持
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package net.momirealms.craftengine.bukkit.pack;
|
||||
|
||||
import net.momirealms.craftengine.bukkit.api.event.AsyncResourcePackGenerateEvent;
|
||||
import net.momirealms.craftengine.bukkit.nms.FastNMS;
|
||||
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
|
||||
import net.momirealms.craftengine.bukkit.plugin.command.feature.ReloadCommand;
|
||||
import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer;
|
||||
@@ -153,6 +154,9 @@ public class BukkitPackManager extends AbstractPackManager implements Listener {
|
||||
player.sendPackets(packets, true);
|
||||
}
|
||||
}
|
||||
}).exceptionally(throwable -> {
|
||||
CraftEngine.instance().logger().warn("Failed to send resource pack to player " + player.name(), throwable);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2132,6 +2132,10 @@ public class PacketConsumers {
|
||||
user.nettyChannel().writeAndFlush(newPacket);
|
||||
user.addResourcePackUUID(data.uuid());
|
||||
}
|
||||
}).exceptionally(throwable -> {
|
||||
CraftEngine.instance().logger().warn("Failed to handle ClientboundResourcePackPushPacket", throwable);
|
||||
user.simulatePacket(FastNMS.INSTANCE.constructor$ServerboundResourcePackPacket$SUCCESSFULLY_LOADED(packUUID));
|
||||
return null;
|
||||
});
|
||||
} catch (Exception e) {
|
||||
CraftEngine.instance().logger().warn("Failed to handle ClientboundResourcePackPushPacket", e);
|
||||
|
||||
@@ -17,6 +17,7 @@ public class ResourcePackHosts {
|
||||
public static final Key LOBFILE = Key.of("craftengine:lobfile");
|
||||
public static final Key S3_HOST = Key.of("craftengine:s3_host");
|
||||
public static final Key CUSTOM_API_HOST = Key.of("craftengine:custom_api_host");
|
||||
public static final Key ALIST_HOST = Key.of("craftengine:alist_host");
|
||||
|
||||
static {
|
||||
register(NONE, NoneHost.FACTORY);
|
||||
@@ -25,6 +26,7 @@ public class ResourcePackHosts {
|
||||
register(LOBFILE, LobFileHost.FACTORY);
|
||||
register(S3_HOST, S3Host.FACTORY);
|
||||
register(CUSTOM_API_HOST, CustomApiHost.FACTORY);
|
||||
register(ALIST_HOST, AlistHost.FACTORY);
|
||||
}
|
||||
|
||||
public static void register(Key key, ResourcePackHostFactory factory) {
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
package net.momirealms.craftengine.core.pack.host.impl;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import net.momirealms.craftengine.core.pack.host.ResourcePackDownloadData;
|
||||
import net.momirealms.craftengine.core.pack.host.ResourcePackHost;
|
||||
import net.momirealms.craftengine.core.pack.host.ResourcePackHostFactory;
|
||||
import net.momirealms.craftengine.core.plugin.CraftEngine;
|
||||
import net.momirealms.craftengine.core.util.GsonHelper;
|
||||
import net.momirealms.craftengine.core.util.Pair;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class AlistHost implements ResourcePackHost {
|
||||
public static final Factory FACTORY = new Factory();
|
||||
private final String apiUrl;
|
||||
private final String userName;
|
||||
private final String password;
|
||||
private final String filePassword;
|
||||
private final String otpCode;
|
||||
private final Duration jwtTokenExpiration;
|
||||
private final String filePath;
|
||||
private final Path localFilePath;
|
||||
private Pair<String, Date> jwtToken;
|
||||
private String cacheSha1;
|
||||
|
||||
public AlistHost(String apiUrl,
|
||||
String userName,
|
||||
String password,
|
||||
String filePassword,
|
||||
String otpCode,
|
||||
Duration jwtTokenExpiration,
|
||||
String filePath,
|
||||
String localFilePath) {
|
||||
this.apiUrl = apiUrl;
|
||||
this.userName = userName;
|
||||
this.password = password;
|
||||
this.filePassword = filePassword;
|
||||
this.otpCode = otpCode;
|
||||
this.jwtTokenExpiration = jwtTokenExpiration;
|
||||
this.filePath = filePath;
|
||||
this.localFilePath = localFilePath == null ? null : Path.of(localFilePath);
|
||||
this.readCacheFromDisk();
|
||||
}
|
||||
|
||||
private void readCacheFromDisk() {
|
||||
Path cachePath = CraftEngine.instance().dataFolderPath().resolve("alist.cache");
|
||||
if (!Files.exists(cachePath)) return;
|
||||
try (InputStream is = Files.newInputStream(cachePath)) {
|
||||
cacheSha1 = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
CraftEngine.instance().logger().warn("[Alist] Failed to read cache file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveCacheToDisk() {
|
||||
Path cachePath = CraftEngine.instance().dataFolderPath().resolve("alist.cache");
|
||||
try {
|
||||
Files.writeString(cachePath, cacheSha1);
|
||||
} catch (IOException e) {
|
||||
CraftEngine.instance().logger().warn("[Alist] Failed to write cache file", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<ResourcePackDownloadData>> requestResourcePackDownloadLink(UUID player) {
|
||||
CompletableFuture<List<ResourcePackDownloadData>> future = new CompletableFuture<>();
|
||||
CraftEngine.instance().scheduler().executeAsync(() -> {
|
||||
try (HttpClient client = HttpClient.newHttpClient()) {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(apiUrl + "/api/fs/get"))
|
||||
.header("Authorization", getOrRefreshJwtToken())
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(getRequestResourcePackDownloadLinkPost())
|
||||
.build();
|
||||
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||
.thenAccept(response -> handleResourcePackDownloadLinkResponse(response, future))
|
||||
.exceptionally(ex -> {
|
||||
CraftEngine.instance().logger().severe("[Alist] Failed to request resource pack download link", ex);
|
||||
future.completeExceptionally(ex);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> upload(Path resourcePackPath) {
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
if (this.localFilePath != null) resourcePackPath = this.localFilePath;
|
||||
Path finalResourcePackPath = resourcePackPath;
|
||||
CraftEngine.instance().scheduler().executeAsync(() -> {
|
||||
try (HttpClient client = HttpClient.newHttpClient()) {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(apiUrl + "/api/fs/put"))
|
||||
.header("Authorization", getOrRefreshJwtToken())
|
||||
.header("File-Path", URLEncoder.encode(filePath, StandardCharsets.UTF_8)
|
||||
.replace("/", "%2F"))
|
||||
.header("overwrite", "true")
|
||||
.header("password", filePassword)
|
||||
.header("Content-Type", "application/x-zip-compressed")
|
||||
.PUT(HttpRequest.BodyPublishers.ofFile(finalResourcePackPath))
|
||||
.build();
|
||||
long requestStart = System.currentTimeMillis();
|
||||
CraftEngine.instance().logger().info("[Alist] Starting file upload...");
|
||||
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||
.thenAccept(response -> {
|
||||
long uploadTime = System.currentTimeMillis() - requestStart;
|
||||
if (response.statusCode() == 200) {
|
||||
cacheSha1 = calculateLocalFileSha1(finalResourcePackPath);
|
||||
saveCacheToDisk();
|
||||
CraftEngine.instance().logger().info("[Alist] Upload resource pack success after " + uploadTime + "ms");
|
||||
future.complete(null);
|
||||
} else {
|
||||
future.completeExceptionally(new RuntimeException("Upload failed with status code: " + response.statusCode()));
|
||||
}
|
||||
})
|
||||
.exceptionally(ex -> {
|
||||
long uploadTime = System.currentTimeMillis() - requestStart;
|
||||
CraftEngine.instance().logger().severe(
|
||||
"[Alist] Failed to upload resource pack after " + uploadTime + "ms", ex);
|
||||
future.completeExceptionally(ex);
|
||||
return null;
|
||||
});
|
||||
} catch (IOException e) {
|
||||
CraftEngine.instance().logger().warn("[Alist] Failed to upload resource pack: " + e.getMessage());
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getOrRefreshJwtToken() {
|
||||
if (jwtToken == null || jwtToken.right().before(new Date())) {
|
||||
try (HttpClient client = HttpClient.newHttpClient()) {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(apiUrl + "/api/auth/login"))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(getLoginPost())
|
||||
.build();
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != 200) {
|
||||
CraftEngine.instance().logger().warn("[Alist] Failed to get JWT token: " + response.body());
|
||||
return null;
|
||||
}
|
||||
JsonObject jsonData = parseJson(response.body());
|
||||
JsonElement code = jsonData.get("code");
|
||||
if (code.isJsonPrimitive() && code.getAsJsonPrimitive().isNumber() && code.getAsJsonPrimitive().getAsInt() == 200) {
|
||||
JsonElement data = jsonData.get("data");
|
||||
if (data.isJsonObject()) {
|
||||
JsonObject jsonObj = data.getAsJsonObject();
|
||||
jwtToken = Pair.of(
|
||||
jsonObj.getAsJsonPrimitive("token").getAsString(),
|
||||
new Date(System.currentTimeMillis() + jwtTokenExpiration.toMillis())
|
||||
);
|
||||
return jwtToken.left();
|
||||
}
|
||||
CraftEngine.instance().logger().warn("[Alist] Failed to get JWT token: " + response.body());
|
||||
return null;
|
||||
}
|
||||
CraftEngine.instance().logger().warn("[Alist] Failed to get JWT token: " + response.body());
|
||||
return null;
|
||||
} catch (IOException | InterruptedException e) {
|
||||
CraftEngine.instance().logger().warn("[Alist] Failed to get JWT token", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return jwtToken.left();
|
||||
}
|
||||
|
||||
private HttpRequest.BodyPublisher getLoginPost() {
|
||||
String body = "{\"username\":\"" + userName + "\",\"password\":\"" + password + "\"";
|
||||
if (otpCode != null && !otpCode.isEmpty()) {
|
||||
body += ",\"otp_code\":\"" + otpCode + "\"";
|
||||
}
|
||||
body += "}";
|
||||
return HttpRequest.BodyPublishers.ofString(body);
|
||||
}
|
||||
|
||||
private HttpRequest.BodyPublisher getRequestResourcePackDownloadLinkPost() {
|
||||
String body = "{\"path\":\"" + filePath + "\",\"password\":\"" + filePassword + "\"}";
|
||||
return HttpRequest.BodyPublishers.ofString(body);
|
||||
}
|
||||
|
||||
private void handleResourcePackDownloadLinkResponse(
|
||||
HttpResponse<String> response, CompletableFuture<List<ResourcePackDownloadData>> future) {
|
||||
if (response.statusCode() == 200) {
|
||||
JsonObject json = parseJson(response.body());
|
||||
JsonElement code = json.get("code");
|
||||
if (code.isJsonPrimitive() && code.getAsJsonPrimitive().isNumber() && code.getAsJsonPrimitive().getAsInt() == 200) {
|
||||
JsonElement data = json.get("data");
|
||||
if (data.isJsonObject()) {
|
||||
JsonObject dataObj = data.getAsJsonObject();
|
||||
boolean isDir = dataObj.getAsJsonPrimitive("is_dir").getAsBoolean();
|
||||
if (!isDir) {
|
||||
String url = dataObj.getAsJsonPrimitive("raw_url").getAsString();
|
||||
UUID uuid = UUID.nameUUIDFromBytes(cacheSha1.getBytes(StandardCharsets.UTF_8));
|
||||
future.complete(List.of(new ResourcePackDownloadData(url, uuid, cacheSha1)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
future.completeExceptionally(
|
||||
new RuntimeException("Failed to request resource pack download link: " + response.body()));
|
||||
}
|
||||
|
||||
private JsonObject parseJson(String json) {
|
||||
try {
|
||||
return GsonHelper.get().fromJson(
|
||||
json,
|
||||
JsonObject.class
|
||||
);
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new RuntimeException("Invalid JSON response: " + json, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String calculateLocalFileSha1(Path filePath) {
|
||||
try (InputStream is = Files.newInputStream(filePath)) {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
byte[] buffer = new byte[8192];
|
||||
int len;
|
||||
while ((len = is.read(buffer)) != -1) {
|
||||
md.update(buffer, 0, len);
|
||||
}
|
||||
byte[] digest = md.digest();
|
||||
return HexFormat.of().formatHex(digest);
|
||||
} catch (IOException | NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("Failed to calculate SHA1", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements ResourcePackHostFactory {
|
||||
|
||||
@Override
|
||||
public ResourcePackHost create(Map<String, Object> arguments) {
|
||||
String apiUrl = (String) arguments.get("api-url");
|
||||
if (apiUrl == null || apiUrl.isEmpty()) {
|
||||
throw new IllegalArgumentException("'api-url' cannot be empty for Alist host");
|
||||
}
|
||||
String userName = (String) arguments.get("username");
|
||||
if (userName == null || userName.isEmpty()) {
|
||||
throw new IllegalArgumentException("'username' cannot be empty for Alist host");
|
||||
}
|
||||
String password = (String) arguments.get("password");
|
||||
if (password == null || password.isEmpty()) {
|
||||
throw new IllegalArgumentException("'password' cannot be empty for Alist host");
|
||||
}
|
||||
String filePassword = (String) arguments.getOrDefault("file-password", "");
|
||||
String otpCode = (String) arguments.get("otp-code");
|
||||
Duration jwtTokenExpiration = Duration.ofHours((int) arguments.getOrDefault("jwt-token-expiration", 48));
|
||||
String filePath = (String) arguments.get("file-path");
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
throw new IllegalArgumentException("'file-path' cannot be empty for Alist host");
|
||||
}
|
||||
String localFilePath = (String) arguments.get("local-file-path");
|
||||
return new AlistHost(apiUrl, userName, password, filePassword, otpCode, jwtTokenExpiration, filePath, localFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user