156 lines
6.2 KiB
Java
156 lines
6.2 KiB
Java
/*
|
|
* Decompiled with CFR 0.152.
|
|
*
|
|
* Could not load the following classes:
|
|
* com.google.gson.JsonElement
|
|
* com.mojang.logging.LogUtils
|
|
* org.apache.commons.io.input.CountingInputStream
|
|
* org.jspecify.annotations.Nullable
|
|
* org.slf4j.Logger
|
|
*/
|
|
package com.mojang.realmsclient.client;
|
|
|
|
import com.google.gson.JsonElement;
|
|
import com.mojang.logging.LogUtils;
|
|
import com.mojang.realmsclient.client.UploadStatus;
|
|
import com.mojang.realmsclient.dto.UploadInfo;
|
|
import com.mojang.realmsclient.gui.screens.UploadResult;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.net.http.HttpClient;
|
|
import java.net.http.HttpRequest;
|
|
import java.net.http.HttpResponse;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.time.Duration;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.function.Supplier;
|
|
import net.minecraft.client.User;
|
|
import net.minecraft.util.LenientJsonParser;
|
|
import net.minecraft.util.Util;
|
|
import org.apache.commons.io.input.CountingInputStream;
|
|
import org.jspecify.annotations.Nullable;
|
|
import org.slf4j.Logger;
|
|
|
|
public class FileUpload
|
|
implements AutoCloseable {
|
|
private static final Logger LOGGER = LogUtils.getLogger();
|
|
private static final int MAX_RETRIES = 5;
|
|
private static final String UPLOAD_PATH = "/upload";
|
|
private final File file;
|
|
private final long realmId;
|
|
private final int slotId;
|
|
private final UploadInfo uploadInfo;
|
|
private final String sessionId;
|
|
private final String username;
|
|
private final String clientVersion;
|
|
private final String worldVersion;
|
|
private final UploadStatus uploadStatus;
|
|
private final HttpClient client;
|
|
|
|
public FileUpload(File file, long realmId, int slotId, UploadInfo uploadInfo, User user, String clientVersion, String worldVersion, UploadStatus uploadStatus) {
|
|
this.file = file;
|
|
this.realmId = realmId;
|
|
this.slotId = slotId;
|
|
this.uploadInfo = uploadInfo;
|
|
this.sessionId = user.getSessionId();
|
|
this.username = user.getName();
|
|
this.clientVersion = clientVersion;
|
|
this.worldVersion = worldVersion;
|
|
this.uploadStatus = uploadStatus;
|
|
this.client = HttpClient.newBuilder().executor(Util.nonCriticalIoPool()).connectTimeout(Duration.ofSeconds(15L)).build();
|
|
}
|
|
|
|
@Override
|
|
public void close() {
|
|
this.client.close();
|
|
}
|
|
|
|
public CompletableFuture<UploadResult> startUpload() {
|
|
long fileSize = this.file.length();
|
|
this.uploadStatus.setTotalBytes(fileSize);
|
|
return this.requestUpload(0, fileSize);
|
|
}
|
|
|
|
private CompletableFuture<UploadResult> requestUpload(int currentAttempt, long fileSize) {
|
|
HttpRequest.BodyPublisher publisher = FileUpload.inputStreamPublisherWithSize(() -> {
|
|
try {
|
|
return new UploadCountingInputStream(new FileInputStream(this.file), this.uploadStatus);
|
|
}
|
|
catch (IOException e) {
|
|
LOGGER.warn("Failed to open file {}", (Object)this.file, (Object)e);
|
|
return null;
|
|
}
|
|
}, fileSize);
|
|
HttpRequest request = HttpRequest.newBuilder(this.uploadInfo.uploadEndpoint().resolve("/upload/" + this.realmId + "/" + this.slotId)).timeout(Duration.ofMinutes(10L)).setHeader("Cookie", this.uploadCookie()).setHeader("Content-Type", "application/octet-stream").POST(publisher).build();
|
|
return this.client.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)).thenCompose(response -> {
|
|
long retryDelaySeconds = this.getRetryDelaySeconds((HttpResponse<?>)response);
|
|
if (this.shouldRetry(retryDelaySeconds, currentAttempt)) {
|
|
this.uploadStatus.restart();
|
|
try {
|
|
Thread.sleep(Duration.ofSeconds(retryDelaySeconds));
|
|
}
|
|
catch (InterruptedException interruptedException) {
|
|
// empty catch block
|
|
}
|
|
return this.requestUpload(currentAttempt + 1, fileSize);
|
|
}
|
|
return CompletableFuture.completedFuture(this.handleResponse((HttpResponse<String>)response));
|
|
});
|
|
}
|
|
|
|
private static HttpRequest.BodyPublisher inputStreamPublisherWithSize(Supplier<@Nullable InputStream> inputStreamSupplier, long fileSize) {
|
|
return HttpRequest.BodyPublishers.fromPublisher(HttpRequest.BodyPublishers.ofInputStream(inputStreamSupplier), fileSize);
|
|
}
|
|
|
|
private String uploadCookie() {
|
|
return "sid=" + this.sessionId + ";token=" + this.uploadInfo.token() + ";user=" + this.username + ";version=" + this.clientVersion + ";worldVersion=" + this.worldVersion;
|
|
}
|
|
|
|
private UploadResult handleResponse(HttpResponse<String> response) {
|
|
int statusCode = response.statusCode();
|
|
if (statusCode == 401) {
|
|
LOGGER.debug("Realms server returned 401: {}", response.headers().firstValue("WWW-Authenticate"));
|
|
}
|
|
String errorMessage = null;
|
|
String body = response.body();
|
|
if (body != null && !body.isBlank()) {
|
|
try {
|
|
JsonElement errorMsgElement = LenientJsonParser.parse(body).getAsJsonObject().get("errorMsg");
|
|
if (errorMsgElement != null) {
|
|
errorMessage = errorMsgElement.getAsString();
|
|
}
|
|
}
|
|
catch (Exception e) {
|
|
LOGGER.warn("Failed to parse response {}", (Object)body, (Object)e);
|
|
}
|
|
}
|
|
return new UploadResult(statusCode, errorMessage);
|
|
}
|
|
|
|
private boolean shouldRetry(long retryDelaySeconds, int currentAttempt) {
|
|
return retryDelaySeconds > 0L && currentAttempt + 1 < 5;
|
|
}
|
|
|
|
private long getRetryDelaySeconds(HttpResponse<?> response) {
|
|
return response.headers().firstValueAsLong("Retry-After").orElse(0L);
|
|
}
|
|
|
|
private static class UploadCountingInputStream
|
|
extends CountingInputStream {
|
|
private final UploadStatus uploadStatus;
|
|
|
|
private UploadCountingInputStream(InputStream proxy, UploadStatus uploadStatus) {
|
|
super(proxy);
|
|
this.uploadStatus = uploadStatus;
|
|
}
|
|
|
|
protected void afterRead(int n) throws IOException {
|
|
super.afterRead(n);
|
|
this.uploadStatus.onWrite(this.getByteCount());
|
|
}
|
|
}
|
|
}
|
|
|