1
0
mirror of https://github.com/arduino/Arduino.git synced 2025-01-31 20:52:13 +01:00

Add cache.json file and improve stability

This commit is contained in:
Mattia Bertorello 2019-07-03 15:29:42 +02:00
parent 00818af181
commit a7d395f45e
No known key found for this signature in database
GPG Key ID: CE1FB2BE91770F24
3 changed files with 516 additions and 74 deletions

View File

@ -0,0 +1,236 @@
package cc.arduino.utils.network;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Represents a HTTP Cache-Control response header and parses it from string.
*
* <p>Note: This class ignores <tt>1#field-name</tt> parameter for
* <tt>private</tt> and <tt>no-cache</tt> directive and cache extensions.</p>
*
* @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9">HTTP/1.1 section 14.9</a>
*/
public class CacheControl {
// copied from org.apache.abdera.protocol.util.CacheControlUtil
private static final Pattern PATTERN
= Pattern.compile("\\s*([\\w\\-]+)\\s*(=)?\\s*(\\-?\\d+|\\\"([^\"\\\\]*(\\\\.[^\"\\\\]*)*)+\\\")?\\s*");
/**
* Corresponds to the <tt>max-age</tt> cache control directive.
* The default value is <tt>-1</tt>, i.e. not specified.
*
* @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3">HTTP/1.1 section 14.9.3</a>
*/
private int maxAge = -1;
/**
* Corresponds to the <tt>s-maxage</tt> cache control directive.
* The default value is <tt>-1</tt>, i.e. not specified.
*
* @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3">HTTP/1.1 section 14.9.3</a>
*/
private int sMaxAge = -1;
/**
* Whether the <tt>must-revalidate</tt> directive is specified.
* The default value is <tt>false</tt>.
*
* @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4">HTTP/1.1 section 14.9.4</a>
*/
private boolean isMustRevalidate = false;
/**
* Whether the <tt>no-cache</tt> directive is specified.
* The default value is <tt>false</tt>.
*
* @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1">HTTP/1.1 section 14.9.1</a>
*/
private boolean isNoCache = false;
/**
* Whether the <tt>no-store</tt> directive is specified.
* The default value is <tt>false</tt>.
*
* @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.2">HTTP/1.1 section 14.9.2</a>
*/
private boolean isNoStore = false;
/**
* Whether the <tt>no-transform</tt> directive is specified.
* The default value is <tt>false</tt>.
*
* @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5">HTTP/1.1 section 14.9.5</a>
*/
private boolean isNoTransform = false;
/**
* Whether the <tt>private</tt> directive is specified.
* The default value is <tt>false</tt>.
*
* @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1">HTTP/1.1 section 14.9.1</a>
*/
private boolean isPrivate = false;
/**
* Whether the <tt>public</tt> directive is specified.
* The default value is <tt>false</tt>.
*
* @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1">HTTP/1.1 section 14.9.1</a>
*/
private boolean isPublic = false;
/**
* Whether the <tt>proxy-revalidate</tt> directive is specified.
* The default value is <tt>false</tt>.
*
* @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4">HTTP/1.1 section 14.9.4</a>
*/
private boolean isProxyRevalidate = false;
/**
* Creates a new instance of CacheControl by parsing the supplied string.
*
* @param value A value the Cache-Control header.
*/
public static CacheControl valueOf(String value) {
CacheControl cc = new CacheControl();
if (value != null) {
Matcher matcher = PATTERN.matcher(value);
while (matcher.find()) {
switch (matcher.group(1).toLowerCase()) {
case "max-age":
cc.setMaxAge(Integer.parseInt(matcher.group(3))); break;
case "s-maxage":
cc.setSMaxAge(Integer.parseInt(matcher.group(3))); break;
case "must-revalidate":
cc.setMustRevalidate(true); break;
case "no-cache":
cc.setNoCache(true); break;
case "no-store":
cc.setNoStore(true); break;
case "no-transform":
cc.setNoTransform(true); break;
case "private":
cc.setPrivate(true); break;
case "public":
cc.setPublic(true); break;
case "proxy-revalidate":
cc.setProxyRevalidate(true); break;
default: //ignore
}
}
}
return cc;
}
/**
* Returns <tt>max-age</tt>, or <tt>s-maxage</tt> according to whether
* considering a shared cache, or a private cache. If shared cache and the
* <tt>s-maxage</tt> is negative (i.e. not set), then returns
* <tt>max-age</tt> instead.
*
* @param sharedCache <tt>true</tt> for a shared cache,
* or <tt>false</tt> for a private cache
* @return A {@link #maxAge}, or {@link #sMaxAge} according to the given
* sharedCache argument.
*/
public int getMaxAge(boolean sharedCache) {
if (sharedCache) {
return sMaxAge >= 0 ? sMaxAge : maxAge;
} else {
return maxAge;
}
}
public void setMaxAge(int maxAge) {
this.maxAge = maxAge;
}
public int getMaxAge() {
return maxAge;
}
public int getSMaxAge() {
return sMaxAge;
}
public void setSMaxAge(int sMaxAge) {
this.sMaxAge = sMaxAge;
}
public boolean isMustRevalidate() {
return isMustRevalidate;
}
public void setMustRevalidate(boolean mustRevalidate) {
isMustRevalidate = mustRevalidate;
}
public boolean isNoCache() {
return isNoCache;
}
public void setNoCache(boolean noCache) {
isNoCache = noCache;
}
public boolean isNoStore() {
return isNoStore;
}
public void setNoStore(boolean noStore) {
isNoStore = noStore;
}
public boolean isNoTransform() {
return isNoTransform;
}
public void setNoTransform(boolean noTransform) {
isNoTransform = noTransform;
}
public boolean isPrivate() {
return isPrivate;
}
public void setPrivate(boolean aPrivate) {
isPrivate = aPrivate;
}
public boolean isPublic() {
return isPublic;
}
public void setPublic(boolean aPublic) {
isPublic = aPublic;
}
public boolean isProxyRevalidate() {
return isProxyRevalidate;
}
public void setProxyRevalidate(boolean proxyRevalidate) {
isProxyRevalidate = proxyRevalidate;
}
@Override
public String toString() {
return "CacheControl{" +
"maxAge=" + maxAge +
", sMaxAge=" + sMaxAge +
", isMustRevalidate=" + isMustRevalidate +
", isNoCache=" + isNoCache +
", isNoStore=" + isNoStore +
", isNoTransform=" + isNoTransform +
", isPrivate=" + isPrivate +
", isPublic=" + isPublic +
", isProxyRevalidate=" + isProxyRevalidate +
'}';
}
}

