FetchTask 启用透明 GZip 压缩 (#4219)

This commit is contained in:
Glavo 2025-08-08 16:24:48 +08:00 committed by GitHub
parent bbc546e2a0
commit b1d8edc72a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 63 additions and 40 deletions

View File

@ -21,26 +21,24 @@ import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventBus; import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.util.CacheRepository; import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.DigestUtils; import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.io.ContentEncoding;
import org.jackhuang.hmcl.util.io.IOUtils; import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException; import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.Closeable; import java.io.*;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong;
import static org.jackhuang.hmcl.util.Lang.threadPool; import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@ -107,7 +105,7 @@ public abstract class FetchTask<T> extends Task<T> {
break download; break download;
} }
List<String> redirects = null; List<URI> redirects = null;
String bmclapiHash = null; String bmclapiHash = null;
try { try {
beforeDownload(uri); beforeDownload(uri);
@ -117,6 +115,7 @@ public abstract class FetchTask<T> extends Task<T> {
if (conn instanceof HttpURLConnection) { if (conn instanceof HttpURLConnection) {
var httpConnection = (HttpURLConnection) conn; var httpConnection = (HttpURLConnection) conn;
httpConnection.setRequestProperty("Accept-Encoding", "gzip");
if (checkETag) repository.injectConnection(httpConnection); if (checkETag) repository.injectConnection(httpConnection);
Map<String, List<String>> requestProperties = httpConnection.getRequestProperties(); Map<String, List<String>> requestProperties = httpConnection.getRequestProperties();
@ -143,24 +142,24 @@ public abstract class FetchTask<T> extends Task<T> {
throw new IOException("Too much redirects"); throw new IOException("Too much redirects");
} }
URL prevUrl = httpConnection.getURL();
String location = httpConnection.getHeaderField("Location"); String location = httpConnection.getHeaderField("Location");
httpConnection.disconnect(); httpConnection.disconnect();
if (location == null || location.isBlank()) { if (StringUtils.isBlank(location))
throw new IOException("Redirected to an empty location"); throw new IOException("Redirected to an empty location");
}
URL target = new URL(prevUrl, NetworkUtils.encodeLocation(location)); URI target = NetworkUtils.toURI(httpConnection.getURL())
redirects.add(target.toString()); .resolve(NetworkUtils.toURI(location));
redirects.add(target);
HttpURLConnection redirected = (HttpURLConnection) target.openConnection(); if (!NetworkUtils.isHttpUri(target))
throw new IOException("Redirected to not http URI: " + target);
HttpURLConnection redirected = NetworkUtils.createHttpConnection(target);
redirected.setUseCaches(checkETag); redirected.setUseCaches(checkETag);
redirected.setConnectTimeout(NetworkUtils.TIME_OUT);
redirected.setReadTimeout(NetworkUtils.TIME_OUT);
redirected.setInstanceFollowRedirects(false);
requestProperties requestProperties
.forEach((key, value) -> value.forEach(element -> .forEach((key, values) ->
values.forEach(element ->
redirected.addRequestProperty(key, element))); redirected.addRequestProperty(key, element)));
httpConnection = redirected; httpConnection = redirected;
} else { } else {
@ -192,36 +191,36 @@ public abstract class FetchTask<T> extends Task<T> {
} }
} }
long contentLength = conn.getContentLength(); long contentLength = conn.getContentLengthLong();
try (Context context = getContext(conn, checkETag, bmclapiHash); var encoding = ContentEncoding.fromConnection(conn);
InputStream stream = conn.getInputStream()) { try (var context = getContext(conn, checkETag, bmclapiHash);
int lastDownloaded = 0, downloaded = 0; var counter = new CounterInputStream(conn.getInputStream());
var input = encoding.wrap(counter)) {
long lastDownloaded = 0L;
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE]; byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
while (true) { while (true) {
if (isCancelled()) break; if (isCancelled()) break;
int len = stream.read(buffer); int len = input.read(buffer);
if (len == -1) break; if (len == -1) break;
context.write(buffer, 0, len); context.write(buffer, 0, len);
downloaded += len;
if (contentLength >= 0) { if (contentLength >= 0) {
// Update progress information per second // Update progress information per second
updateProgress(downloaded, contentLength); updateProgress(counter.downloaded, contentLength);
} }
updateDownloadSpeed(downloaded - lastDownloaded); updateDownloadSpeed(counter.downloaded - lastDownloaded);
lastDownloaded = downloaded; lastDownloaded = counter.downloaded;
} }
if (isCancelled()) break download; if (isCancelled()) break download;
updateDownloadSpeed(downloaded - lastDownloaded); updateDownloadSpeed(counter.downloaded - lastDownloaded);
if (contentLength >= 0 && downloaded != contentLength) if (contentLength >= 0 && counter.downloaded != contentLength)
throw new IOException("Unexpected file size: " + downloaded + ", expected: " + contentLength); throw new IOException("Unexpected file size: " + counter.downloaded + ", expected: " + contentLength);
context.withResult(true); context.withResult(true);
} }
@ -246,7 +245,7 @@ public abstract class FetchTask<T> extends Task<T> {
} }
private static final Timer timer = new Timer("DownloadSpeedRecorder", true); private static final Timer timer = new Timer("DownloadSpeedRecorder", true);
private static final AtomicInteger downloadSpeed = new AtomicInteger(0); private static final AtomicLong downloadSpeed = new AtomicLong(0L);
public static final EventBus speedEvent = new EventBus(); public static final EventBus speedEvent = new EventBus();
static { static {
@ -258,14 +257,38 @@ public abstract class FetchTask<T> extends Task<T> {
}, 0, 1000); }, 0, 1000);
} }
private static void updateDownloadSpeed(int speed) { private static void updateDownloadSpeed(long speed) {
downloadSpeed.addAndGet(speed); downloadSpeed.addAndGet(speed);
} }
public static class SpeedEvent extends Event { private static final class CounterInputStream extends FilterInputStream {
private final int speed; long downloaded;
public SpeedEvent(Object source, int speed) { CounterInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int b = in.read();
if (b >= 0)
downloaded++;
return b;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int n = in.read(b, off, len);
if (n >= 0)
downloaded += n;
return n;
}
}
public static class SpeedEvent extends Event {
private final long speed;
public SpeedEvent(Object source, long speed) {
super(source); super(source);
this.speed = speed; this.speed = speed;
@ -276,7 +299,7 @@ public abstract class FetchTask<T> extends Task<T> {
* *
* @return download speed * @return download speed
*/ */
public int getSpeed() { public long getSpeed() {
return speed; return speed;
} }

View File

@ -28,7 +28,7 @@ import java.util.zip.GZIPInputStream;
* @author Glavo * @author Glavo
*/ */
public enum ContentEncoding { public enum ContentEncoding {
NONE { IDENTITY {
@Override @Override
public InputStream wrap(InputStream inputStream) { public InputStream wrap(InputStream inputStream) {
return inputStream; return inputStream;
@ -43,8 +43,8 @@ public enum ContentEncoding {
public static @NotNull ContentEncoding fromConnection(URLConnection connection) throws IOException { public static @NotNull ContentEncoding fromConnection(URLConnection connection) throws IOException {
String encoding = connection.getContentEncoding(); String encoding = connection.getContentEncoding();
if (encoding == null || encoding.isEmpty()) { if (encoding == null || encoding.isEmpty() || "identity".equals(encoding)) {
return NONE; return IDENTITY;
} else if ("gzip".equalsIgnoreCase(encoding)) { } else if ("gzip".equalsIgnoreCase(encoding)) {
return GZIP; return GZIP;
} else { } else {