261 lines
		
	
	
	
		
			8 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
			
		
		
	
	
			261 lines
		
	
	
	
		
			8 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
| package com.mojang.realmsclient.client;
 | |
| 
 | |
| import com.google.gson.JsonElement;
 | |
| import com.mojang.logging.LogUtils;
 | |
| import com.mojang.realmsclient.client.worldupload.RealmsUploadCanceledException;
 | |
| import com.mojang.realmsclient.dto.UploadInfo;
 | |
| import com.mojang.realmsclient.gui.screens.UploadResult;
 | |
| import java.io.File;
 | |
| import java.io.FileInputStream;
 | |
| import java.io.FileNotFoundException;
 | |
| import java.io.IOException;
 | |
| import java.io.InputStream;
 | |
| import java.io.OutputStream;
 | |
| import java.time.Duration;
 | |
| import java.util.Optional;
 | |
| import java.util.concurrent.CompletableFuture;
 | |
| import java.util.concurrent.TimeUnit;
 | |
| import java.util.concurrent.atomic.AtomicBoolean;
 | |
| import net.fabricmc.api.EnvType;
 | |
| import net.fabricmc.api.Environment;
 | |
| import net.minecraft.Util;
 | |
| import net.minecraft.client.User;
 | |
| import net.minecraft.util.LenientJsonParser;
 | |
| import org.apache.http.HttpResponse;
 | |
| import org.apache.http.NameValuePair;
 | |
| import org.apache.http.client.config.RequestConfig;
 | |
| import org.apache.http.client.methods.HttpPost;
 | |
| import org.apache.http.entity.InputStreamEntity;
 | |
| import org.apache.http.impl.client.CloseableHttpClient;
 | |
| import org.apache.http.impl.client.HttpClientBuilder;
 | |
| import org.apache.http.util.Args;
 | |
| import org.apache.http.util.EntityUtils;
 | |
| import org.jetbrains.annotations.Nullable;
 | |
| import org.slf4j.Logger;
 | |
| 
 | |
| @Environment(EnvType.CLIENT)
 | |
| public class FileUpload {
 | |
| 	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;
 | |
| 	final AtomicBoolean cancelled = new AtomicBoolean(false);
 | |
| 	@Nullable
 | |
| 	private CompletableFuture<UploadResult> uploadTask;
 | |
| 	private final RequestConfig requestConfig = RequestConfig.custom()
 | |
| 		.setSocketTimeout((int)TimeUnit.MINUTES.toMillis(10L))
 | |
| 		.setConnectTimeout((int)TimeUnit.SECONDS.toMillis(15L))
 | |
| 		.build();
 | |
| 
 | |
| 	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;
 | |
| 	}
 | |
| 
 | |
