Use detached plugins as a cache for the Update Center (#9476)

This commit is contained in:
Basil Crow authored and GitHub committed 2024-07-29 18:57:31 -06:00
1 parent 6f23dea734
commit ca241b4bf6
1 file changed
+156 -1
@@ -58,14 +58,18 @@ import hudson.util.NamingThreadFactory;
import hudson.util.PersistedList;
import hudson.util.VersionNumber;
import hudson.util.XStream2;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.lang.reflect.Constructor;
import java.net.HttpRetryException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
@@ -86,6 +90,7 @@ import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
@@ -1809,6 +1814,83 @@ public class UpdateCenter extends AbstractModelObject implements Loadable, Savea
String getComputedSHA512();
}
@SuppressFBWarnings(value = "WEAK_MESSAGE_DIGEST_SHA1", justification = "SHA-1 is only used as a fallback if SHA-256/SHA-512 are not available")
private static class FileWithComputedChecksums implements WithComputedChecksums {
private final File file;
private String computedSHA1;
private String computedSHA256;
private String computedSHA512;
FileWithComputedChecksums(File file) {
this.file = Objects.requireNonNull(file);
}
@Override
public synchronized String getComputedSHA1() {
if (computedSHA1 != null) {
return computedSHA1;
}
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException(e);
}
computedSHA1 = computeDigest(messageDigest);
return computedSHA1;
}
@Override
public synchronized String getComputedSHA256() {
if (computedSHA256 != null) {
return computedSHA256;
}
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException(e);
}
computedSHA256 = computeDigest(messageDigest);
return computedSHA256;
}
@Override
public synchronized String getComputedSHA512() {
if (computedSHA512 != null) {
return computedSHA512;
}
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance("SHA-512");
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException(e);
}
computedSHA512 = computeDigest(messageDigest);
return computedSHA512;
}
private String computeDigest(MessageDigest digest) {
try (InputStream is = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(is)) {
byte[] buffer = new byte[1024];
int read = bis.read(buffer, 0, buffer.length);
while (read > -1) {
digest.update(buffer, 0, read);
read = bis.read(buffer, 0, buffer.length);
}
return Base64.getEncoder().encodeToString(digest.digest());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
/**
* Base class for a job that downloads a file from the Jenkins project.
*/
@@ -2245,7 +2327,24 @@ public class UpdateCenter extends AbstractModelObject implements Loadable, Savea
return;
}
try {
super._run();
File cached = getCached(this);
if (cached != null) {
File dst = getDestination();
// A bit naive, but following the corresponding logic in UpdateCenterConfiguration#download...
File tmp = new File(dst.getPath() + ".tmp");
Files.copy(cached.toPath(), tmp.toPath(), StandardCopyOption.REPLACE_EXISTING);
config.postValidate(this, tmp);
/*
* Will unfortunately validate the checksum a second time, but this should still be faster than
* network I/O and at least allows us to reuse code...
*/
config.install(this, tmp, dst);
} else {
super._run();
}
// if this is a bundled plugin, make sure it won't get overwritten
PluginWrapper pw = plugin.getInstalled();
@@ -2278,6 +2377,62 @@ public class UpdateCenter extends AbstractModelObject implements Loadable, Savea
}
}
/**
* If we happen to have the file already in the {@coode WEB-INF/detached-plugins} directory and it happens to
* match the checksum we were expecting, then save ourselves a trip to the download site. This method is
* best-effort, and if anything goes wrong we simply fall back to the standard download path.
*
* @return The cached file, or null for a cache miss
*/
@CheckForNull
private File getCached(DownloadJob job) {
URL src;
try {
/*
* Could make PluginManager#getDetachedLocation public and consume it here, but this method is
* best-effort anyway.
*/
src = Jenkins.get().servletContext.getResource(String.format("/WEB-INF/detached-plugins/%s.hpi", plugin.name));
} catch (MalformedURLException e) {
return null;
}
if (src == null || !"file".equals(src.getProtocol())) {
return null;
}
try {
config.preValidate(this, src);
} catch (IOException e) {
return null;
}
File cached;
try {
cached = new File(src.toURI());
} catch (URISyntaxException e) {
return null;
}
if (!cached.isFile()) {
return null;
}
WithComputedChecksums withComputedChecksums = new FileWithComputedChecksums(cached);
try {
verifyChecksums(withComputedChecksums, plugin, cached);
} catch (IOException | UncheckedIOException | UnsupportedOperationException e) {
return null;
}
// Allow us to reuse UpdateCenter.InstallationJob#replace.
job.computedSHA1 = withComputedChecksums.getComputedSHA1();
job.computedSHA256 = withComputedChecksums.getComputedSHA256();
job.computedSHA512 = withComputedChecksums.getComputedSHA512();
return cached;
}
/**
* Indicates there is another installation job for this plugin
* @since 2.1