import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:path/path.dart' as path; import 'package:reboot_common/common.dart'; import 'package:reboot_common/src/extension/types.dart'; const String kStopBuildDownloadSignal = "kill"; final Dio _dio = _buildDioInstance(); Dio _buildDioInstance() { final dio = Dio(); final httpClientAdapter = dio.httpClientAdapter as IOHttpClientAdapter; httpClientAdapter.createHttpClient = () { final client = HttpClient(); client.badCertificateCallback = (X509Certificate cert, String host, int port) => true; return client; }; return dio; } final String _archiveSourceUrl = "http://185.203.216.3/versions.json"; final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$"); const String _deniedConnectionError = "The connection was denied: your firewall might be blocking the download"; const String _unavailableError = "The build downloader is not available right now"; const String _genericError = "The build downloader is not working correctly"; const int _maxErrors = 100; Future> fetchBuilds(ignored) async { final response = await _dio.get( _archiveSourceUrl, options: Options( responseType: ResponseType.plain ) ); if (response.statusCode != 200) { return []; } final data = jsonDecode(response.data ?? "{}"); var results = []; for(final entry in data.entries) { results.add(FortniteBuild( identifier: entry.key, version: "${entry.value["title"]} (${entry.key})", link: entry.value["url"] )); } return results; } Future downloadArchiveBuild(FortniteBuildDownloadOptions options) async { try { final stopped = _setupLifecycle(options); final outputDir = Directory("${options.destination.path}\\.build"); await outputDir.create(recursive: true); final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1); final extension = path.extension(fileName); final tempFile = File("${outputDir.path}\\$fileName"); if(await tempFile.exists()) { await tempFile.delete(recursive: true); } final startTime = DateTime.now().millisecondsSinceEpoch; final response = _downloadArchive(options, tempFile, startTime); await Future.any([stopped.future, response]); if(!stopped.isCompleted) { await _extractArchive(stopped, extension, tempFile, options); } delete(outputDir); }catch(error) { _onError(error, options); } } Future _downloadArchive(FortniteBuildDownloadOptions options, File tempFile, int startTime, [int? byteStart = null, int errorsCount = 0]) async { var received = byteStart ?? 0; try { await _dio.download( options.build.link, tempFile.path, onReceiveProgress: (data, length) { received = data; final percentage = (received / length) * 100; _onProgress(startTime, percentage < 1 ? null : DateTime.now().millisecondsSinceEpoch, percentage, false, options); }, deleteOnError: false, options: Options( validateStatus: (statusCode) { if(statusCode == 200) { return true; } if(statusCode == 403 || statusCode == 503) { throw _deniedConnectionError; } if(statusCode == 404) { throw _unavailableError; } throw _genericError; }, headers: byteStart == null || byteStart <= 0 ? { "Cookie": "_c_t_c=1" } : { "Cookie": "_c_t_c=1", "Range": "bytes=${byteStart}-" }, ) ); }catch(error) { if(errorsCount > _maxErrors || error.toString().contains(_deniedConnectionError) || error.toString().contains(_unavailableError)) { _onError(error, options); return; } await _downloadArchive(options, tempFile, startTime, received, errorsCount + 1); } } Future _extractArchive(Completer stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async { final startTime = DateTime.now().millisecondsSinceEpoch; Process? process; switch (extension.toLowerCase()) { case ".zip": final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe"); if(!sevenZip.existsSync()) { throw "Corrupted installation: missing 7zip.exe"; } process = await startProcess( executable: sevenZip, args: [ "x", "-bsp1", '-o"${options.destination.path}"', "-y", '"${tempFile.path}"' ], ); var completed = false; process.stdOutput.listen((data) { final now = DateTime.now().millisecondsSinceEpoch; if(data.toLowerCase().contains("everything is ok")) { completed = true; _onProgress(startTime, now, 100, true, options); process?.kill(ProcessSignal.sigabrt); return; } final element = data.trim().split(" ")[0]; if(!element.endsWith("%")) { return; } final percentage = int.parse(element.substring(0, element.length - 1)).toDouble(); _onProgress(startTime, now, percentage, true, options); }); process.stdError.listen((data) { if(!data.isBlank) { _onError(data, options); } }); process.exitCode.then((_) { if(!completed) { _onError("Corrupted zip archive", options); } }); break; case ".rar": final winrar = File("${assetsDirectory.path}\\build\\winrar.exe"); if(!winrar.existsSync()) { throw "Corrupted installation: missing winrar.exe"; } process = await startProcess( executable: winrar, args: [ "x", "-o+", '"${tempFile.path}"', "*.*", '"${options.destination.path}"' ] ); var completed = false; process.stdOutput.listen((data) { final now = DateTime.now().millisecondsSinceEpoch; data = data.replaceAll("\r", "").replaceAll("\b", "").trim(); if(data == "All OK") { completed = true; _onProgress(startTime, now, 100, true, options); process?.kill(ProcessSignal.sigabrt); return; } final element = _rarProgressRegex.firstMatch(data)?.group(1); if(element == null) { return; } final percentage = int.parse(element).toDouble(); _onProgress(startTime, now, percentage, true, options); }); process.stdError.listen((data) { if(!data.isBlank) { _onError(data, options); } }); process.exitCode.then((_) { if(!completed) { _onError("Corrupted rar archive", options); } }); break; default: throw ArgumentError("Unexpected file extension: $extension}"); } await Future.any([stopped.future, process.exitCode]); } void _onProgress(int startTime, int? now, double percentage, bool extracting, FortniteBuildDownloadOptions options) { if(percentage == 0) { options.port.send(FortniteBuildDownloadProgress( progress: percentage, extracting: extracting )); return; } final msLeft = now == null ? null : startTime + (now - startTime) * 100 / percentage - now; final minutesLeft = msLeft == null ? null : (msLeft / 1000 / 60).round(); options.port.send(FortniteBuildDownloadProgress( progress: percentage, extracting: extracting, minutesLeft: minutesLeft )); } void _onError(Object? error, FortniteBuildDownloadOptions options) { if(error != null) { options.port.send(error.toString()); } } Completer _setupLifecycle(FortniteBuildDownloadOptions options) { var stopped = Completer(); var lifecyclePort = ReceivePort(); lifecyclePort.listen((message) { if(message == kStopBuildDownloadSignal && !stopped.isCompleted) { stopped.complete(); } }); options.port.send(lifecyclePort.sendPort); return stopped; }