Add support for duplicate links

Support duplicate links [1] to handle better temporarily unavailable
mirrors.

[1] https://tools.ietf.org/html/rfc6249

Change-Id: If78fb4a90da68ef221294eed2c59063a14cf1f43
This commit is contained in:
Gabriele M
2018-01-22 22:10:55 +01:00
parent 3fc17494eb
commit 0e79b791f0
3 changed files with 112 additions and 3 deletions

View File

@@ -375,6 +375,7 @@ public class UpdaterController implements Controller {
.setDestination(update.getFile())
.setDownloadCallback(getDownloadCallback(downloadId))
.setProgressListener(getProgressListener(downloadId))
.setUseDuplicateLinks(true)
.build();
} catch (IOException exception) {
Log.e(TAG, "Could not build download client");
@@ -417,6 +418,7 @@ public class UpdaterController implements Controller {
.setDestination(update.getFile())
.setDownloadCallback(getDownloadCallback(downloadId))
.setProgressListener(getProgressListener(downloadId))
.setUseDuplicateLinks(true)
.build();
} catch (IOException exception) {
Log.e(TAG, "Could not build download client");

View File

@@ -63,6 +63,7 @@ public interface DownloadClient {
private File mDestination;
private DownloadClient.DownloadCallback mCallback;
private DownloadClient.ProgressListener mProgressListener;
private boolean mUseDuplicateLinks;
public DownloadClient build() throws IOException {
if (mUrl == null) {
@@ -72,7 +73,8 @@ public interface DownloadClient {
} else if (mCallback == null) {
throw new IllegalStateException("No download callback defined");
}
return new HttpURLConnectionClient(mUrl, mDestination, mProgressListener, mCallback);
return new HttpURLConnectionClient(mUrl, mDestination, mProgressListener, mCallback,
mUseDuplicateLinks);
}
public Builder setUrl(String url) {
@@ -94,5 +96,10 @@ public interface DownloadClient {
mProgressListener = progressListener;
return this;
}
public Builder setUseDuplicateLinks(boolean useDuplicateLinks) {
mUseDuplicateLinks = useDuplicateLinks;
return this;
}
}
}

View File

@@ -25,18 +25,23 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class HttpURLConnectionClient implements DownloadClient {
private final static String TAG = "HttpURLConnectionClient";
private final HttpURLConnection mClient;
private HttpURLConnection mClient;
private final File mDestination;
private final DownloadClient.ProgressListener mProgressListener;
private final DownloadClient.DownloadCallback mCallback;
private final boolean mUseDuplicateLinks;
private DownloadThread mDownloadThread;
@@ -54,11 +59,13 @@ public class HttpURLConnectionClient implements DownloadClient {
HttpURLConnectionClient(String url, File destination,
DownloadClient.ProgressListener progressListener,
DownloadClient.DownloadCallback callback) throws IOException {
DownloadClient.DownloadCallback callback,
boolean useDuplicateLinks) throws IOException {
mClient = (HttpURLConnection) new URL(url).openConnection();
mDestination = destination;
mProgressListener = progressListener;
mCallback = callback;
mUseDuplicateLinks = useDuplicateLinks;
}
@Override
@@ -113,6 +120,10 @@ public class HttpURLConnectionClient implements DownloadClient {
return (statusCode / 100) == 2;
}
private static boolean isRedirectCode(int statusCode) {
return (statusCode / 100) == 3;
}
private static boolean isPartialContentCode(int statusCode) {
return statusCode == 206;
}
@@ -155,11 +166,100 @@ public class HttpURLConnectionClient implements DownloadClient {
}
}
private void changeClientUrl(URL newUrl) throws IOException {
String range = mClient.getRequestProperty("Range");
mClient.disconnect();
mClient = (HttpURLConnection) newUrl.openConnection();
if (range != null) {
mClient.setRequestProperty("Range", range);
}
}
private void handleDuplicateLinks() throws IOException {
String protocol = mClient.getURL().getProtocol();
class DuplicateLink {
private String mUrl;
private int mPriority;
private DuplicateLink(String url, int priority) {
mUrl = url;
mPriority = priority;
}
}
PriorityQueue<DuplicateLink> duplicates = null;
for (Map.Entry<String, List<String>> entry : mClient.getHeaderFields().entrySet()) {
if ("Link".equalsIgnoreCase((entry.getKey()))) {
duplicates = new PriorityQueue<>(entry.getValue().size(),
new Comparator<DuplicateLink>() {
@Override
public int compare(DuplicateLink d1, DuplicateLink d2) {
return Integer.compare(d1.mPriority, d2.mPriority);
}
});
// https://tools.ietf.org/html/rfc6249
// https://tools.ietf.org/html/rfc5988#section-5
String regex = "(?i)<(.+)>\\s*;\\s*rel=duplicate(?:.*pri=([0-9]+).*|.*)?";
Pattern pattern = Pattern.compile(regex);
for (String field : entry.getValue()) {
Matcher matcher = pattern.matcher(field);
if (matcher.matches()) {
String url = matcher.group(1);
String pri = matcher.group(2);
int priority = pri != null ? Integer.parseInt(pri) : 999999;
duplicates.add(new DuplicateLink(url, priority));
Log.d(TAG, "Adding duplicate link " + url);
} else {
Log.d(TAG, "Ignoring link " + field);
}
}
}
}
String newUrl = mClient.getHeaderField("Location");
for (;;) {
try {
URL url = new URL(newUrl);
if (!url.getProtocol().equals(protocol)) {
// If we hadn't handled duplicate links, we wouldn't have
// used this url.
throw new IOException("Protocol changes are not allowed");
}
Log.d(TAG, "Downloading from " + newUrl);
changeClientUrl(url);
mClient.setConnectTimeout(5000);
mClient.connect();
if (!isSuccessCode(mClient.getResponseCode())) {
throw new IOException("Server replied with " + mClient.getResponseCode());
}
return;
} catch (IOException e) {
if (duplicates != null && !duplicates.isEmpty()) {
DuplicateLink link = duplicates.poll();
duplicates.remove(link);
newUrl = link.mUrl;
Log.e(TAG, "Using duplicate link " + link.mUrl, e);
} else {
throw e;
}
}
}
}
@Override
public void run() {
try {
mClient.setInstanceFollowRedirects(!mUseDuplicateLinks);
mClient.connect();
int responseCode = mClient.getResponseCode();
if (mUseDuplicateLinks && isRedirectCode(responseCode)) {
handleDuplicateLinks();
responseCode = mClient.getResponseCode();
}
mCallback.onResponse(responseCode, mClient.getURL().toString(), new Headers());
if (mResume && isPartialContentCode(responseCode)) {