缓存文件时遵循 Cache-Control 设置 (#4462)
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.task;
|
package org.jackhuang.hmcl.task;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.util.CacheRepository;
|
||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@@ -51,8 +52,11 @@ public final class CacheFileTask extends FetchTask<Path> {
|
|||||||
// Check cache
|
// Check cache
|
||||||
for (URI uri : uris) {
|
for (URI uri : uris) {
|
||||||
try {
|
try {
|
||||||
setResult(repository.getCachedRemoteFile(uri));
|
setResult(repository.getCachedRemoteFile(uri, true));
|
||||||
|
LOG.info("Using cached file for " + NetworkUtils.dropQuery(uri));
|
||||||
return EnumCheckETag.CACHED;
|
return EnumCheckETag.CACHED;
|
||||||
|
} catch (CacheRepository.CacheExpiredException e) {
|
||||||
|
LOG.info("Cache expired for " + NetworkUtils.dropQuery(uri));
|
||||||
} catch (IOException ignored) {
|
} catch (IOException ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,17 @@ public abstract class FetchTask<T> extends Task<T> {
|
|||||||
int repeat = 0;
|
int repeat = 0;
|
||||||
download:
|
download:
|
||||||
for (URI uri : uris) {
|
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++) {
|
for (int retryTime = 0; retryTime < retry; retryTime++) {
|
||||||
if (isCancelled()) {
|
if (isCancelled()) {
|
||||||
break download;
|
break download;
|
||||||
@@ -113,8 +124,7 @@ public abstract class FetchTask<T> extends Task<T> {
|
|||||||
|
|
||||||
URLConnection conn = NetworkUtils.createConnection(uri);
|
URLConnection conn = NetworkUtils.createConnection(uri);
|
||||||
|
|
||||||
if (conn instanceof HttpURLConnection) {
|
if (conn instanceof HttpURLConnection httpConnection) {
|
||||||
var httpConnection = (HttpURLConnection) conn;
|
|
||||||
httpConnection.setRequestProperty("Accept-Encoding", "gzip");
|
httpConnection.setRequestProperty("Accept-Encoding", "gzip");
|
||||||
|
|
||||||
if (checkETag) repository.injectConnection(httpConnection);
|
if (checkETag) repository.injectConnection(httpConnection);
|
||||||
@@ -172,10 +182,12 @@ public abstract class FetchTask<T> extends Task<T> {
|
|||||||
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||||
// Handle cache
|
// Handle cache
|
||||||
try {
|
try {
|
||||||
Path cache = repository.getCachedRemoteFile(NetworkUtils.toURI(conn.getURL()));
|
Path cache = repository.getCachedRemoteFile(NetworkUtils.toURI(conn.getURL()), false);
|
||||||
useCachedResult(cache);
|
useCachedResult(cache);
|
||||||
LOG.info("Using cached file for " + NetworkUtils.dropQuery(uri));
|
LOG.info("Using cached file for " + NetworkUtils.dropQuery(uri));
|
||||||
return;
|
return;
|
||||||
|
} catch (CacheRepository.CacheExpiredException e) {
|
||||||
|
LOG.info("Cache expired for " + NetworkUtils.dropQuery(uri));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.warning("Unable to use cached file, redownload " + NetworkUtils.dropQuery(uri), e);
|
LOG.warning("Unable to use cached file, redownload " + NetworkUtils.dropQuery(uri), e);
|
||||||
repository.removeRemoteEntry(conn.getURL().toURI());
|
repository.removeRemoteEntry(conn.getURL().toURI());
|
||||||
|
|||||||
@@ -36,12 +36,15 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.nio.file.attribute.FileTime;
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.time.Instant;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.locks.ReadWriteLock;
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
import java.util.function.BiFunction;
|
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 java.nio.charset.StandardCharsets.UTF_8;
|
||||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.*;
|
import static org.jackhuang.hmcl.util.gson.JsonUtils.*;
|
||||||
@@ -150,7 +153,7 @@ public class CacheRepository {
|
|||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Path getCachedRemoteFile(URI uri) throws IOException {
|
public Path getCachedRemoteFile(URI uri, boolean checkExpires) throws IOException {
|
||||||
lock.readLock().lock();
|
lock.readLock().lock();
|
||||||
ETagItem eTagItem;
|
ETagItem eTagItem;
|
||||||
try {
|
try {
|
||||||
@@ -160,6 +163,9 @@ public class CacheRepository {
|
|||||||
}
|
}
|
||||||
if (eTagItem == null) throw new IOException("Cannot find the URL");
|
if (eTagItem == null) throw new IOException("Cannot find the URL");
|
||||||
if (StringUtils.isBlank(eTagItem.hash) || !fileExists(SHA1, eTagItem.hash)) throw new FileNotFoundException();
|
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);
|
Path file = getFile(SHA1, eTagItem.hash);
|
||||||
if (Files.getLastModifiedTime(file).toMillis() != eTagItem.localLastModified) {
|
if (Files.getLastModifiedTime(file).toMillis() != eTagItem.localLastModified) {
|
||||||
String hash = DigestUtils.digestToString(SHA1, file);
|
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 {
|
private Path cacheData(URLConnection connection, ExceptionalSupplier<CacheResult, IOException> cacheSupplier) throws IOException {
|
||||||
String eTag = connection.getHeaderField("ETag");
|
String eTag = connection.getHeaderField("ETag");
|
||||||
if (StringUtils.isBlank(eTag)) return null;
|
if (StringUtils.isBlank(eTag)) return null;
|
||||||
@@ -231,9 +239,41 @@ public class CacheRepository {
|
|||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
throw new IOException(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");
|
String lastModified = connection.getHeaderField("Last-Modified");
|
||||||
|
|
||||||
CacheResult cacheResult = cacheSupplier.get();
|
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();
|
lock.writeLock().lock();
|
||||||
try {
|
try {
|
||||||
index.compute(uri, updateEntity(eTagItem, true));
|
index.compute(uri, updateEntity(eTagItem, true));
|
||||||
@@ -336,20 +376,26 @@ public class CacheRepository {
|
|||||||
private final long localLastModified;
|
private final long localLastModified;
|
||||||
@SerializedName("remote")
|
@SerializedName("remote")
|
||||||
private final String remoteLastModified;
|
private final String remoteLastModified;
|
||||||
|
private final long expires;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For Gson.
|
* For Gson.
|
||||||
*/
|
*/
|
||||||
public ETagItem() {
|
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.url = url;
|
||||||
this.eTag = eTag;
|
this.eTag = eTag;
|
||||||
this.hash = hash;
|
this.hash = hash;
|
||||||
this.localLastModified = localLastModified;
|
this.localLastModified = localLastModified;
|
||||||
this.remoteLastModified = remoteLastModified;
|
this.remoteLastModified = remoteLastModified;
|
||||||
|
this.expires = expires;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getExpires() {
|
||||||
|
return expires;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int compareTo(ETagItem other) {
|
public int compareTo(ETagItem other) {
|
||||||
@@ -367,18 +413,18 @@ public class CacheRepository {
|
|||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
return o instanceof ETagItem that
|
||||||
ETagItem eTagItem = (ETagItem) o;
|
&& localLastModified == that.localLastModified
|
||||||
return localLastModified == eTagItem.localLastModified &&
|
&& Objects.equals(url, that.url)
|
||||||
Objects.equals(url, eTagItem.url) &&
|
&& Objects.equals(eTag, that.eTag)
|
||||||
Objects.equals(eTag, eTagItem.eTag) &&
|
&& Objects.equals(hash, that.hash)
|
||||||
Objects.equals(hash, eTagItem.hash) &&
|
&& Objects.equals(remoteLastModified, that.remoteLastModified)
|
||||||
Objects.equals(remoteLastModified, eTagItem.remoteLastModified);
|
&& this.expires == that.expires;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(url, eTag, hash, localLastModified, remoteLastModified);
|
return Objects.hash(url, eTag, hash, localLastModified, remoteLastModified, expires);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -389,6 +435,7 @@ public class CacheRepository {
|
|||||||
", hash='" + hash + '\'' +
|
", hash='" + hash + '\'' +
|
||||||
", localLastModified=" + localLastModified +
|
", localLastModified=" + localLastModified +
|
||||||
", remoteLastModified='" + remoteLastModified + '\'' +
|
", remoteLastModified='" + remoteLastModified + '\'' +
|
||||||
|
", expires=" + expires +
|
||||||
']';
|
']';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -404,4 +451,21 @@ public class CacheRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static final String SHA1 = "SHA-1";
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user