diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 4b1d0e6..cff5b0b 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -202,14 +202,18 @@ Future checkPartialDownloadHash(String url, int bytesToGrab, Future downloadFile( String url, String fileNameNoExt, Function? onProgress, String destDir, {bool useExisting = true, Map? 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)); - if (headers != null) { - req.headers.addAll(headers); - } + req.headers.addAll(reqHeaders); var client = http.Client(); StreamedResponse response = await client.send(req); - String ext = - response.headers['content-disposition']?.split('.').last ?? 'apk'; + var resHeaders = response.headers; + + // 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")) { ext = ext.substring(0, ext.length - 1); } @@ -217,41 +221,107 @@ Future downloadFile( ext = 'apk'; } File downloadedFile = File('$destDir/$fileNameNoExt.$ext'); - if (!(downloadedFile.existsSync() && useExisting)) { - File tempDownloadedFile = File('${downloadedFile.path}.part'); - if (tempDownloadedFile.existsSync()) { - tempDownloadedFile.deleteSync(recursive: true); - } - var length = response.contentLength; - var received = 0; - double? progress; - var sink = tempDownloadedFile.openWrite(); - await response.stream.map((s) { - received += s.length; - progress = (length != null ? received / length * 100 : 30); - if (onProgress != null) { - onProgress(progress); + + bool rangeFeatureEnabled = false; + if (resHeaders['accept-ranges']?.isNotEmpty == true) { + rangeFeatureEnabled = + resHeaders['accept-ranges']?.trim().toLowerCase() == 'bytes'; + } + + // If you have an existing file that is usable, + // decide whether you can use it (either return full or resume partial) + var fullContentLength = response.contentLength; + if (useExisting && downloadedFile.existsSync()) { + var length = downloadedFile.lengthSync(); + if (fullContentLength == null) { + // Assume full + client.close(); + return downloadedFile; + } else { + // Check if resume needed/possible + if (length == fullContentLength) { + client.close(); + return downloadedFile; } - return s; - }).pipe(sink); - await sink.close(); - progress = null; + if (length > fullContentLength) { + useExisting = false; + } + } + } + + // 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) { onProgress(progress); } - if (response.statusCode != 200) { - tempDownloadedFile.deleteSync(recursive: true); - throw response.reasonPhrase ?? tr('unexpectedError'); - } - if (tempDownloadedFile.existsSync()) { - tempDownloadedFile.renameSync(downloadedFile.path); - } - } else { - client.close(); + return s; + }).pipe(sink); + await sink.close(); + progress = null; + if (onProgress != null) { + onProgress(progress); } + 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; } +Future> getHeaders(String url, + {Map? 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 getInstalledInfo(String? packageName, {bool printErr = true}) async { if (packageName != null) {