diff --git a/pkg/download/extension.go b/pkg/download/extension.go index 45f5c4f5..7ad6edb3 100644 --- a/pkg/download/extension.go +++ b/pkg/download/extension.go @@ -3,6 +3,14 @@ package download import ( "encoding/json" "fmt" + "io" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "time" + "github.com/GopeedLab/gopeed/internal/logger" "github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/download/engine" @@ -12,13 +20,6 @@ import ( "github.com/dop251/goja" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/transport" - "io" - "os" - "path" - "path/filepath" - "strconv" - "strings" - "time" ) var ( @@ -200,10 +201,14 @@ func (d *Downloader) fetchExtensionByGit(url string, handler func(tempExtPath st subPath = pathArr[1] } - tempExtDir := filepath.Join(d.cfg.StorageDir, tempExtensionsDir, projectPath) - if err := util.RmAndMkDirAll(tempExtDir); err != nil { + // Use unique suffix to avoid concurrent conflicts when multiple git operations + // target the same extension (e.g., install and update check happening simultaneously) + tempExtDir := filepath.Join(d.cfg.StorageDir, tempExtensionsDir, fmt.Sprintf("%s_%d", projectPath, time.Now().UnixNano())) + if err := os.MkdirAll(tempExtDir, 0755); err != nil { return nil, err } + defer os.RemoveAll(tempExtDir) + proxyOptions := transport.ProxyOptions{} proxyUrl := d.cfg.DownloaderStoreConfig.Proxy.ToUrl() if proxyUrl != nil { @@ -220,7 +225,6 @@ func (d *Downloader) fetchExtensionByGit(url string, handler func(tempExtPath st }); err != nil { return nil, err } - defer os.RemoveAll(tempExtDir) return handler(filepath.Join(tempExtDir, subPath), false) } diff --git a/pkg/util/path.go b/pkg/util/path.go index 09397574..71290fdb 100644 --- a/pkg/util/path.go +++ b/pkg/util/path.go @@ -141,17 +141,6 @@ func CopyDir(source string, target string, excludeDir ...string) error { return nil } -// RmAndMkDirAll remove and create directory, if the directory already exists, it will be overwritten. -func RmAndMkDirAll(path string) error { - if err := os.RemoveAll(path); err != nil { - return err - } - if err := os.MkdirAll(path, 0755); err != nil { - return err - } - return nil -} - // copy file, if the target file already exists, it will be overwritten. func copyForce(source string, target string) error { sourceFile, err := os.Open(source) diff --git a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart index e52e6926..19a28803 100644 --- a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart +++ b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart @@ -20,6 +20,7 @@ import 'package:window_manager/window_manager.dart'; import '../../../../api/api.dart'; import '../../../../api/model/create_task.dart'; import '../../../../api/model/downloader_config.dart'; +import '../../../../api/model/install_extension.dart'; import '../../../../api/model/request.dart'; import '../../../../api/model/result.dart'; import '../../../../core/common/start_config.dart'; @@ -293,7 +294,7 @@ class AppController extends GetxController with WindowListener, TrayListener { final jsonMeta = jsonDecode(meta); final silent = jsonMeta['silent'] as bool? ?? false; final params = await ctx.readText(); - final createTaskParams = _decodeToCreatTaskParams(params); + final createTaskParams = CreateTask.fromJson(_decodeParams(params)); if (!silent) { await windowManager.show(); _handleToCreate0(createTaskParams); @@ -382,6 +383,7 @@ class AppController extends GetxController with WindowListener, TrayListener { Future _handleDeepLink(Uri uri) async { if (uri.scheme == "gopeed") { + // gopeed:///create?params=eyJyZXEiOnsidXJsIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9maWxlLnR4dCJ9fQ== if (uri.path == "/create") { final params = uri.queryParameters["params"]; if (params?.isNotEmpty == true) { @@ -391,6 +393,16 @@ class AppController extends GetxController with WindowListener, TrayListener { Get.rootDelegate.offAndToNamed(Routes.CREATE); return; } + // gopeed:///extension?params=eyJ1cmwiOiJodHRwczovL2dpdGh1Yi5jb20vbW9ua2V5V2llL2dvcGVlZC1leHRlbnNpb24tYmlsaWJpbGkiLCJkZXZNb2RlIjpmYWxzZX0= + if (uri.path == "/extension") { + final params = uri.queryParameters["params"]; + if (params?.isNotEmpty == true) { + _handleToExtension(params!); + return; + } + Get.rootDelegate.offAndToNamed(Routes.EXTENSION); + return; + } Get.rootDelegate.offAndToNamed(Routes.HOME); return; } @@ -650,15 +662,15 @@ class AppController extends GetxController with WindowListener, TrayListener { await putConfig(downloaderConfig.value); } - CreateTask _decodeToCreatTaskParams(String params) { + Map _decodeParams(String params) { final safeParams = params.replaceAll('"', "").replaceAll(" ", "+"); final paramsJson = String.fromCharCodes(base64Decode(base64.normalize(safeParams))); - return CreateTask.fromJson(jsonDecode(paramsJson)); + return jsonDecode(paramsJson); } _handleToCreate(String params) { - final createTaskParams = _decodeToCreatTaskParams(params); + final createTaskParams = CreateTask.fromJson(_decodeParams(params)); _handleToCreate0(createTaskParams); } @@ -666,4 +678,10 @@ class AppController extends GetxController with WindowListener, TrayListener { Get.rootDelegate.offAndToNamed(Routes.REDIRECT, arguments: RedirectArgs(Routes.CREATE, arguments: createTaskParams)); } + + _handleToExtension(String params) { + final installExtension = InstallExtension.fromJson(_decodeParams(params)); + Get.rootDelegate.offAndToNamed(Routes.REDIRECT, + arguments: RedirectArgs(Routes.EXTENSION, arguments: installExtension)); + } } diff --git a/ui/flutter/lib/app/modules/create/controllers/create_controller.dart b/ui/flutter/lib/app/modules/create/controllers/create_controller.dart index 6d522002..83f3c84b 100644 --- a/ui/flutter/lib/app/modules/create/controllers/create_controller.dart +++ b/ui/flutter/lib/app/modules/create/controllers/create_controller.dart @@ -20,6 +20,9 @@ class CreateController extends GetxController final oldUrl = "".obs; final fileDataUri = "".obs; + /// Flag to prevent handling pending create task from deep link multiple times + bool pendingCreateHandled = false; + @override void onInit() { super.onInit(); diff --git a/ui/flutter/lib/app/modules/create/views/create_view.dart b/ui/flutter/lib/app/modules/create/views/create_view.dart index 0171448f..6e23cd7e 100644 --- a/ui/flutter/lib/app/modules/create/views/create_view.dart +++ b/ui/flutter/lib/app/modules/create/views/create_view.dart @@ -82,17 +82,16 @@ class CreateView extends GetView { _pathController.text = renderPathPlaceholders(downloadDir); } // Initialize archive settings from global config if not already set - if (_autoExtractController.value == null) { - _autoExtractController.value = - appController.downloaderConfig.value.archive.autoExtract; - } - if (_deleteAfterExtractController.value == null) { - _deleteAfterExtractController.value = - appController.downloaderConfig.value.archive.deleteAfterExtract; - } + _autoExtractController.value ??= + appController.downloaderConfig.value.archive.autoExtract; + _deleteAfterExtractController.value ??= + appController.downloaderConfig.value.archive.deleteAfterExtract; + // Handle pending create task from deep link final CreateTask? routerParams = Get.rootDelegate.arguments(); - if (routerParams?.req?.url.isNotEmpty ?? false) { + if ((routerParams?.req?.url.isNotEmpty ?? false) && + !controller.pendingCreateHandled) { + controller.pendingCreateHandled = true; // get url from route arguments final url = routerParams!.req!.url; _urlController.text = url; diff --git a/ui/flutter/lib/app/modules/extension/controllers/extension_controller.dart b/ui/flutter/lib/app/modules/extension/controllers/extension_controller.dart index 54b4c3f6..c421980f 100644 --- a/ui/flutter/lib/app/modules/extension/controllers/extension_controller.dart +++ b/ui/flutter/lib/app/modules/extension/controllers/extension_controller.dart @@ -9,6 +9,9 @@ class ExtensionController extends GetxController { final devMode = false.obs; var _devModeCount = 0; + /// Flag to prevent handling pending install multiple times + bool pendingInstallHandled = false; + @override void onInit() async { super.onInit(); diff --git a/ui/flutter/lib/app/modules/extension/views/extension_view.dart b/ui/flutter/lib/app/modules/extension/views/extension_view.dart index 422f6d75..c3987efe 100644 --- a/ui/flutter/lib/app/modules/extension/views/extension_view.dart +++ b/ui/flutter/lib/app/modules/extension/views/extension_view.dart @@ -30,8 +30,34 @@ class ExtensionView extends GetView { final _installUrlController = TextEditingController(); final _installBtnController = IconButtonLoadingController(); + Future _doInstall() async { + if (_installUrlController.text.isEmpty) { + controller.tryOpenDevMode(); + return; + } + _installBtnController.start(); + try { + await installExtension(InstallExtension(url: _installUrlController.text)); + Get.snackbar('tip'.tr, 'extensionInstallSuccess'.tr); + await controller.load(); + } catch (e) { + showErrorMessage(e); + } finally { + _installBtnController.stop(); + } + } + @override Widget build(BuildContext context) { + // Handle pending install extension from deep link + final args = Get.rootDelegate.arguments(); + if (args is InstallExtension && !controller.pendingInstallHandled) { + controller.pendingInstallHandled = true; + _installUrlController.text = args.url; + WidgetsBinding.instance.addPostFrameCallback((_) { + _doInstall(); + }); + } return Scaffold( body: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -71,23 +97,7 @@ class ExtensionView extends GetView { const SizedBox(width: 10), IconButtonLoading( controller: _installBtnController, - onPressed: () async { - if (_installUrlController.text.isEmpty) { - controller.tryOpenDevMode(); - return; - } - _installBtnController.start(); - try { - await installExtension(InstallExtension( - url: _installUrlController.text)); - Get.snackbar('tip'.tr, 'extensionInstallSuccess'.tr); - await controller.load(); - } catch (e) { - showErrorMessage(e); - } finally { - _installBtnController.stop(); - } - }, + onPressed: _doInstall, icon: const Icon(Icons.download)), controller.devMode.value && Util.isDesktop() ? IconButton(