Resume failed downloads when possible (#634)

This commit is contained in:
Imran Remtulla
2024-03-17 03:04:05 -04:00
parent 4495051813
commit 3943caeedb

View File

@@ -202,14 +202,18 @@ Future<String> checkPartialDownloadHash(String url, int bytesToGrab,
Future<File> downloadFile( Future<File> downloadFile(
String url, String fileNameNoExt, Function? onProgress, String destDir, String url, String fileNameNoExt, Function? onProgress, String destDir,
{bool useExisting = true, Map<String, String>? headers}) async { {bool useExisting = true, Map<String, String>? headers}) async {
// Send the initial request but cancel it as soon as you have the headers
var reqHeaders = headers ?? {};
var req = Request('GET', Uri.parse(url)); var req = Request('GET', Uri.parse(url));
if (headers != null) { req.headers.addAll(reqHeaders);
req.headers.addAll(headers);
}
var client = http.Client(); var client = http.Client();
StreamedResponse response = await client.send(req); StreamedResponse response = await client.send(req);
String ext = var resHeaders = response.headers;
response.headers['content-disposition']?.split('.').last ?? 'apk';
// Use the headers to decide what the file extension is, and
// whether it supports partial downloads (range request), and
// what the total size of the file is (if provided)
String ext = resHeaders['content-disposition']?.split('.').last ?? 'apk';
if (ext.endsWith('"') || ext.endsWith("other")) { if (ext.endsWith('"') || ext.endsWith("other")) {
ext = ext.substring(0, ext.length - 1); ext = ext.substring(0, ext.length - 1);
} }
@@ -217,41 +221,107 @@ Future<File> downloadFile(
ext = 'apk'; ext = 'apk';
} }
File downloadedFile = File('$destDir/$fileNameNoExt.$ext'); File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
if (!(downloadedFile.existsSync() && useExisting)) {
File tempDownloadedFile = File('${downloadedFile.path}.part'); bool rangeFeatureEnabled = false;
if (tempDownloadedFile.existsSync()) { if (resHeaders['accept-ranges']?.isNotEmpty == true) {
tempDownloadedFile.deleteSync(recursive: true); rangeFeatureEnabled =
} resHeaders['accept-ranges']?.trim().toLowerCase() == 'bytes';
var length = response.contentLength; }
var received = 0;
double? progress; // If you have an existing file that is usable,
var sink = tempDownloadedFile.openWrite(); // decide whether you can use it (either return full or resume partial)
await response.stream.map((s) { var fullContentLength = response.contentLength;
received += s.length; if (useExisting && downloadedFile.existsSync()) {
progress = (length != null ? received / length * 100 : 30); var length = downloadedFile.lengthSync();
if (onProgress != null) { if (fullContentLength == null) {
onProgress(progress); // Assume full
client.close();
return downloadedFile;
} else {
// Check if resume needed/possible
if (length == fullContentLength) {
client.close();
return downloadedFile;
} }
return s; if (length > fullContentLength) {
}).pipe(sink); useExisting = false;
await sink.close(); }
progress = null; }
}
// Download to a '.temp' file (to distinguish btn. complete/incomplete files)
File tempDownloadedFile = File('${downloadedFile.path}.part');
// If the range feature is not available (or you need to start a ranged req from 0),
// complete the already-started request, else cancel it and start a ranged request,
// and open the file for writing in the appropriate mode
var targetFileLength = useExisting && tempDownloadedFile.existsSync()
? tempDownloadedFile.lengthSync()
: null;
int rangeStart = targetFileLength ?? 0;
IOSink? sink;
if (rangeFeatureEnabled && fullContentLength != null && rangeStart > 0) {
client.close();
client = http.Client();
req = Request('GET', Uri.parse(url));
req.headers.addAll(reqHeaders);
req.headers.addAll({'range': 'bytes=$rangeStart-${fullContentLength - 1}'});
response = await client.send(req);
sink = tempDownloadedFile.openWrite(mode: FileMode.writeOnlyAppend);
} else if (tempDownloadedFile.existsSync()) {
tempDownloadedFile.deleteSync(recursive: true);
}
sink ??= tempDownloadedFile.openWrite(mode: FileMode.writeOnly);
// Perform the download
var received = 0;
double? progress;
if (rangeStart > 0 && fullContentLength != null) {
received = rangeStart;
}
await response.stream.map((s) {
received += s.length;
progress =
(fullContentLength != null ? (received / fullContentLength) * 100 : 30);
if (onProgress != null) { if (onProgress != null) {
onProgress(progress); onProgress(progress);
} }
if (response.statusCode != 200) { return s;
tempDownloadedFile.deleteSync(recursive: true); }).pipe(sink);
throw response.reasonPhrase ?? tr('unexpectedError'); await sink.close();
} progress = null;
if (tempDownloadedFile.existsSync()) { if (onProgress != null) {
tempDownloadedFile.renameSync(downloadedFile.path); onProgress(progress);
}
} else {
client.close();
} }
if (response.statusCode < 200 || response.statusCode > 299) {
tempDownloadedFile.deleteSync(recursive: true);
throw response.reasonPhrase ?? tr('unexpectedError');
}
print(tempDownloadedFile.lengthSync());
print(fullContentLength);
if (tempDownloadedFile.existsSync()) {
tempDownloadedFile.renameSync(downloadedFile.path);
}
client.close();
return downloadedFile; return downloadedFile;
} }
Future<Map<String, String>> getHeaders(String url,
{Map<String, String>? headers}) async {
var req = http.Request('GET', Uri.parse(url));
if (headers != null) {
req.headers.addAll(headers);
}
var client = http.Client();
var response = await client.send(req);
if (response.statusCode < 200 || response.statusCode > 299) {
throw ObtainiumError(response.reasonPhrase ?? tr('unexpectedError'));
}
var returnHeaders = response.headers;
client.close();
return returnHeaders;
}
Future<PackageInfo?> getInstalledInfo(String? packageName, Future<PackageInfo?> getInstalledInfo(String? packageName,
{bool printErr = true}) async { {bool printErr = true}) async {
if (packageName != null) { if (packageName != null) {