| 	public UploadResult upload() {
 | |
| 		if (this.uploadTask != null) {
 | |
| 			return new UploadResult.Builder().build();
 | |
| 		} else {
 | |
| 			this.uploadTask = CompletableFuture.supplyAsync(() -> this.requestUpload(0), Util.backgroundExecutor());
 | |
| 			if (this.cancelled.get()) {
 | |
| 				this.cancel();
 | |
| 				return new UploadResult.Builder().build();
 | |
| 			} else {
 | |
| 				return (UploadResult)this.uploadTask.join();
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	public void cancel() {
 | |
| 		this.cancelled.set(true);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * @param retries The number of times this upload has already been attempted
 | |
| 	 */
 | |
| 	private UploadResult requestUpload(int retries) {
 | |
| 		UploadResult.Builder builder = new UploadResult.Builder();
 | |
| 		if (this.cancelled.get()) {
 | |
| 			return builder.build();
 | |
| 		} else {
 | |
| 			this.uploadStatus.setTotalBytes(this.file.length());
 | |
| 			HttpPost httpPost = new HttpPost(this.uploadInfo.getUploadEndpoint().resolve("/upload/" + this.realmId + "/" + this.slotId));
 | |
| 			CloseableHttpClient closeableHttpClient = HttpClientBuilder.create().setDefaultRequestConfig(this.requestConfig).build();
 | |
| 
 | |
| 			UploadResult var8;
 | |
| 			try {
 | |
| 				this.setupRequest(httpPost);
 | |
| 				HttpResponse httpResponse = closeableHttpClient.execute(httpPost);
 | |
| 				long l = this.getRetryDelaySeconds(httpResponse);
 | |
| 				if (!this.shouldRetry(l, retries)) {
 | |
| 					this.handleResponse(httpResponse, builder);
 | |
| 					return builder.build();
 | |
| 				}
 | |
| 
 | |
| 				var8 = this.retryUploadAfter(l, retries);
 | |
| 			} catch (Exception var12) {
 | |
| 				if (!this.cancelled.get()) {
 | |
| 					LOGGER.error("Caught exception while uploading: ", (Throwable)var12);
 | |
| 					return builder.build();
 | |
| 				}
 | |
| 
 | |
| 				throw new RealmsUploadCanceledException();
 | |
| 			} finally {
 | |
| 				this.cleanup(httpPost, closeableHttpClient);
 | |
| 			}
 | |
| 
 | |
| 			return var8;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	private void cleanup(HttpPost post, @Nullable CloseableHttpClient httpClient) {
 | |
| 		post.releaseConnection();
 | |
| 		if (httpClient != null) {
 | |
| 			try {
 | |
| 				httpClient.close();
 | |
| 			} catch (IOException var4) {
 | |
| 				LOGGER.error("Failed to close Realms upload client");
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	private void setupRequest(HttpPost post) throws FileNotFoundException {
 | |
| 		post.setHeader(
 | |
| 			"Cookie",
 | |
| 			"sid="
 | |
| 				+ this.sessionId
 | |
| 				+ ";token="
 | |
| 				+ this.uploadInfo.getToken()
 | |
| 				+ ";user="
 | |
| 				+ this.username
 | |
| 				+ ";version="
 | |
| 				+ this.clientVersion
 | |
| 				+ ";worldVersion="
 | |
| 				+ this.worldVersion
 | |
| 		);
 | |
| 		FileUpload.CustomInputStreamEntity customInputStreamEntity = new FileUpload.CustomInputStreamEntity(
 | |
| 			new FileInputStream(this.file), this.file.length(), this.uploadStatus
 | |
| 		);
 | |
| 		customInputStreamEntity.setContentType("application/octet-stream");
 | |
| 		post.setEntity(customInputStreamEntity);
 | |
| 	}
 | |
| 
 | |
| 	private void handleResponse(HttpResponse response, UploadResult.Builder uploadResult) throws IOException {
 | |
| 		int i = response.getStatusLine().getStatusCode();
 | |
| 		if (i == 401) {
 | |
| 			LOGGER.debug("Realms server returned 401: {}", response.getFirstHeader("WWW-Authenticate"));
 | |
| 		}
 | |
| 
 | |
| 		uploadResult.withStatusCode(i);
 | |
| 		if (response.getEntity() != null) {
 | |
| 			String string = EntityUtils.toString(response.getEntity(), "UTF-8");
 | |
| 			if (string != null) {
 | |
| 				try {
 | |
| 					JsonElement jsonElement = LenientJsonParser.parse(string).getAsJsonObject().get("errorMsg");
 | |
| 					Optional<String> optional = Optional.ofNullable(jsonElement).map(JsonElement::getAsString);
 | |
| 					uploadResult.withErrorMessage((String)optional.orElse(null));
 | |
| 				} catch (Exception var7) {
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	private boolean shouldRetry(long retryDelaySeconds, int retries) {
 | |
| 		return retryDelaySeconds > 0L && retries + 1 < 5;
 | |
| 	}
 | |
| 
 | |
| 	private UploadResult retryUploadAfter(long seconds, int retries) throws InterruptedException {
 | |
| 		Thread.sleep(Duration.ofSeconds(seconds).toMillis());
 | |
| 		return this.requestUpload(retries + 1);
 | |
| 	}
 | |
| 
 | |
| 	private long getRetryDelaySeconds(HttpResponse httpResponse) {
 | |
| 		return (Long)Optional.ofNullable(httpResponse.getFirstHeader("Retry-After")).map(NameValuePair::getValue).map(Long::valueOf).orElse(0L);
 | |
| 	}
 | |
| 
 | |
| 	public boolean isFinished() {
 | |
| 		return this.uploadTask.isDone() || this.uploadTask.isCancelled();
 | |
| 	}
 | |
| 
 | |
| 	@Environment(EnvType.CLIENT)
 | |
| 	class CustomInputStreamEntity extends InputStreamEntity {
 | |
| 		private final long length;
 | |
| 		private final InputStream content;
 | |
| 		private final UploadStatus uploadStatus;
 | |
| 
 | |
| 		public CustomInputStreamEntity(final InputStream content, final long length, final UploadStatus uploadStatus) {
 | |
| 			super(content);
 | |
| 			this.content = content;
 | |
| 			this.length = length;
 | |
| 			this.uploadStatus = uploadStatus;
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		public void writeTo(OutputStream outputStream) throws IOException {
 | |
| 			Args.notNull(outputStream, "Output stream");
 | |
| 			InputStream inputStream = this.content;
 | |
| 
 | |
| 			try {
 | |
| 				byte[] bs = new byte[4096];
 | |
| 				int i;
 | |
| 				if (this.length < 0L) {
 | |
| 					while ((i = inputStream.read(bs)) != -1) {
 | |
| 						if (FileUpload.this.cancelled.get()) {
 | |
| 							throw new RealmsUploadCanceledException();
 | |
| 						}
 | |
| 
 | |
| 						outputStream.write(bs, 0, i);
 | |
| 						this.uploadStatus.onWrite(i);
 | |
| 					}
 | |
| 				} else {
 | |
| 					long l = this.length;
 | |
| 
 | |
| 					while (l > 0L) {
 | |
| 						i = inputStream.read(bs, 0, (int)Math.min(4096L, l));
 | |
| 						if (i == -1) {
 | |
| 							break;
 | |
| 						}
 | |
| 
 | |
| 						if (FileUpload.this.cancelled.get()) {
 | |
| 							throw new RealmsUploadCanceledException();
 | |
| 						}
 | |
| 
 | |
| 						outputStream.write(bs, 0, i);
 | |
| 						this.uploadStatus.onWrite(i);
 | |
| 						l -= i;
 | |
| 						outputStream.flush();
 | |
| 					}
 | |
| 				}
 | |
| 			} catch (Throwable var8) {
 | |
| 				if (inputStream != null) {
 | |
| 					try {
 | |
| 						inputStream.close();
 | |
| 					} catch (Throwable var7) {
 | |
| 						var8.addSuppressed(var7);
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				throw var8;
 | |
| 			}
 | |
| 
 | |
| 			if (inputStream != null) {
 | |
| 				inputStream.close();
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 |