mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-08-11 17:40:15 +02:00
Resume failed downloads when possible (#634)
This commit is contained in:
@@ -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) {
|
||||||
|
Reference in New Issue
Block a user