From ae6fb109e5a44a0ee36f466f6d9ee373800d2ec5 Mon Sep 17 00:00:00 2001 From: Henry Le Grys Date: Wed, 10 Mar 2021 00:01:16 +0000 Subject: [PATCH] Add support for continuing partial downloads If the server offers byte-range requests & the request fails, we retry with a Range header to continue the download where it was interrupted. --- .../launcher/install/HttpDownloader.java | 9 +++- .../skcraft/launcher/util/HttpRequest.java | 42 ++++++++++++++++--- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/launcher/src/main/java/com/skcraft/launcher/install/HttpDownloader.java b/launcher/src/main/java/com/skcraft/launcher/install/HttpDownloader.java index 32ba558..9832169 100644 --- a/launcher/src/main/java/com/skcraft/launcher/install/HttpDownloader.java +++ b/launcher/src/main/java/com/skcraft/launcher/install/HttpDownloader.java @@ -238,6 +238,7 @@ public class HttpDownloader implements Downloader { int trial = 0; boolean first = true; IOException lastException = null; + HttpRequest.PartialDownloadInfo retryDetails = null; do { for (URL url : urls) { @@ -249,11 +250,16 @@ public class HttpDownloader implements Downloader { try { request = HttpRequest.get(url); - request.execute().expectResponseCode(200).saveContent(file); + request.setResumeInfo(retryDetails).execute().expectResponseCode(200).saveContent(file); return; } catch (IOException e) { lastException = e; log.log(Level.WARNING, "Failed to download " + url, e); + + Optional byteRangeSupport = request.canRetryPartial(); + if (byteRangeSupport.isPresent()) { + retryDetails = byteRangeSupport.get(); + } } } } while (++trial < tryCount); @@ -277,5 +283,4 @@ public class HttpDownloader implements Downloader { } } } - } diff --git a/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java b/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java index 0823669..43d4f37 100644 --- a/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java +++ b/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java @@ -9,6 +9,7 @@ package com.skcraft.launcher.util; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.skcraft.concurrency.ProgressObservable; +import lombok.Data; import lombok.Getter; import lombok.extern.java.Log; @@ -17,10 +18,7 @@ import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import java.io.*; import java.net.*; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static com.skcraft.launcher.LauncherUtils.checkInterrupted; import static org.apache.commons.io.IOUtils.closeQuietly; @@ -46,6 +44,7 @@ public class HttpRequest implements Closeable, ProgressObservable { private InputStream inputStream; private int redirectCount; + private PartialDownloadInfo resumeInfo = null; private long contentLength = -1; private long readBytes = 0; @@ -143,6 +142,10 @@ public class HttpRequest implements Closeable, ProgressObservable { conn.setDoInput(true); } + if (resumeInfo != null) { + conn.setRequestProperty("Range", String.format("bytes=%d-", resumeInfo.currentLength)); + } + for (Map.Entry entry : headers.entrySet()) { conn.setRequestProperty(entry.getKey(), entry.getValue()); } @@ -198,6 +201,11 @@ public class HttpRequest implements Closeable, ProgressObservable { } } + if (resumeInfo != null && responseCode == 206) { + // Allow 206 Partial Content for resumed requests + return this; + } + close(); throw new IOException("Did not get expected response code, got " + responseCode + " for " + url); } @@ -284,7 +292,7 @@ public class HttpRequest implements Closeable, ProgressObservable { BufferedOutputStream bos = null; try { - fos = new FileOutputStream(file); + fos = new FileOutputStream(file, resumeInfo != null); bos = new BufferedOutputStream(fos); saveContent(bos); @@ -340,6 +348,24 @@ public class HttpRequest implements Closeable, ProgressObservable { return this; } + public Optional canRetryPartial() { + if (conn.getHeaderField("Accept-Ranges").equals("bytes")) { + return Optional.of(new PartialDownloadInfo(contentLength, readBytes)); + } + + return Optional.empty(); + } + + public HttpRequest setResumeInfo(PartialDownloadInfo info) { + this.resumeInfo = info; + + return this; + } + + public boolean isResumedRequest() { + return resumeInfo != null; + } + @Override public double getProgress() { if (contentLength >= 0) { @@ -594,4 +620,10 @@ public class HttpRequest implements Closeable, ProgressObservable { } } + @Data + public static class PartialDownloadInfo { + private final long expectedLength; + private final long currentLength; + } + }