mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-10 12:26:16 -04:00
feat: WIP
This commit is contained in:
parent
697ec45620
commit
46d502a5fd
@ -10,50 +10,68 @@ import java.nio.file.Path;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
|
|
||||||
class DownloadManager {
|
class DownloadManager {
|
||||||
|
|
||||||
static DownloadState ne(int contentLength, int initialParts) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static DownloadTaskState download(List<String> urls, Path file, int initialParts) throws IOException {
|
static DownloadTaskState download(List<String> urls, Path file, int initialParts) throws IOException {
|
||||||
Path downloadingFile = file.resolveSibling(FileUtils.getName(file) + ".download");
|
Path downloadingFile = file.resolveSibling(FileUtils.getName(file) + ".download");
|
||||||
Path stateFile = file.resolveSibling(FileUtils.getName(file) + ".status");
|
Path stateFile = file.resolveSibling(FileUtils.getName(file) + ".status");
|
||||||
DownloadState state;
|
DownloadState state = null;
|
||||||
if (Files.exists(downloadingFile) && Files.exists(stateFile)) {
|
if (Files.exists(downloadingFile) && Files.exists(stateFile)) {
|
||||||
// Resume downloading from state
|
// Resume downloading from state
|
||||||
try {
|
try {
|
||||||
String status = FileUtils.readText(stateFile);
|
String status = FileUtils.readText(stateFile);
|
||||||
state = JsonUtils.fromNonNullJson(status, DownloadState.class);
|
state = JsonUtils.fromNonNullJson(status, DownloadState.class);
|
||||||
} catch (JsonParseException e) {
|
} catch (JsonParseException e) {
|
||||||
state =
|
LOG.log(Level.WARNING, "Failed to parse download state file", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state == null || !urls.equals(state.urls)) {
|
||||||
|
return DownloadTaskState.newWithLengthUnknown(urls, initialParts);
|
||||||
|
} else {
|
||||||
|
return new DownloadTaskState(state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static class DownloadTaskState {
|
protected static class DownloadTaskState {
|
||||||
private final List<String> urls;
|
private final List<String> urls;
|
||||||
private final List<DownloadSegment> segments;
|
private final List<DownloadSegment> segments;
|
||||||
|
private final List<Thread> threads;
|
||||||
|
private String fastestUrl;
|
||||||
|
private int retry = 0;
|
||||||
|
private boolean cancelled = false;
|
||||||
|
|
||||||
DownloadTaskState(DownloadState state) {
|
DownloadTaskState(DownloadState state) {
|
||||||
urls = new ArrayList<>(state.urls);
|
urls = new ArrayList<>(state.urls);
|
||||||
segments = new ArrayList<>(state.segments);
|
segments = new ArrayList<>(state.segments);
|
||||||
|
threads = IntStream.range(0, state.segments.size()).mapToObj(x -> (Thread) null).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadTaskState(List<String> urls, int contentLength, int initialParts) {
|
DownloadTaskState(List<String> urls, int contentLength, int initialParts) {
|
||||||
urls = new ArrayList<>(urls);
|
if (urls == null || urls.size() == 0) {
|
||||||
|
throw new IllegalArgumentException("DownloadTaskState requires at least one url candidate");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.urls = new ArrayList<>(urls);
|
||||||
segments = new ArrayList<>(initialParts);
|
segments = new ArrayList<>(initialParts);
|
||||||
|
threads = new ArrayList<>(initialParts);
|
||||||
int partLength = contentLength / initialParts;
|
int partLength = contentLength / initialParts;
|
||||||
for (int i = 0; i < initialParts; i++) {
|
for (int i = 0; i < initialParts; i++) {
|
||||||
int begin = partLength * i;
|
int begin = partLength * i;
|
||||||
int end = Math.min((partLength + 1) * i, contentLength);
|
int end = Math.min((partLength + 1) * i, contentLength);
|
||||||
segments.add(new DownloadSegment(begin, end, 0));
|
segments.add(new DownloadSegment(begin, end, 0));
|
||||||
|
threads.add(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DownloadTaskState newWithLengthUnknown(List<String> urls, int initialParts) {
|
public static DownloadTaskState newWithLengthUnknown(List<String> urls, int initialParts) {
|
||||||
return
|
return new DownloadTaskState(urls, 0, initialParts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getUrls() {
|
public List<String> getUrls() {
|
||||||
@ -63,6 +81,52 @@ class DownloadManager {
|
|||||||
public List<DownloadSegment> getSegments() {
|
public List<DownloadSegment> getSegments() {
|
||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFirstUrl() {
|
||||||
|
return urls.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Next url for download runnable to retry.
|
||||||
|
*
|
||||||
|
* If some download runnable fails to connect to url, it will call this method
|
||||||
|
* to acquire next url for retry. Making all download runnable try different
|
||||||
|
* candidates concurrently to speed up finding fastest download source.
|
||||||
|
*
|
||||||
|
* @return next url to retry
|
||||||
|
*/
|
||||||
|
public synchronized String getNextUrlToRetry() {
|
||||||
|
String url = urls.get(retry);
|
||||||
|
retry = (retry + 1) % urls.size();
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One candidate that is accessible and best network connection qualified.
|
||||||
|
*
|
||||||
|
* When some download runnable have started downloading, DownloadManager will
|
||||||
|
* monitor download speed and make failed download runnable connect to the
|
||||||
|
* fastest url directly without retry.
|
||||||
|
*
|
||||||
|
* In some times, the fastest url may be the first url suceeded in connection.
|
||||||
|
*
|
||||||
|
* @return fastest url, null if no url have successfully connected yet.
|
||||||
|
*/
|
||||||
|
public synchronized String getFastestUrl() {
|
||||||
|
return fastestUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setFastestUrl(String fastestUrl) {
|
||||||
|
this.fastestUrl = fastestUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void cancel() {
|
||||||
|
cancelled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isCancelled() {
|
||||||
|
return cancelled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static class DownloadState {
|
protected static class DownloadState {
|
||||||
|
@ -1,5 +1,156 @@
|
|||||||
package org.jackhuang.hmcl.task;
|
package org.jackhuang.hmcl.task;
|
||||||
|
|
||||||
class DownloadTask {
|
import org.jackhuang.hmcl.util.CacheRepository;
|
||||||
|
import org.jackhuang.hmcl.util.Logging;
|
||||||
|
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||||
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
|
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
abstract class DownloadTask implements Runnable {
|
||||||
|
|
||||||
|
private final DownloadManager.DownloadTaskState state;
|
||||||
|
private final RandomAccessFile file;
|
||||||
|
private URLConnection conn;
|
||||||
|
private DownloadManager.DownloadSegment segment;
|
||||||
|
protected boolean caching;
|
||||||
|
protected CacheRepository repository = CacheRepository.getInstance();
|
||||||
|
|
||||||
|
public DownloadTask(DownloadManager.DownloadTaskState state, RandomAccessFile file, DownloadManager.DownloadSegment segment) {
|
||||||
|
this.state = state;
|
||||||
|
this.file = file;
|
||||||
|
this.segment = segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCaching(boolean caching) {
|
||||||
|
this.caching = caching;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCacheRepository(CacheRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void beforeDownload(URL url) throws IOException {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void useCachedResult(Path cachedFile) throws IOException;
|
||||||
|
|
||||||
|
protected abstract FetchTask.EnumCheckETag shouldCheckETag();
|
||||||
|
|
||||||
|
protected abstract FetchTask.Context getContext(URLConnection conn, boolean checkETag) throws IOException;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Exception exception = null;
|
||||||
|
URL failedURL = null;
|
||||||
|
boolean checkETag;
|
||||||
|
switch (shouldCheckETag()) {
|
||||||
|
case CHECK_E_TAG:
|
||||||
|
checkETag = true;
|
||||||
|
break;
|
||||||
|
case NOT_CHECK_E_TAG:
|
||||||
|
checkETag = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int repeat = 0;
|
||||||
|
while (true) {
|
||||||
|
if (state.isCancelled()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = repeat == 0 ? state.getFirstUrl() : state.getNextUrlToRetry();
|
||||||
|
repeat++;
|
||||||
|
|
||||||
|
if (url == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
beforeDownload(url);
|
||||||
|
|
||||||
|
updateProgress(0);
|
||||||
|
|
||||||
|
URLConnection conn = NetworkUtils.createConnection(NetworkUtils.toURL(url);
|
||||||
|
if (checkETag) repository.injectConnection(conn);
|
||||||
|
|
||||||
|
if (conn instanceof HttpURLConnection) {
|
||||||
|
conn = NetworkUtils.resolveConnection((HttpURLConnection) conn);
|
||||||
|
int responseCode = ((HttpURLConnection) conn).getResponseCode();
|
||||||
|
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||||
|
// Handle cache
|
||||||
|
try {
|
||||||
|
Path cache = repository.getCachedRemoteFile(conn);
|
||||||
|
useCachedResult(cache);
|
||||||
|
return;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Logging.LOG.log(Level.WARNING, "Unable to use cached file, redownload " + url, e);
|
||||||
|
repository.removeRemoteEntry(conn);
|
||||||
|
// Now we must reconnect the server since 304 may result in empty content,
|
||||||
|
// if we want to redownload the file, we must reconnect the server without etag settings.
|
||||||
|
repeat--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (responseCode / 100 == 4) {
|
||||||
|
break; // we will not try this URL again
|
||||||
|
} else if (responseCode / 100 != 2) {
|
||||||
|
throw new ResponseCodeException(url, responseCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long contentLength = conn.getContentLength();
|
||||||
|
try (FetchTask.Context context = getContext(conn, checkETag); InputStream stream = conn.getInputStream()) {
|
||||||
|
int lastDownloaded = 0, downloaded = 0;
|
||||||
|
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||||
|
while (true) {
|
||||||
|
if (state.isCancelled()) break;
|
||||||
|
|
||||||
|
int len = stream.read(buffer);
|
||||||
|
if (len == -1) break;
|
||||||
|
|
||||||
|
context.write(buffer, 0, len);
|
||||||
|
|
||||||
|
downloaded += len;
|
||||||
|
|
||||||
|
if (contentLength >= 0) {
|
||||||
|
// Update progress information per second
|
||||||
|
updateProgress(downloaded, contentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDownloadSpeed(downloaded - lastDownloaded);
|
||||||
|
lastDownloaded = downloaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isCancelled()) break;
|
||||||
|
|
||||||
|
updateDownloadSpeed(downloaded - lastDownloaded);
|
||||||
|
|
||||||
|
if (contentLength >= 0 && downloaded != contentLength)
|
||||||
|
throw new IOException("Unexpected file size: " + downloaded + ", expected: " + contentLength);
|
||||||
|
|
||||||
|
context.withResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (IOException ex) {
|
||||||
|
failedURL = url;
|
||||||
|
exception = ex;
|
||||||
|
Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", repeat times: " + repeat, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exception != null)
|
||||||
|
throw new DownloadException(failedURL, exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ public final class GetTask extends FetchTask<String> {
|
|||||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(byte[] buffer, int offset, int len) {
|
public synchronized void write(byte[] buffer, int offset, int len) {
|
||||||
baos.write(buffer, offset, len);
|
baos.write(buffer, offset, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user