mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-22 10:43:57 -04:00
缓存文件时遵循 Cache-Control 设置 (#4462)
This commit is contained in:
parent
e4bc8f48c5
commit
5005343d00
@ -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) {
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user