fix(update): check valid update files

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou 2026-04-29 23:15:03 +08:00
parent d4a1430c27
commit 4638e0f526
10 changed files with 1484 additions and 269 deletions

View file

@ -7,10 +7,10 @@ import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
final _isExtracting = false.obs;
const _eventKeyUpdateMe = 'update-me';
const _eventKeyUpdateMeReady = 'update-me-ready';
void handleUpdate(String releasePageUrl) {
_isExtracting.value = false;
String downloadUrl = releasePageUrl.replaceAll('tag', 'download');
String version = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1);
final String downloadFile =
@ -23,46 +23,112 @@ void handleUpdate(String releasePageUrl) {
}
downloadUrl = '$downloadUrl/$downloadFile';
SimpleWrapper downloadId = SimpleWrapper('');
SimpleWrapper<String> downloadId = SimpleWrapper('');
SimpleWrapper<VoidCallback> onCanceled = SimpleWrapper(() {});
SimpleWrapper<bool> pendingCancel = SimpleWrapper(false);
SimpleWrapper<Future<void> Function()> cancelDownload =
SimpleWrapper(() async {});
gFFI.dialogManager.dismissAll();
gFFI.dialogManager.show((setState, close, context) {
cancelDownload.value = () async {
final id = downloadId.value;
if (id.isEmpty) {
pendingCancel.value = true;
return;
}
pendingCancel.value = false;
onCanceled.value();
await bind.mainSetCommon(key: 'cancel-downloader', value: id);
// Wait for the downloader to be removed.
for (int i = 0; i < 10; i++) {
await Future.delayed(const Duration(milliseconds: 300));
final isCanceled = 'error:Downloader not found' ==
await bind.mainGetCommon(key: 'download-data-$id');
if (isCanceled) {
break;
}
}
close();
};
return CustomAlertDialog(
title: Obx(() => Text(translate(_isExtracting.isTrue
? 'Preparing for installation ...'
: 'Downloading {$appName}'))),
content:
UpdateProgress(releasePageUrl, downloadUrl, downloadId, onCanceled)
.marginSymmetric(horizontal: 8)
.paddingOnly(top: 12),
title: Text(translate('Downloading {$appName}')),
content: UpdateProgress(releasePageUrl, downloadUrl, downloadId,
onCanceled, pendingCancel, cancelDownload)
.marginSymmetric(horizontal: 8)
.paddingOnly(top: 12),
actions: [
if (_isExtracting.isFalse) dialogButton(translate('Cancel'), onPressed: () async {
onCanceled.value();
await bind.mainSetCommon(
key: 'cancel-downloader', value: downloadId.value);
// Wait for the downloader to be removed.
for (int i = 0; i < 10; i++) {
await Future.delayed(const Duration(milliseconds: 300));
final isCanceled = 'error:Downloader not found' ==
await bind.mainGetCommon(
key: 'download-data-${downloadId.value}');
if (isCanceled) {
break;
}
}
close();
dialogButton(translate('Cancel'), onPressed: () async {
await cancelDownload.value();
}, isOutline: true),
]);
});
}
void _showUpdateError(String releasePageUrl, String error,
{bool showRetry = true}) {
debugPrint('Update error: $error');
final dialogManager = gFFI.dialogManager;
jumplink() {
launchUrl(Uri.parse(releasePageUrl));
dialogManager.dismissAll();
}
retry() {
dialogManager.dismissAll();
handleUpdate(releasePageUrl);
}
dialogManager.dismissAll();
dialogManager.show(
(setState, close, context) => CustomAlertDialog(
title: null,
content: SelectionArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
msgboxContent('custom-nocancel-nook-hasclose', 'Error',
'download-new-version-failed-tip'),
const SizedBox(height: 8),
Text(error),
],
),
),
actions: [
dialogButton('Download', onPressed: jumplink),
if (showRetry) dialogButton('Retry', onPressed: retry),
dialogButton('Close', onPressed: close),
],
),
tag: 'custom-nocancel-nook-hasclose-Error-Error',
);
}
void _showPreparingForInstallation() {
gFFI.dialogManager.dismissAll();
gFFI.dialogManager.show(
(setState, close, context) => CustomAlertDialog(
title: Text(translate('Preparing for installation ...')),
content: const LinearProgressIndicator(
minHeight: 20,
borderRadius: BorderRadius.all(Radius.circular(5)),
).marginSymmetric(horizontal: 8).paddingOnly(top: 12),
actions: const [],
),
tag: 'preparing-for-installation',
);
}
class UpdateProgress extends StatefulWidget {
final String releasePageUrl;
final String downloadUrl;
final SimpleWrapper downloadId;
final SimpleWrapper onCanceled;
UpdateProgress(
this.releasePageUrl, this.downloadUrl, this.downloadId, this.onCanceled,
final SimpleWrapper<String> downloadId;
final SimpleWrapper<VoidCallback> onCanceled;
final SimpleWrapper<bool> pendingCancel;
final SimpleWrapper<Future<void> Function()> cancelDownload;
UpdateProgress(this.releasePageUrl, this.downloadUrl, this.downloadId,
this.onCanceled, this.pendingCancel, this.cancelDownload,
{Key? key})
: super(key: key);
@ -76,7 +142,6 @@ class UpdateProgressState extends State<UpdateProgress> {
int _downloadedSize = 0;
int _getDataFailedCount = 0;
final String _eventKeyDownloadNewVersion = 'download-new-version';
final String _eventKeyExtractUpdateDmg = 'extract-update-dmg';
@override
void initState() {
@ -88,11 +153,6 @@ class UpdateProgressState extends State<UpdateProgress> {
_eventKeyDownloadNewVersion, handleDownloadNewVersion,
replace: true);
bind.mainSetCommon(key: 'download-new-version', value: widget.downloadUrl);
if (isMacOS) {
platformFFI.registerEventHandler(_eventKeyExtractUpdateDmg,
_eventKeyExtractUpdateDmg, handleExtractUpdateDmg,
replace: true);
}
}
@override
@ -100,10 +160,6 @@ class UpdateProgressState extends State<UpdateProgress> {
cancelQueryTimer();
platformFFI.unregisterEventHandler(
_eventKeyDownloadNewVersion, _eventKeyDownloadNewVersion);
if (isMacOS) {
platformFFI.unregisterEventHandler(
_eventKeyExtractUpdateDmg, _eventKeyExtractUpdateDmg);
}
super.dispose();
}
@ -118,6 +174,9 @@ class UpdateProgressState extends State<UpdateProgress> {
_timer = Timer.periodic(const Duration(milliseconds: 300), (timer) {
_updateDownloadData();
});
if (widget.pendingCancel.value) {
await widget.cancelDownload.value();
}
} else {
if (evt.containsKey('error')) {
_onError(evt['error'] as String);
@ -128,47 +187,9 @@ class UpdateProgressState extends State<UpdateProgress> {
}
}
// `isExtractDmg` is true when handling extract-update-dmg event.
// It's a rare case that the dmg file is corrupted and cannot be extracted.
void _onError(String error, {bool isExtractDmg = false}) {
void _onError(String error) {
cancelQueryTimer();
debugPrint(
'${isExtractDmg ? "Extract" : "Download"} new version error: $error');
final msgBoxType = 'custom-nocancel-nook-hasclose';
final msgBoxTitle = 'Error';
final msgBoxText = 'download-new-version-failed-tip';
final dialogManager = gFFI.dialogManager;
close() {
dialogManager.dismissAll();
}
jumplink() {
launchUrl(Uri.parse(widget.releasePageUrl));
dialogManager.dismissAll();
}
retry() {
dialogManager.dismissAll();
handleUpdate(widget.releasePageUrl);
}
final List<Widget> buttons = [
dialogButton('Download', onPressed: jumplink),
if (!isExtractDmg) dialogButton('Retry', onPressed: retry),
dialogButton('Close', onPressed: close),
];
dialogManager.dismissAll();
dialogManager.show(
(setState, close, context) => CustomAlertDialog(
title: null,
content: SelectionArea(
child: msgboxContent(msgBoxType, msgBoxTitle, msgBoxText)),
actions: buttons,
),
tag: '$msgBoxType-$msgBoxTitle-$msgBoxTitle',
);
_showUpdateError(widget.releasePageUrl, error);
}
void _updateDownloadData() {
@ -212,13 +233,7 @@ class UpdateProgressState extends State<UpdateProgress> {
_onError('The download file size is 0.');
} else {
setState(() {});
if (isMacOS) {
bind.mainSetCommon(
key: 'extract-update-dmg', value: widget.downloadUrl);
_isExtracting.value = true;
} else {
updateMsgBox();
}
updateMsgBox();
}
} else {
setState(() {});
@ -236,28 +251,41 @@ class UpdateProgressState extends State<UpdateProgress> {
gFFI.dialogManager,
onSubmit: () {
debugPrint('Downloaded, update to new version now');
if (isMacOS) {
_showPreparingForInstallation();
platformFFI.registerEventHandler(
_eventKeyUpdateMeReady, _eventKeyUpdateMeReady, (evt) async {
platformFFI.unregisterEventHandler(
_eventKeyUpdateMeReady, _eventKeyUpdateMeReady);
gFFI.dialogManager.dismissAll();
}, replace: true);
}
platformFFI.registerEventHandler(_eventKeyUpdateMe, _eventKeyUpdateMe,
(evt) async {
platformFFI.unregisterEventHandler(
_eventKeyUpdateMe, _eventKeyUpdateMe);
if (isMacOS) {
platformFFI.unregisterEventHandler(
_eventKeyUpdateMeReady, _eventKeyUpdateMeReady);
}
if (evt.containsKey('error')) {
_showUpdateError(widget.releasePageUrl, evt['error'] as String,
showRetry: false);
}
}, replace: true);
bind.mainSetCommon(key: 'update-me', value: widget.downloadUrl);
},
submitTimeout: 5,
);
}
Future<void> handleExtractUpdateDmg(Map<String, dynamic> evt) async {
_isExtracting.value = false;
if (evt.containsKey('err') && (evt['err'] as String).isNotEmpty) {
_onError(evt['err'] as String, isExtractDmg: true);
} else {
updateMsgBox();
}
}
@override
Widget build(BuildContext context) {
getValue() => _totalSize == null
? 0.0
: (_totalSize == 0 ? 1.0 : _downloadedSize / _totalSize!);
return LinearProgressIndicator(
value: _isExtracting.isTrue ? null : getValue(),
value: getValue(),
minHeight: 20,
borderRadius: BorderRadius.circular(5),
backgroundColor: Colors.grey[300],