minecraft-src/com/mojang/realmsclient/client/FileDownload.java
2025-07-04 01:41:11 +03:00

437 lines
14 KiB
Java

package com.mojang.realmsclient.client;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import com.mojang.logging.LogUtils;
import com.mojang.realmsclient.dto.WorldDownload;
import com.mojang.realmsclient.exception.RealmsDefaultUncaughtExceptionHandler;
import com.mojang.realmsclient.gui.screens.RealmsDownloadLatestWorldScreen;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.SharedConstants;
import net.minecraft.client.Minecraft;
import net.minecraft.nbt.NbtException;
import net.minecraft.nbt.ReportedNbtException;
import net.minecraft.world.level.storage.LevelStorageSource;
import net.minecraft.world.level.validation.ContentValidationException;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.CountingOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
@Environment(EnvType.CLIENT)
public class FileDownload {
static final Logger LOGGER = LogUtils.getLogger();
volatile boolean cancelled;
volatile boolean finished;
volatile boolean error;
volatile boolean extracting;
@Nullable
private volatile File tempFile;
volatile File resourcePackPath;
@Nullable
private volatile HttpGet request;
@Nullable
private Thread currentThread;
private final RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(120000).setConnectTimeout(120000).build();
private static final String[] INVALID_FILE_NAMES = new String[]{
"CON",
"COM",
"PRN",
"AUX",
"CLOCK$",
"NUL",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9"
};
public long contentLength(String uri) {
CloseableHttpClient closeableHttpClient = null;
HttpGet httpGet = null;
long var5;
try {
httpGet = new HttpGet(uri);
closeableHttpClient = HttpClientBuilder.create().setDefaultRequestConfig(this.requestConfig).build();
CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(httpGet);
return Long.parseLong(closeableHttpResponse.getFirstHeader("Content-Length").getValue());
} catch (Throwable var16) {
LOGGER.error("Unable to get content length for download");
var5 = 0L;
} finally {
if (httpGet != null) {
httpGet.releaseConnection();
}
if (closeableHttpClient != null) {
try {
closeableHttpClient.close();
} catch (IOException var15) {
LOGGER.error("Could not close http client", (Throwable)var15);
}
}
}
return var5;
}
public void download(WorldDownload download, String worldName, RealmsDownloadLatestWorldScreen.DownloadStatus status, LevelStorageSource source) {
if (this.currentThread == null) {
this.currentThread = new Thread(
() -> {
CloseableHttpClient closeableHttpClient = null;
try {
this.tempFile = File.createTempFile("backup", ".tar.gz");
this.request = new HttpGet(download.downloadLink);
closeableHttpClient = HttpClientBuilder.create().setDefaultRequestConfig(this.requestConfig).build();
HttpResponse httpResponse = closeableHttpClient.execute(this.request);
status.totalBytes = Long.parseLong(httpResponse.getFirstHeader("Content-Length").getValue());
if (httpResponse.getStatusLine().getStatusCode() == 200) {
OutputStream outputStream2 = new FileOutputStream(this.tempFile);
FileDownload.ProgressListener progressListener = new FileDownload.ProgressListener(worldName.trim(), this.tempFile, source, status);
FileDownload.DownloadCountingOutputStream downloadCountingOutputStream2 = new FileDownload.DownloadCountingOutputStream(outputStream2);
downloadCountingOutputStream2.setListener(progressListener);
IOUtils.copy(httpResponse.getEntity().getContent(), downloadCountingOutputStream2);
return;
}
this.error = true;
this.request.abort();
} catch (Exception var93) {
LOGGER.error("Caught exception while downloading: {}", var93.getMessage());
this.error = true;
return;
} finally {
this.request.releaseConnection();
if (this.tempFile != null) {
this.tempFile.delete();
}
if (!this.error) {
if (!download.resourcePackUrl.isEmpty() && !download.resourcePackHash.isEmpty()) {
try {
this.tempFile = File.createTempFile("resources", ".tar.gz");
this.request = new HttpGet(download.resourcePackUrl);
HttpResponse httpResponse3 = closeableHttpClient.execute(this.request);
status.totalBytes = Long.parseLong(httpResponse3.getFirstHeader("Content-Length").getValue());
if (httpResponse3.getStatusLine().getStatusCode() != 200) {
this.error = true;
this.request.abort();
return;
}
OutputStream outputStream3 = new FileOutputStream(this.tempFile);
FileDownload.ResourcePackProgressListener resourcePackProgressListener3 = new FileDownload.ResourcePackProgressListener(
this.tempFile, status, download
);
FileDownload.DownloadCountingOutputStream downloadCountingOutputStream3 = new FileDownload.DownloadCountingOutputStream(outputStream3);
downloadCountingOutputStream3.setListener(resourcePackProgressListener3);
IOUtils.copy(httpResponse3.getEntity().getContent(), downloadCountingOutputStream3);
} catch (Exception var91) {
LOGGER.error("Caught exception while downloading: {}", var91.getMessage());
this.error = true;
} finally {
this.request.releaseConnection();
if (this.tempFile != null) {
this.tempFile.delete();
}
}
} else {
this.finished = true;
}
}
if (closeableHttpClient != null) {
try {
closeableHttpClient.close();
} catch (IOException var90) {
LOGGER.error("Failed to close Realms download client");
}
}
}
}
);
this.currentThread.setUncaughtExceptionHandler(new RealmsDefaultUncaughtExceptionHandler(LOGGER));
this.currentThread.start();
}
}
public void cancel() {
if (this.request != null) {
this.request.abort();
}
if (this.tempFile != null) {
this.tempFile.delete();
}
this.cancelled = true;
}
public boolean isFinished() {
return this.finished;
}
public boolean isError() {
return this.error;
}
public boolean isExtracting() {
return this.extracting;
}
/**
* Modifies a folder name to make sure it is valid to store on disk.
* @return the modified folder name
*
* @param folderName The folder name to modify
*/
public static String findAvailableFolderName(String folderName) {
folderName = folderName.replaceAll("[\\./\"]", "_");
for (String string : INVALID_FILE_NAMES) {
if (folderName.equalsIgnoreCase(string)) {
folderName = "_" + folderName + "_";
}
}
return folderName;
}
void untarGzipArchive(String worldName, @Nullable File tempFile, LevelStorageSource levelStorageSource) throws IOException {
Pattern pattern = Pattern.compile(".*-([0-9]+)$");
int i = 1;
for (char c : SharedConstants.ILLEGAL_FILE_CHARACTERS) {
worldName = worldName.replace(c, '_');
}
if (StringUtils.isEmpty(worldName)) {
worldName = "Realm";
}
worldName = findAvailableFolderName(worldName);
try {
for (LevelStorageSource.LevelDirectory levelDirectory : levelStorageSource.findLevelCandidates()) {
String string = levelDirectory.directoryName();
if (string.toLowerCase(Locale.ROOT).startsWith(worldName.toLowerCase(Locale.ROOT))) {
Matcher matcher = pattern.matcher(string);
if (matcher.matches()) {
int j = Integer.parseInt(matcher.group(1));
if (j > i) {
i = j;
}
} else {
i++;
}
}
}
} catch (Exception var43) {
LOGGER.error("Error getting level list", (Throwable)var43);
this.error = true;
return;
}
String string2;
if (levelStorageSource.isNewLevelIdAcceptable(worldName) && i <= 1) {
string2 = worldName;
} else {
string2 = worldName + (i == 1 ? "" : "-" + i);
if (!levelStorageSource.isNewLevelIdAcceptable(string2)) {
boolean bl = false;
while (!bl) {
i++;
string2 = worldName + (i == 1 ? "" : "-" + i);
if (levelStorageSource.isNewLevelIdAcceptable(string2)) {
bl = true;
}
}
}
}
TarArchiveInputStream tarArchiveInputStream = null;
File file = new File(Minecraft.getInstance().gameDirectory.getAbsolutePath(), "saves");
try {
file.mkdir();
tarArchiveInputStream = new TarArchiveInputStream(new GzipCompressorInputStream(new BufferedInputStream(new FileInputStream(tempFile))));
for (TarArchiveEntry tarArchiveEntry = tarArchiveInputStream.getNextTarEntry();
tarArchiveEntry != null;
tarArchiveEntry = tarArchiveInputStream.getNextTarEntry()
) {
File file2 = new File(file, tarArchiveEntry.getName().replace("world", string2));
if (tarArchiveEntry.isDirectory()) {
file2.mkdirs();
} else {
file2.createNewFile();
FileOutputStream fileOutputStream = new FileOutputStream(file2);
try {
IOUtils.copy(tarArchiveInputStream, fileOutputStream);
} catch (Throwable var37) {
try {
fileOutputStream.close();
} catch (Throwable var36) {
var37.addSuppressed(var36);
}
throw var37;
}
fileOutputStream.close();
}
}
} catch (Exception var41) {
LOGGER.error("Error extracting world", (Throwable)var41);
this.error = true;
} finally {
if (tarArchiveInputStream != null) {
tarArchiveInputStream.close();
}
if (tempFile != null) {
tempFile.delete();
}
try (LevelStorageSource.LevelStorageAccess levelStorageAccess2 = levelStorageSource.validateAndCreateAccess(string2)) {
levelStorageAccess2.renameAndDropPlayer(string2);
} catch (NbtException | ReportedNbtException | IOException var39) {
LOGGER.error("Failed to modify unpacked realms level {}", string2, var39);
} catch (ContentValidationException var40) {
LOGGER.warn("{}", var40.getMessage());
}
this.resourcePackPath = new File(file, string2 + File.separator + "resources.zip");
}
}
@Environment(EnvType.CLIENT)
static class DownloadCountingOutputStream extends CountingOutputStream {
@Nullable
private ActionListener listener;
public DownloadCountingOutputStream(OutputStream out) {
super(out);
}
public void setListener(ActionListener listener) {
this.listener = listener;
}
@Override
protected void afterWrite(int i) throws IOException {
super.afterWrite(i);
if (this.listener != null) {
this.listener.actionPerformed(new ActionEvent(this, 0, null));
}
}
}
@Environment(EnvType.CLIENT)
class ProgressListener implements ActionListener {
private final String worldName;
private final File tempFile;
private final LevelStorageSource levelStorageSource;
private final RealmsDownloadLatestWorldScreen.DownloadStatus downloadStatus;
ProgressListener(
final String worldName,
final File tempFile,
final LevelStorageSource levelStorageSource,
final RealmsDownloadLatestWorldScreen.DownloadStatus downloadStatus
) {
this.worldName = worldName;
this.tempFile = tempFile;
this.levelStorageSource = levelStorageSource;
this.downloadStatus = downloadStatus;
}
public void actionPerformed(ActionEvent actionEvent) {
this.downloadStatus.bytesWritten = ((FileDownload.DownloadCountingOutputStream)actionEvent.getSource()).getByteCount();
if (this.downloadStatus.bytesWritten >= this.downloadStatus.totalBytes && !FileDownload.this.cancelled && !FileDownload.this.error) {
try {
FileDownload.this.extracting = true;
FileDownload.this.untarGzipArchive(this.worldName, this.tempFile, this.levelStorageSource);
} catch (IOException var3) {
FileDownload.LOGGER.error("Error extracting archive", (Throwable)var3);
FileDownload.this.error = true;
}
}
}
}
@Environment(EnvType.CLIENT)
class ResourcePackProgressListener implements ActionListener {
private final File tempFile;
private final RealmsDownloadLatestWorldScreen.DownloadStatus downloadStatus;
private final WorldDownload worldDownload;
ResourcePackProgressListener(final File tempFile, final RealmsDownloadLatestWorldScreen.DownloadStatus downloadStatus, final WorldDownload worldDownload) {
this.tempFile = tempFile;
this.downloadStatus = downloadStatus;
this.worldDownload = worldDownload;
}
public void actionPerformed(ActionEvent actionEvent) {
this.downloadStatus.bytesWritten = ((FileDownload.DownloadCountingOutputStream)actionEvent.getSource()).getByteCount();
if (this.downloadStatus.bytesWritten >= this.downloadStatus.totalBytes && !FileDownload.this.cancelled) {
try {
String string = Hashing.sha1().hashBytes(Files.toByteArray(this.tempFile)).toString();
if (string.equals(this.worldDownload.resourcePackHash)) {
FileUtils.copyFile(this.tempFile, FileDownload.this.resourcePackPath);
FileDownload.this.finished = true;
} else {
FileDownload.LOGGER.error("Resourcepack had wrong hash (expected {}, found {}). Deleting it.", this.worldDownload.resourcePackHash, string);
FileUtils.deleteQuietly(this.tempFile);
FileDownload.this.error = true;
}
} catch (IOException var3) {
FileDownload.LOGGER.error("Error copying resourcepack file: {}", var3.getMessage());
FileDownload.this.error = true;
}
}
}
}
}