Skip to content

Commit

Permalink
Use detached plugins as a cache for the Update Center (#9476)
Browse files Browse the repository at this point in the history
  • Loading branch information
basil authored Jul 30, 2024
1 parent 6f23dea commit ca241b4
Showing 1 changed file with 156 additions and 1 deletion.
157 changes: 156 additions & 1 deletion core/src/main/java/hudson/model/UpdateCenter.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,18 @@
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;
Expand All @@ -86,6 +90,7 @@
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;
Expand Down Expand Up @@ -1809,6 +1814,83 @@ public void run() {
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.
*/
Expand Down Expand Up @@ -2245,7 +2327,24 @@ public void _run() throws IOException, InstallationStatus {
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();
Expand Down Expand Up @@ -2278,6 +2377,62 @@ public void _run() throws IOException, InstallationStatus {
}
}

/**
* 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
Expand Down

0 comments on commit ca241b4

Please sign in to comment.