mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-15 04:43:41 +00:00
feat: auto updater (#933)
This commit is contained in:
30 files changed
+1209
-245
No files matched your search
@@ -0,0 +1,15 @@
|
||||
import 'arch_stub.dart'
|
||||
if (dart.library.io) 'entry/arch_native.dart'
|
||||
if (dart.library.html) 'entry/arch_web.dart';
|
||||
|
||||
// Copy from pkg/sky_engine/lib/ffi/abi.dart
|
||||
enum Architecture {
|
||||
arm,
|
||||
arm64,
|
||||
ia32,
|
||||
x64,
|
||||
riscv32,
|
||||
riscv64,
|
||||
}
|
||||
|
||||
Architecture getArch() => doGetArch();
|
||||
@@ -0,0 +1,3 @@
|
||||
import 'arch.dart';
|
||||
|
||||
Architecture doGetArch() => throw UnimplementedError();
|
||||
@@ -0,0 +1,13 @@
|
||||
// ignore: avoid_web_libraries_in_flutter
|
||||
import 'dart:ffi';
|
||||
|
||||
import '../arch.dart';
|
||||
|
||||
Architecture doGetArch() {
|
||||
final currentAbi = Abi.current().toString();
|
||||
final archName = currentAbi.split("_")[1];
|
||||
final arch = Architecture.values.firstWhere(
|
||||
(element) => element.name == archName,
|
||||
orElse: () => Architecture.x64);
|
||||
return arch;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import '../arch.dart';
|
||||
|
||||
Architecture doGetArch() {
|
||||
return Architecture.x64;
|
||||
}
|
||||
+6
-35
@@ -1,10 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:win32_registry/win32_registry.dart';
|
||||
|
||||
import '../../util.dart';
|
||||
import '../../win32.dart';
|
||||
import '../browser_extension_host.dart';
|
||||
|
||||
@@ -26,32 +26,9 @@ const _firefoxNativeHostsKey = r'Software\Mozilla\NativeMessagingHosts';
|
||||
|
||||
/// Install host binary for browser extension
|
||||
Future<void> doInstallHost() async {
|
||||
final hostPath = _joinExePath(_hostExecName);
|
||||
|
||||
Future<List<int>> getHostAssetData() async {
|
||||
final hostAsset = await rootBundle.load('assets/host/$_hostExecName');
|
||||
return hostAsset.buffer
|
||||
.asUint8List(hostAsset.offsetInBytes, hostAsset.lengthInBytes);
|
||||
}
|
||||
|
||||
// Check if host binary is not installed
|
||||
if (!await File(hostPath).exists()) {
|
||||
final hostData = await getHostAssetData();
|
||||
final file = File(hostPath);
|
||||
await file.writeAsBytes(hostData);
|
||||
// Add execute permission
|
||||
if (!Platform.isWindows) {
|
||||
await Process.run('chmod', ['+x', hostPath]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Check if host binary is outdated
|
||||
final hostAssetData = await getHostAssetData();
|
||||
final hostFileData = await File(hostPath).readAsBytes();
|
||||
if (hostAssetData.length != hostFileData.length) {
|
||||
await File(hostPath).writeAsBytes(hostAssetData);
|
||||
return;
|
||||
}
|
||||
final hostPath = Util.homePathJoin(_hostExecName);
|
||||
await Util.installAsset('assets/exec/$_hostExecName', hostPath,
|
||||
executable: true);
|
||||
}
|
||||
|
||||
/// Check if specified browser is installed
|
||||
@@ -236,7 +213,7 @@ String? _getManifestPath(Browser browser) {
|
||||
final manifestName =
|
||||
browser == Browser.firefox ? '$_hostName.moz.json' : '$_hostName.json';
|
||||
if (Platform.isWindows) {
|
||||
return _joinExePath(manifestName);
|
||||
return Util.homePathJoin(manifestName);
|
||||
}
|
||||
|
||||
final home =
|
||||
@@ -282,7 +259,7 @@ Future<bool> _checkWindowsRegistry(String keyPath) async {
|
||||
}
|
||||
|
||||
Future<String> _getManifestContent(Browser browser) async {
|
||||
final hostPath = _joinExePath(_hostExecName);
|
||||
final hostPath = Util.homePathJoin(_hostExecName);
|
||||
final manifest = {
|
||||
'name': _hostName,
|
||||
'description': 'Gopeed browser extension host',
|
||||
@@ -309,9 +286,3 @@ String _getWindowsRegistryKey(Browser browser) {
|
||||
return _firefoxNativeHostsKey;
|
||||
}
|
||||
}
|
||||
|
||||
String _joinExePath(String fileName) {
|
||||
final execPath = Platform.resolvedExecutable;
|
||||
final execDir = path.dirname(execPath);
|
||||
return path.join(execDir, fileName);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
// List of mirrors for GitHub
|
||||
const _sourceMirror = [
|
||||
// ghfast.top: https://ghfast.top/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
["https://ghfast.top/", r"(.*)"],
|
||||
// github.moeyy.xyz: https://github.moeyy.xyz/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
["https://github.moeyy.xyz/", r"(.*)"],
|
||||
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
|
||||
[
|
||||
"https://fastly.jsdelivr.net/gh",
|
||||
r".*raw.githubusercontent.com(/.*)(/.*)/(?:master|main)(/.*)"
|
||||
],
|
||||
];
|
||||
|
||||
// List of mirrors for GitHub release
|
||||
const _assertMirror = [
|
||||
// ghfast.top: https://ghfast.top/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||
["https://ghfast.top/", r"(.*)"],
|
||||
// github.moeyy.xyz: https://github.moeyy.xyz/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||
["https://github.moeyy.xyz/", r"(.*)"],
|
||||
];
|
||||
|
||||
enum MirrorType {
|
||||
githubSource,
|
||||
githubRelease,
|
||||
}
|
||||
|
||||
/// Auto detect the best mirror for the given [rawUrl] and [type]
|
||||
Future<String> githubAutoMirror(String rawUrl, MirrorType type) async {
|
||||
final mirrorUrls = githubMirrorUrls(rawUrl, type);
|
||||
|
||||
// Ping all mirrors and get the fastest one
|
||||
final pingResult = await Future.wait(mirrorUrls.map((e) async {
|
||||
final client = Dio()
|
||||
..options.sendTimeout = const Duration(seconds: 3)
|
||||
..options.connectTimeout = const Duration(seconds: 3);
|
||||
var time = DateTime.now().millisecondsSinceEpoch;
|
||||
try {
|
||||
final response = await client.head(e);
|
||||
if (response.statusCode == 200) {
|
||||
time = DateTime.now().millisecondsSinceEpoch - time;
|
||||
return (e, time);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return (e, -1);
|
||||
}));
|
||||
|
||||
var list = pingResult.where((e) => e.$2 != -1).toList()
|
||||
..sort((a, b) => a.$2.compareTo(b.$2));
|
||||
if (list.isNotEmpty) {
|
||||
return list.first.$1;
|
||||
}
|
||||
|
||||
return rawUrl;
|
||||
}
|
||||
|
||||
List<String> githubMirrorUrls(String rawUrl, MirrorType type) {
|
||||
final mirrors = switch (type) {
|
||||
MirrorType.githubSource => _sourceMirror,
|
||||
MirrorType.githubRelease => _assertMirror,
|
||||
};
|
||||
|
||||
final ret = <String>[];
|
||||
for (final cdn in mirrors) {
|
||||
final reg = RegExp(cdn[1]);
|
||||
final match = reg.firstMatch(rawUrl.toString());
|
||||
var matchStr = "";
|
||||
for (var i = 1; i <= match!.groupCount; i++) {
|
||||
matchStr += match.group(i)!;
|
||||
}
|
||||
ret.add("${cdn[0]}$matchStr");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:install_plugin/install_plugin.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../api/api.dart';
|
||||
import '../app/views/outlined_button_loading.dart';
|
||||
import 'arch/arch.dart';
|
||||
import 'github_mirror.dart';
|
||||
import 'log_util.dart';
|
||||
import 'message.dart';
|
||||
import 'package_info.dart';
|
||||
import 'util.dart';
|
||||
|
||||
enum Channel {
|
||||
windowsInstaller,
|
||||
windowsPortable,
|
||||
macosDmg,
|
||||
linuxFlathub,
|
||||
linuxSnap,
|
||||
linuxDeb,
|
||||
linuxAppImage,
|
||||
androidApk,
|
||||
iosIpa,
|
||||
docker,
|
||||
}
|
||||
|
||||
const _channelEnv = String.fromEnvironment("UPDATE_CHANNEL");
|
||||
final _channel =
|
||||
Channel.values.where((e) => e.name == _channelEnv).firstOrNull ??
|
||||
() {
|
||||
if (Util.isWindows()) {
|
||||
return Channel.windowsPortable;
|
||||
} else if (Util.isMacos()) {
|
||||
return Channel.macosDmg;
|
||||
} else if (Util.isAndroid()) {
|
||||
return Channel.androidApk;
|
||||
} else if (Util.isIOS()) {
|
||||
return Channel.iosIpa;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}();
|
||||
final _updaterBin = "updater${Util.isWindows() ? ".exe" : ""}";
|
||||
|
||||
Future<void> installUpdater() async {
|
||||
await Util.installAsset(
|
||||
'assets/exec/$_updaterBin', Util.homePathJoin(_updaterBin),
|
||||
executable: true);
|
||||
}
|
||||
|
||||
class VersionInfo {
|
||||
final String version;
|
||||
final String changeLog;
|
||||
|
||||
VersionInfo(this.version, this.changeLog);
|
||||
}
|
||||
|
||||
Future<VersionInfo?> checkUpdate() async {
|
||||
String? releaseDataStr;
|
||||
try {
|
||||
releaseDataStr = (await proxyRequest(
|
||||
"https://api.github.com/repos/GopeedLab/gopeed/releases/latest"))
|
||||
.data;
|
||||
} catch (e) {
|
||||
releaseDataStr =
|
||||
(await proxyRequest("https://gopeed.com/api/release")).data;
|
||||
}
|
||||
if (releaseDataStr == null) {
|
||||
return null;
|
||||
}
|
||||
final releaseData = jsonDecode(releaseDataStr);
|
||||
final tagName = releaseData["tag_name"];
|
||||
if (tagName == null) {
|
||||
return null;
|
||||
}
|
||||
final latestVersion = releaseData["tag_name"].substring(1);
|
||||
|
||||
// compare version x.y.z to x.y.z
|
||||
final currentVersion = packageInfo.version;
|
||||
var isNewVersion = false;
|
||||
if (latestVersion != currentVersion) {
|
||||
final currentVersionList = currentVersion.split(".");
|
||||
final latestVersionList = latestVersion.split(".");
|
||||
for (var i = 0; i < currentVersionList.length; i++) {
|
||||
if (int.parse(latestVersionList[i]) > int.parse(currentVersionList[i])) {
|
||||
isNewVersion = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNewVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return VersionInfo(latestVersion, releaseData["body"]);
|
||||
}
|
||||
|
||||
Future<void> showUpdateDialog(
|
||||
BuildContext context, VersionInfo versionInfo) async {
|
||||
final fullChangeLog = versionInfo.changeLog;
|
||||
final isZh = Get.locale?.languageCode == "zh";
|
||||
final changeLogRegex = isZh
|
||||
? RegExp(r"(#\s+更新日志.*)", multiLine: true, dotAll: true)
|
||||
: RegExp(r"(# Release notes.*)#\s+更新日志", multiLine: true, dotAll: true);
|
||||
final changeLog = changeLogRegex.firstMatch(fullChangeLog)?.group(1) ?? "";
|
||||
await showDialog(
|
||||
context: Get.context!,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
bool isDownloading = false;
|
||||
double progress = 0;
|
||||
int total = 0;
|
||||
final buttonController = OutlinedButtonLoadingController();
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final dialogWidth =
|
||||
screenSize.width < 500 ? screenSize.width * 0.9 : 500.0;
|
||||
final dialogHeight =
|
||||
screenSize.height < 600 ? screenSize.height * 0.8 : 400.0;
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: dialogWidth,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'newVersionTitle'
|
||||
.trParams({'version': versionInfo.version}),
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
height: dialogHeight * 0.5,
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border.all(color: Theme.of(context).dividerColor),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
scrollbars: true,
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final controller = ScrollController();
|
||||
return Scrollbar(
|
||||
controller: controller,
|
||||
thumbVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children:
|
||||
_parseMarkdown(changeLog, context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isDownloading) ...[
|
||||
const SizedBox(height: 16),
|
||||
LinearProgressIndicator(
|
||||
value: progress,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${(progress * 100).toStringAsFixed(1)}%',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
Text(
|
||||
total == 0
|
||||
? ''
|
||||
: '${Util.fmtByte((total * progress).toInt())} / ${Util.fmtByte(total)}',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: isDownloading ? null : () => Get.back(),
|
||||
child: Text(
|
||||
'newVersionLater'.tr,
|
||||
style: TextStyle(
|
||||
color: isDownloading
|
||||
? Theme.of(context).disabledColor
|
||||
: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButtonLoading(
|
||||
controller: buttonController,
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
isDownloading = true;
|
||||
});
|
||||
buttonController.start();
|
||||
try {
|
||||
await _update(versionInfo.version,
|
||||
(received, fileTotal) {
|
||||
setState(() {
|
||||
total = fileTotal;
|
||||
progress = received / fileTotal;
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
} finally {
|
||||
setState(() {
|
||||
isDownloading = false;
|
||||
});
|
||||
buttonController.stop();
|
||||
}
|
||||
},
|
||||
child: Text('newVersionUpdate'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _parseMarkdown(String markdown, BuildContext context) {
|
||||
final List<Widget> widgets = [];
|
||||
final lines = markdown.split('\n');
|
||||
|
||||
for (final line in lines) {
|
||||
if (line.trim().isEmpty) continue;
|
||||
if (line.startsWith('# ')) {
|
||||
// H1 header
|
||||
widgets.add(Text(
|
||||
line.substring(2),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
));
|
||||
} else if (line.startsWith('## ')) {
|
||||
// H2 header
|
||||
widgets.add(Text(
|
||||
line.substring(3),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
));
|
||||
} else if (line.trim().startsWith('- ')) {
|
||||
// List item
|
||||
widgets.add(Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('• ', style: TextStyle(fontSize: 14)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
line.substring(line.indexOf('-') + 1).trim().replaceFirst(
|
||||
RegExp(r'@[^\s]*\s\(#\d+\)'),
|
||||
''), // Remove contributor and pr number
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
} else {
|
||||
// Normal text
|
||||
widgets.add(Text(
|
||||
line,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
));
|
||||
}
|
||||
|
||||
// Add spacing between elements
|
||||
widgets.add(const SizedBox(height: 8));
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
Future<void> _update(String version, Function(int, int) onProgress) async {
|
||||
var newVersionAssetPath = "";
|
||||
final newVersionAssetName = _getAssetName(version);
|
||||
|
||||
// Need to download the asset
|
||||
if (newVersionAssetName.isNotEmpty) {
|
||||
final downloadUrl =
|
||||
'https://github.com/GopeedLab/gopeed/releases/download/v$version/$newVersionAssetName';
|
||||
newVersionAssetPath = await _getAssetPath(version);
|
||||
|
||||
if (downloadUrl.isNotEmpty) {
|
||||
final fastDownloadUrl =
|
||||
await githubAutoMirror(downloadUrl, MirrorType.githubRelease);
|
||||
final downloadClient = Dio();
|
||||
await downloadClient.download(fastDownloadUrl, newVersionAssetPath,
|
||||
onReceiveProgress: onProgress);
|
||||
}
|
||||
}
|
||||
|
||||
switch (_channel) {
|
||||
case Channel.windowsInstaller:
|
||||
case Channel.windowsPortable:
|
||||
case Channel.macosDmg:
|
||||
case Channel.linuxFlathub:
|
||||
case Channel.linuxSnap:
|
||||
case Channel.linuxDeb:
|
||||
final updaterPath = Util.homePathJoin(_updaterBin);
|
||||
// Check the updater binary is exists
|
||||
if (!await File(updaterPath).exists()) {
|
||||
await launchUrl(
|
||||
Uri.parse(
|
||||
'https://github.com/GopeedLab/gopeed/releases/tag/v$version'),
|
||||
mode: LaunchMode.externalApplication);
|
||||
break;
|
||||
}
|
||||
/**
|
||||
*Usage of updater command:
|
||||
-pid int
|
||||
PID of the process to update
|
||||
-channel string
|
||||
Update channel
|
||||
-asset string
|
||||
Path to the package asset
|
||||
-log string
|
||||
Log file path
|
||||
*/
|
||||
await Process.run(updaterPath, [
|
||||
"-pid",
|
||||
pid.toString(),
|
||||
"-channel",
|
||||
_channel!.name,
|
||||
"-asset",
|
||||
newVersionAssetPath,
|
||||
"-log",
|
||||
path.join(logsDir(), "updater.log")
|
||||
]);
|
||||
break;
|
||||
case Channel.androidApk:
|
||||
await InstallPlugin.installApk(newVersionAssetPath);
|
||||
break;
|
||||
default:
|
||||
await launchUrl(
|
||||
Uri.parse(
|
||||
'https://github.com/GopeedLab/gopeed/releases/tag/v$version'),
|
||||
mode: LaunchMode.externalApplication);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String _getAssetName(String version) {
|
||||
final arch = getArch();
|
||||
|
||||
String commonArchName() {
|
||||
return switch (arch) {
|
||||
Architecture.ia32 => "386",
|
||||
Architecture.x64 => "amd64",
|
||||
_ => arch.name
|
||||
};
|
||||
}
|
||||
|
||||
switch (_channel) {
|
||||
case Channel.windowsInstaller:
|
||||
return 'Gopeed-v$version-windows-${commonArchName()}.zip';
|
||||
case Channel.windowsPortable:
|
||||
return 'Gopeed-v$version-windows-${commonArchName()}-portable.zip';
|
||||
case Channel.macosDmg:
|
||||
return 'Gopeed-v$version-macos.dmg';
|
||||
case Channel.linuxDeb:
|
||||
return 'Gopeed-v$version-linux-${commonArchName()}.deb';
|
||||
case Channel.androidApk:
|
||||
final apkArchName = switch (arch) {
|
||||
Architecture.arm => "armeabi-v7a",
|
||||
Architecture.arm64 => "arm64-v8a",
|
||||
Architecture.x64 => "x86_64",
|
||||
_ => null
|
||||
};
|
||||
var apkNamePrefix = "Gopeed-v$version-android";
|
||||
if (apkArchName != null) {
|
||||
apkNamePrefix += "-$apkArchName";
|
||||
}
|
||||
return "$apkNamePrefix.apk";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getAssetPath(String version) async {
|
||||
return path.join(
|
||||
(await getTemporaryDirectory()).path, _getAssetName(version));
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
@@ -153,4 +155,45 @@ class Util {
|
||||
timer = Timer(Duration(milliseconds: ms), fn);
|
||||
};
|
||||
}
|
||||
|
||||
static String homePathJoin(String fileName) {
|
||||
final execPath = Platform.resolvedExecutable;
|
||||
final execDir = path.dirname(execPath);
|
||||
return path.join(execDir, fileName);
|
||||
}
|
||||
|
||||
static Future<void> installAsset(String assetPath, String targetPath,
|
||||
{bool executable = false}) async {
|
||||
Future<List<int>> getAssetData() async {
|
||||
final asset = await rootBundle.load(assetPath);
|
||||
return asset.buffer.asUint8List(asset.offsetInBytes, asset.lengthInBytes);
|
||||
}
|
||||
|
||||
// Check if target file is not installed
|
||||
if (!await File(targetPath).exists()) {
|
||||
final assetData = await getAssetData();
|
||||
final file = File(targetPath);
|
||||
await file.writeAsBytes(assetData);
|
||||
// Add execute permission when file first created
|
||||
if (executable && !Platform.isWindows) {
|
||||
await Process.run('chmod', ['+x', targetPath]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if target file needs to be updated
|
||||
final assetData = await getAssetData();
|
||||
if (_md5(assetData) != await _md5File(File(targetPath))) {
|
||||
await File(targetPath).writeAsBytes(assetData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static String _md5(List<int> data) {
|
||||
return md5.convert(data).toString();
|
||||
}
|
||||
|
||||
static Future<String> _md5File(File file) async {
|
||||
return file.openRead().transform(md5).first.toString();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user