缓存文件时遵循 Cache-Control 设置 (#4462)

This commit is contained in:
Glavo 2025-09-16 15:15:30 +08:00 committed by GitHub
parent e4bc8f48c5
commit 5005343d00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 96 additions and 16 deletions

View File

@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull;
@ -51,8 +52,11 @@ public final class CacheFileTask extends FetchTask<Path> {
// Check cache
for (URI uri : uris) {
try {
setResult(repository.getCachedRemoteFile(uri));
setResult(repository.getCachedRemoteFile(uri, true));
LOG.info("Using cached file for " + NetworkUtils.dropQuery(uri));
return EnumCheckETag.CACHED;
} catch (CacheRepository.CacheExpiredException e) {
LOG.info("Cache expired for " + NetworkUtils.dropQuery(uri));
} catch (IOException ignored) {
}
}

View File

@ -100,6 +100,17 @@ public abstract class FetchTask<T> extends Task<T> {
int repeat = 0;
download:
for (URI uri : uris) {
if (checkETag) {
// Handle cache
try {
Path cache = repository.getCachedRemoteFile(uri, true);
useCachedResult(cache);
LOG.info("Using cached file for " + NetworkUtils.dropQuery(uri));
return;
} catch (IOException ignored) {
}
}
for (int retryTime = 0; retryTime < retry; retryTime++) {
if (isCancelled()) {
break download;
@ -113,8 +124,7 @@ public abstract class FetchTask<T> extends Task<T> {
URLConnection conn = NetworkUtils.createConnection(uri);
if (conn instanceof HttpURLConnection) {
var httpConnection = (HttpURLConnection) conn;
if (conn instanceof HttpURLConnection httpConnection) {
httpConnection.setRequestProperty("Accept-Encoding", "gzip");
if (checkETag) repository.injectConnection(httpConnection);
@ -172,10 +182,12 @@ public abstract class FetchTask<T> extends Task<T> {
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
// Handle cache
try {
Path cache = repository.getCachedRemoteFile(NetworkUtils.toURI(conn.getURL()));
Path cache = repository.getCachedRemoteFile(NetworkUtils.toURI(conn.getURL()), false);
useCachedResult(cache);
LOG.info("Using cached file for " + NetworkUtils.dropQuery(uri));
return;
} catch (CacheRepository.CacheExpiredException e) {
LOG.info("Cache expired for " + NetworkUtils.dropQuery(uri));
} catch (IOException e) {
LOG.warning("Unable to use cached file, redownload " + NetworkUtils.dropQuery(uri), e);
repository.removeRemoteEntry(conn.getURL().toURI());

View File

@ -36,12 +36,15 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.gson.JsonUtils.*;
@ -150,7 +153,7 @@ public class CacheRepository {
return cache;
}
public Path getCachedRemoteFile(URI uri) throws IOException {
public Path getCachedRemoteFile(URI uri, boolean checkExpires) throws IOException {
lock.readLock().lock();
ETagItem eTagItem;
try {
@ -160,6 +163,9 @@ public class CacheRepository {
}
if (eTagItem == null) throw new IOException("Cannot find the URL");
if (StringUtils.isBlank(eTagItem.hash) || !fileExists(SHA1, eTagItem.hash)) throw new FileNotFoundException();
if (checkExpires && System.currentTimeMillis() > eTagItem.expires)
throw new CacheExpiredException(eTagItem.expires);
Path file = getFile(SHA1, eTagItem.hash);
if (Files.getLastModifiedTime(file).toMillis() != eTagItem.localLastModified) {
String hash = DigestUtils.digestToString(SHA1, file);
@ -222,6 +228,8 @@ public class CacheRepository {
});
}
private static final Pattern MAX_AGE = Pattern.compile("(s-maxage|max-age)=(?<time>[0-9]+)");
private Path cacheData(URLConnection connection, ExceptionalSupplier<CacheResult, IOException> cacheSupplier) throws IOException {
String eTag = connection.getHeaderField("ETag");
if (StringUtils.isBlank(eTag)) return null;
@ -231,9 +239,41 @@ public class CacheRepository {
} catch (IllegalArgumentException e) {
throw new IOException(e);
}
long expires = 0L;
expires:
try {
String cacheControl = connection.getHeaderField("Cache-Control");
if (StringUtils.isNotBlank(cacheControl)) {
if (cacheControl.contains("no-store"))
return null;
Matcher matcher = MAX_AGE.matcher(cacheControl);
if (matcher.find()) {
long seconds = Long.parseLong(matcher.group("time"));
expires = Instant.now().plusSeconds(seconds).toEpochMilli();
break expires;
}
}
String expiresHeader = connection.getHeaderField("Expires");
if (StringUtils.isNotBlank(expiresHeader)) {
expires = ZonedDateTime.parse(expiresHeader.trim(), DateTimeFormatter.RFC_1123_DATE_TIME)
.toInstant().toEpochMilli();
}
} catch (Throwable e) {
LOG.warning("Failed to parse expires time", e);
}
String lastModified = connection.getHeaderField("Last-Modified");
CacheResult cacheResult = cacheSupplier.get();
ETagItem eTagItem = new ETagItem(uri.toString(), eTag, cacheResult.hash, Files.getLastModifiedTime(cacheResult.cachedFile).toMillis(), lastModified);
ETagItem eTagItem = new ETagItem(uri.toString(),
eTag,
cacheResult.hash,
Files.getLastModifiedTime(cacheResult.cachedFile).toMillis(),
lastModified,
expires);
lock.writeLock().lock();
try {
index.compute(uri, updateEntity(eTagItem, true));
@ -336,20 +376,26 @@ public class CacheRepository {
private final long localLastModified;
@SerializedName("remote")
private final String remoteLastModified;
private final long expires;
/**
* For Gson.
*/
public ETagItem() {
this(null, null, null, 0, null);
this(null, null, null, 0, null, 0L);
}
public ETagItem(String url, String eTag, String hash, long localLastModified, String remoteLastModified) {
public ETagItem(String url, String eTag, String hash, long localLastModified, String remoteLastModified, long expires) {
this.url = url;
this.eTag = eTag;
this.hash = hash;
this.localLastModified = localLastModified;
this.remoteLastModified = remoteLastModified;
this.expires = expires;
}
public long getExpires() {
return expires;
}
public int compareTo(ETagItem other) {
@ -367,18 +413,18 @@ public class CacheRepository {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ETagItem eTagItem = (ETagItem) o;
return localLastModified == eTagItem.localLastModified &&
Objects.equals(url, eTagItem.url) &&
Objects.equals(eTag, eTagItem.eTag) &&
Objects.equals(hash, eTagItem.hash) &&
Objects.equals(remoteLastModified, eTagItem.remoteLastModified);
return o instanceof ETagItem that
&& localLastModified == that.localLastModified
&& Objects.equals(url, that.url)
&& Objects.equals(eTag, that.eTag)
&& Objects.equals(hash, that.hash)
&& Objects.equals(remoteLastModified, that.remoteLastModified)
&& this.expires == that.expires;
}
@Override
public int hashCode() {
return Objects.hash(url, eTag, hash, localLastModified, remoteLastModified);
return Objects.hash(url, eTag, hash, localLastModified, remoteLastModified, expires);
}
@Override
@ -389,6 +435,7 @@ public class CacheRepository {
", hash='" + hash + '\'' +
", localLastModified=" + localLastModified +
", remoteLastModified='" + remoteLastModified + '\'' +
", expires=" + expires +
']';
}
}
@ -404,4 +451,21 @@ public class CacheRepository {
}
public static final String SHA1 = "SHA-1";
public static class CacheExpiredException extends IOException {
private final long expires;
public CacheExpiredException(long expires) {
this.expires = expires;
}
public CacheExpiredException(String message, long expires) {
super(message);
this.expires = expires;
}
public long getExpires() {
return expires;
}
}
}