View File

@ -30,9 +30,8 @@
package cc.arduino.utils.network;
import org.apache.commons.compress.utils.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import processing.app.BaseNoGui;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import processing.app.helpers.FileUtils;
import java.io.File;
@ -48,7 +47,7 @@ import java.util.Observable;
import java.util.Optional;
public class FileDownloader extends Observable {
private static Logger log = LoggerFactory.getLogger(FileDownloader.class);
private static Logger log = LogManager.getLogger(FileDownloader.class);
public enum Status {
CONNECTING, //
@ -146,15 +145,14 @@ public class FileDownloader extends Observable {
try {
setStatus(Status.CONNECTING);
final File settingsFolder = BaseNoGui.getPlatform().getSettingsFolder();
final String cacheFolder = Paths.get(settingsFolder.getPath(), "cache").toString();
final FileDownloaderCache fileDownloaderCache = FileDownloaderCache.getFileCached(cacheFolder, downloadUrl);
final Optional<FileDownloaderCache.FileCached> fileCached = FileDownloaderCache.getFileCached(downloadUrl);
if (!fileDownloaderCache.isChange()) {
if (fileCached.isPresent() && !fileCached.get().isChange()) {
try {
final Optional<File> fileFromCache =
fileDownloaderCache.getFileFromCache();
fileCached.get().getFileFromCache();
if (fileFromCache.isPresent()) {
log.info("No need to download using cached file: {}", fileCached.get());
FileUtils.copyFile(fileFromCache.get(), outputFile);
setStatus(Status.COMPLETE);
return;
@ -195,7 +193,7 @@ public class FileDownloader extends Observable {
synchronized (this) {
stream = connection.getInputStream();
}
byte buffer[] = new byte[10240];
byte[] buffer = new byte[10240];
while (status == Status.DOWNLOADING) {
int read = stream.read(buffer);
if (read == -1)
@ -216,7 +214,9 @@ public class FileDownloader extends Observable {
}
// Set the cache whe it finish to download the file
IOUtils.closeQuietly(randomAccessOutputFile);
fileDownloaderCache.fillCache(outputFile);
if (fileCached.isPresent()) {
fileCached.get().updateCacheFile(outputFile);
}
setStatus(Status.COMPLETE);
} catch (InterruptedException e) {
setStatus(Status.CANCELLED);

View File

@ -1,8 +1,16 @@
package cc.arduino.utils.network;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import processing.app.PreferencesData;
import cc.arduino.utils.FileHash;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import processing.app.BaseNoGui;
import processing.app.helpers.FileUtils;
import javax.script.ScriptException;
@ -15,91 +23,289 @@ import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
public class FileDownloaderCache {
private static Logger log = LoggerFactory.getLogger(FileDownloaderCache.class);
private final Path cacheFilePath;
private final String remoteETag;
private final String preferencesDataKey;
private final static Logger log = LogManager
.getLogger(FileDownloaderCache.class);
private static Map<String, FileCached> cachedFiles = Collections
.synchronizedMap(new HashMap<>());
private static String cacheFolder;
// BaseNoGui.getSettingsFolder()
private FileDownloaderCache(Path cacheFilePath, String remoteETag, String preferencesDataKey) {
this.cacheFilePath = cacheFilePath;
this.remoteETag = remoteETag;
this.preferencesDataKey = preferencesDataKey;
static {
final File settingsFolder;
try {
settingsFolder = BaseNoGui.getPlatform().getSettingsFolder();
cacheFolder = Paths.get(settingsFolder.getPath(), "cache")
.toString();
final Path pathCacheInfo = getCachedInfoPath();
if (Files.exists(pathCacheInfo)) {
ObjectMapper mapper = new ObjectMapper();
final JsonNode jsonNode = mapper.readTree(pathCacheInfo.toFile());
// Read the files array
TypeReference<List<FileCached>> typeRef = new TypeReference<List<FileCached>>() {
};
final List<FileCached> files = mapper
.readValue(mapper.treeAsTokens(jsonNode.get("files")), typeRef);
// Create a map with the remote url as a key and the file cache info as a value
cachedFiles = Collections
.synchronizedMap(files.stream().collect(
Collectors.toMap(FileCached::getRemoteURL, Function.identity())));
}
} catch (Exception e) {
log.error("Cannot initialized the cache", e);
}
}
public static FileDownloaderCache getFileCached(String cacheFolder, URL remoteURL)
throws IOException, NoSuchMethodException, ScriptException, URISyntaxException {
public static Optional<FileCached> getFileCached(URL remoteURL)
throws URISyntaxException, NoSuchMethodException, ScriptException,
IOException {
final String[] splitPath = remoteURL.getPath().split("/");
final String preferencesDataKey = "cache.file." + remoteURL.getPath();
final Path cacheFilePath;
if (splitPath.length > 0) {
cacheFilePath = Paths.get(cacheFolder, splitPath);
final Optional<FileCached> fileCachedOpt;
// The file info must exist in the cachedFiles map but also the real file must exist in the file system
if (cachedFiles.containsKey(remoteURL.toString()) && cachedFiles.get(remoteURL.toString()).exists()) {
fileCachedOpt = Optional.of(cachedFiles.get(remoteURL.toString()));
} else {
cacheFilePath = null;
}
final HttpURLConnection headRequest = new HttpConnectionManager(remoteURL)
.makeConnection((connection) -> {
try {
connection.setRequestMethod("HEAD");
} catch (ProtocolException e) {
log.error("Invalid protocol", e);
// Update
fileCachedOpt = FileDownloaderCache.updateCacheInfo(remoteURL, (remoteETagClean, cacheControl) -> {
// Check cache control data
if (cacheControl.isNoCache() || cacheControl.isMustRevalidate()) {
log.warn("The file must not be cache due to cache control header {}",
cacheControl);
return Optional.empty();
}
final String[] splitPath = remoteURL.getPath().split("/");
final Path cacheFilePath;
if (splitPath.length > 0) {
Deque<String> addFirstRemoteURL = new LinkedList<>(Arrays.asList(splitPath));
addFirstRemoteURL.addFirst(remoteURL.getHost());
cacheFilePath = Paths.get(cacheFolder, addFirstRemoteURL.toArray(new String[0]));
return Optional.of(
new FileCached(remoteETagClean, remoteURL.toString(), cacheFilePath.toString(),
cacheControl));
}
log.warn("The remote path as no file name {}", remoteURL);
return Optional.empty();
});
}
return fileCachedOpt;
}
private static Optional<FileCached> updateCacheInfo(URL remoteURL, BiFunction<String, CacheControl, Optional<FileCached>> getNewFile)
throws URISyntaxException, NoSuchMethodException, ScriptException,
IOException {
// Update the headers of the cached file
final HttpURLConnection headRequest = new HttpConnectionManager(
remoteURL).makeConnection((connection) -> {
try {
connection.setRequestMethod("HEAD");
} catch (ProtocolException e) {
log.error("Invalid protocol", e);
}
});
final int responseCode = headRequest.getResponseCode();
headRequest.disconnect();
// Something bad is happening return a conservative true to try to download the file
if (responseCode < 200 || responseCode >= 300) {
log.warn("The head request return a bad response code " + responseCode);
// if something bad happend
return new FileDownloaderCache(cacheFilePath, null, preferencesDataKey);
return Optional.empty();
}
final String remoteETag = headRequest.getHeaderField("ETag");
String remoteETagClean = null;
if (remoteETag != null) {
remoteETagClean = remoteETag.trim().replace("\"", "");
// Get all the useful headers
String remoteETag = headRequest.getHeaderField("ETag");
String cacheControlHeader = headRequest.getHeaderField("Cache-Control");
if (remoteETag != null && cacheControlHeader != null) {
final String remoteETagClean = remoteETag.trim().replace("\"", "");
final CacheControl cacheControl = CacheControl.valueOf(cacheControlHeader);
return getNewFile.apply(remoteETagClean, cacheControl);
}
return new FileDownloaderCache(cacheFilePath, remoteETagClean, preferencesDataKey);
log.warn("The head request do not return the ETag {} or the Cache-Control {}", remoteETag, cacheControlHeader);
return Optional.empty();
}
public boolean isChange() {
private static void updateCacheFilesInfo() throws IOException {
if (cachedFiles != null) {
synchronized (cachedFiles) {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
final ObjectNode objectNode = mapper.createObjectNode();
objectNode.putArray("files").addAll(
cachedFiles.values().stream()
.map((v) -> mapper.convertValue(v, JsonNode.class))
.collect(Collectors.toList()));
// Create the path Arduino15/cache
Path cachedFileInfo = getCachedInfoPath();
if (Files.notExists(cachedFileInfo)) {
Files.createDirectories(cachedFileInfo.getParent());
}
mapper.writeValue(cachedFileInfo.toFile(), objectNode);
}
}
}
final String localETag = PreferencesData.get(preferencesDataKey);
private static Path getCachedInfoPath() {
return Paths.get(cacheFolder, "cache.json");
}
// If the header doesn't exist or the local cache doesn't exist you need to download the file
if (cacheFilePath == null || remoteETag == null || localETag == null) {
@JsonIgnoreProperties(ignoreUnknown = true)
static class FileCached {
private String eTag;
@JsonIgnore
private final String lastETag;
private final String remoteURL;
private final String localPath;
private String md5;
private String createdAt;
private final CacheControl cacheControl;
FileCached() {
this.lastETag = null;
this.remoteURL = null;
this.localPath = null;
this.cacheControl = null;
}
FileCached(String lastETag, String remoteURL, String localPath, CacheControl cacheControl) {
this.lastETag = lastETag;
this.remoteURL = remoteURL;
this.localPath = localPath;
this.cacheControl = cacheControl;
}
@JsonIgnore
public boolean isChange() {
// Check if the file is expire
if (this.getExpiresTime().isAfter(LocalDateTime.now())) {
log.info("The file \"{}\" is no expire, the etag will not be checked. Expire time: {}", localPath, this.getExpiresTime().toString());
return false;
}
if (lastETag != null) {
// If are different means that the file is change
return !lastETag.equals(eTag);
}
return true;
}
// If are different means that the file is change
return !remoteETag.equals(localETag);
}
public Optional<File> getFileFromCache() {
if (Optional.ofNullable(cacheFilePath).isPresent() && Files.exists(cacheFilePath)) {
return Optional.of(new File(cacheFilePath.toUri()));
}
return Optional.empty();
}
public void fillCache(File fileToCache) throws Exception {
if (Optional.ofNullable(remoteETag).isPresent() &&
Optional.ofNullable(cacheFilePath).isPresent()) {
PreferencesData.set(preferencesDataKey, remoteETag);
// If the cache directory does not exist create it
if (!Files.exists(cacheFilePath.getParent())) {
Files.createDirectories(cacheFilePath.getParent());
@JsonIgnore
public boolean exists() throws IOException {
if (localPath != null && Files.exists(Paths.get(localPath))) {
try {
final String md5Local = FileHash.hash(Paths.get(localPath).toFile(), "MD5");
if (md5Local.equals(md5)) {
return true;
}
} catch (NoSuchAlgorithmException e) {
log.error("MD5 algorithm is not supported", e);
}
}
FileUtils.copyFile(fileToCache, cacheFilePath.toFile());
return false;
}
@JsonIgnore
public Optional<File> getFileFromCache() {
if (localPath != null && Files.exists(Paths.get(localPath))) {
return Optional.of(new File(localPath));
}
return Optional.empty();
}
public void updateCacheFile(File fileToCache) throws Exception {
if (Optional.ofNullable(lastETag).isPresent() && Optional
.ofNullable(localPath).isPresent()) {
Path cacheFilePath = Paths.get(localPath);
// If the cache directory does not exist create it
if (!Files.exists(cacheFilePath.getParent())) {
Files.createDirectories(cacheFilePath.getParent());
}
final File cacheFile = cacheFilePath.toFile();
FileUtils.copyFile(fileToCache, cacheFile);
eTag = lastETag;
createdAt = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
updateMD5();
}
log.info("Update cache file: {}", this);
cachedFiles.put(remoteURL, this);
updateCacheFilesInfo();
}
private void updateMD5() throws IOException, NoSuchAlgorithmException {
if (localPath != null) {
md5 = FileHash.hash(Paths.get(localPath).toFile(), "MD5");
}
}
@JsonIgnore
public LocalDateTime getExpiresTime() {
final int maxAge;
if (cacheControl != null) {
maxAge = cacheControl.getMaxAge();
} else {
maxAge = 0;
}
if (createdAt != null) {
return LocalDateTime.parse(createdAt, DateTimeFormatter.ISO_DATE_TIME)
.plusSeconds(maxAge);
}
return LocalDateTime.now();
}
public String getExpires() {
return getExpiresTime().toString();
}
public String getMD5() {
return md5;
}
public String geteTag() {
return eTag;
}
public String getRemoteURL() {
return remoteURL;
}
public String getLocalPath() {
return localPath;
}
public void setMd5(String md5) {
this.md5 = md5;
}
public String getCreatedAt() {
return createdAt;
}
public CacheControl getCacheControl() {
return cacheControl;
}
@Override
public String toString() {
return "FileCached{" +
"eTag='" + eTag + '\'' +
", lastETag='" + lastETag + '\'' +
", remoteURL='" + remoteURL + '\'' +
", localPath='" + localPath + '\'' +
", md5='" + md5 + '\'' +
", createdAt='" + createdAt + '\'' +
", cacheControl=" + cacheControl +
'}';
}
}
}