Skip to content

Commit

Permalink
feat: torrent scheme deep link and file associations (#866)
Browse files Browse the repository at this point in the history
  • Loading branch information
monkeyWie authored Jan 15, 2025
1 parent ccb3267 commit acc7ed8
Show file tree
Hide file tree
Showing 30 changed files with 464 additions and 79 deletions.
9 changes: 8 additions & 1 deletion internal/protocol/bt/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,14 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
f.torrent, err = client.AddMagnet(req.URL)
} else {
var reader io.Reader
if schema == "DATA" {
if schema == "FILE" {
fileUrl, _ := url.Parse(req.URL)
filePath := fileUrl.Path[1:]
reader, err = os.Open(filePath)
if err != nil {
return
}
} else if schema == "DATA" {
_, data := util.ParseDataUri(req.URL)
reader = bytes.NewBuffer(data)
} else {
Expand Down
11 changes: 11 additions & 0 deletions internal/protocol/bt/fetcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
gohttp "net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"testing"
)
Expand Down Expand Up @@ -149,6 +150,16 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
}
})

t.Run("Resolve file scheme Torrent", func(t *testing.T) {
file, _ := filepath.Abs("./testdata/test.unclean.torrent")
uri := "file:///" + file
err := fetcher.Resolve(&base.Request{
URL: uri,
})
if err != nil {
t.Errorf("Resolve file scheme Torrent Resolve() got = %v, want nil", err)
}
})
}

func TestFetcherManager_ParseName(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions internal/protocol/http/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ func (f *Fetcher) Resolve(req *base.Request) error {
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
// response 206 status code, support breakpoint continuation
res.Range = true
// parse content length from Content-Range header, eg: bytes 0-1000/1001
// parse content length from Content-Range header, eg: bytes 0-1000/1001 or bytes 0-0/*
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
if contentTotal != "" {
if contentTotal != "" && contentTotal != "*" {
parse, err := strconv.ParseInt(contentTotal, 10, 64)
if err != nil {
return err
Expand Down
1 change: 0 additions & 1 deletion pkg/util/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

func ParseSchema(url string) string {
index := strings.Index(url, ":")
// if no schema or windows path like C:\a.txt, return FILE
if index == -1 || index == 1 {
return ""
}
Expand Down
2 changes: 2 additions & 0 deletions ui/flutter/lib/api/model/downloader_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class ExtraConfig {
String locale;
bool lastDeleteTaskKeep;
bool defaultDirectDownload;
bool defaultBtClient;

ExtraConfigBt bt = ExtraConfigBt();

Expand All @@ -88,6 +89,7 @@ class ExtraConfig {
this.locale = '',
this.lastDeleteTaskKeep = false,
this.defaultDirectDownload = false,
this.defaultBtClient = true,
});

factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
Expand Down
34 changes: 23 additions & 11 deletions ui/flutter/lib/app/modules/app/controllers/app_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import '../../../../util/log_util.dart';
import '../../../../util/package_info.dart';
import '../../../../util/util.dart';
import '../../../routes/app_pages.dart';
import '../../redirect/views/redirect_view.dart';

const unixSocketPath = 'gopeed.sock';

Expand Down Expand Up @@ -160,22 +161,21 @@ class AppController extends GetxController with WindowListener, TrayListener {
}

Future<void> _initDeepLinks() async {
// currently only support android
if (!Util.isAndroid()) {
if (Util.isWeb()) {
return;
}

_appLinks = AppLinks();

// Handle link when app is in warm state (front or background)
_linkSubscription = _appLinks.uriLinkStream.listen((uri) async {
await _toCreate(uri);
await _handleDeepLink(uri);
});

// Check initial link if app was in cold state (terminated)
final uri = await _appLinks.getInitialLink();
if (uri != null) {
await _toCreate(uri);
await _handleDeepLink(uri);
}
}

Expand Down Expand Up @@ -304,13 +304,25 @@ class AppController extends GetxController with WindowListener, TrayListener {
}
}

Future<void> _toCreate(Uri uri) async {
final path = (uri.scheme == "magnet" ||
uri.scheme == "http" ||
uri.scheme == "https")
? uri.toString()
: (await toFile(uri.toString())).path;
await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path);
Future<void> _handleDeepLink(Uri uri) async {
// Wake up application only
if (uri.scheme == "gopeed") {
Get.rootDelegate.offAndToNamed(Routes.HOME);
return;
}

String path;
if (uri.scheme == "magnet" ||
uri.scheme == "http" ||
uri.scheme == "https") {
path = uri.toString();
} else if (uri.scheme == "file") {
path = Util.isWindows() ? uri.path.substring(1) : uri.path;
} else {
path = (await toFile(uri.toString())).path;
}
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
arguments: RedirectArgs(Routes.CREATE, arguments: path));
}

String runningAddress() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import '../../app/controllers/app_controller.dart';

class CreateController extends GetxController
with GetSingleTickerProviderStateMixin {
// final files = [].obs;
final RxList fileInfos = [].obs;
final RxList openedFolders = [].obs;
final selectedIndexes = <int>[].obs;
Expand Down
46 changes: 22 additions & 24 deletions ui/flutter/lib/app/modules/create/views/create_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,31 +61,29 @@ class CreateView extends GetView<CreateController> {
}

final String? filePath = Get.rootDelegate.arguments();
if (_urlController.text.isEmpty) {
if (filePath?.isNotEmpty ?? false) {
// get file path from route arguments
_urlController.text = filePath!;
_urlController.selection = TextSelection.fromPosition(
TextPosition(offset: _urlController.text.length));
} else {
// read clipboard
Clipboard.getData('text/plain').then((value) {
if (value?.text?.isNotEmpty ?? false) {
if (_availableSchemes
.where((e) =>
value!.text!.startsWith(e) ||
value.text!.startsWith(e.toUpperCase()))
.isNotEmpty) {
_urlController.text = value!.text!;
_urlController.selection = TextSelection.fromPosition(
TextPosition(offset: _urlController.text.length));
return;
}

recognizeMagnetUri(value!.text!);
if (filePath?.isNotEmpty ?? false) {
// get file path from route arguments
_urlController.text = filePath!;
_urlController.selection = TextSelection.fromPosition(
TextPosition(offset: _urlController.text.length));
} else if (_urlController.text.isEmpty) {
// read clipboard
Clipboard.getData('text/plain').then((value) {
if (value?.text?.isNotEmpty ?? false) {
if (_availableSchemes
.where((e) =>
value!.text!.startsWith(e) ||
value.text!.startsWith(e.toUpperCase()))
.isNotEmpty) {
_urlController.text = value!.text!;
_urlController.selection = TextSelection.fromPosition(
TextPosition(offset: _urlController.text.length));
return;
}
});
}

recognizeMagnetUri(value!.text!);
}
});
}

return Scaffold(
Expand Down
12 changes: 12 additions & 0 deletions ui/flutter/lib/app/modules/redirect/bindings/redirect_binding.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:get/get.dart';

import '../controllers/redirect_controller.dart';

class RedirectBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<RedirectController>(
() => RedirectController(),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import 'package:get/get.dart';

class RedirectController extends GetxController {}
23 changes: 23 additions & 0 deletions ui/flutter/lib/app/modules/redirect/views/redirect_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';

import '../controllers/redirect_controller.dart';

class RedirectArgs {
final String page;
final dynamic arguments;

RedirectArgs(this.page, {this.arguments});
}

class RedirectView extends GetView<RedirectController> {
const RedirectView({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
final redirectArgs = Get.rootDelegate.arguments() as RedirectArgs;
Get.rootDelegate
.offAndToNamed(redirectArgs.page, arguments: redirectArgs.arguments);
return const SizedBox();
}
}
42 changes: 38 additions & 4 deletions ui/flutter/lib/app/modules/setting/views/setting_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import '../../../../util/locale_manager.dart';
import '../../../../util/log_util.dart';
import '../../../../util/message.dart';
import '../../../../util/package_info.dart';
import '../../../../util/scheme_register/scheme_register.dart';
import '../../../../util/util.dart';
import '../../../views/check_list_view.dart';
import '../../../views/directory_selector.dart';
Expand Down Expand Up @@ -111,7 +112,7 @@ class SettingView extends GetView<SettingController> {
});

final buildDefaultDirectDownload =
_buildConfigItem('defaultDirectDownload'.tr, () {
_buildConfigItem('defaultDirectDownload', () {
return appController.downloaderConfig.value.extra.defaultDirectDownload
? 'on'.tr
: 'off'.tr;
Expand Down Expand Up @@ -161,7 +162,7 @@ class SettingView extends GetView<SettingController> {
// Currently auto startup only support Windows and Linux
final buildAutoStartup = !Util.isWindows() && !Util.isLinux()
? () => null
: _buildConfigItem('launchAtStartup'.tr, () {
: _buildConfigItem('launchAtStartup', () {
return appController.autoStartup.value ? 'on'.tr : 'off'.tr;
}, (Key key) {
return Container(
Expand Down Expand Up @@ -229,8 +230,9 @@ class SettingView extends GetView<SettingController> {
],
);
});
final buildHttpUseServerCtime = _buildConfigItem('useServerCtime'.tr,
() => httpConfig.useServerCtime ? 'on'.tr : 'off'.tr, (Key key) {
final buildHttpUseServerCtime = _buildConfigItem(
'useServerCtime', () => httpConfig.useServerCtime ? 'on'.tr : 'off'.tr,
(Key key) {
return Container(
alignment: Alignment.centerLeft,
child: Switch(
Expand Down Expand Up @@ -420,6 +422,37 @@ class SettingView extends GetView<SettingController> {
].where((e) => e != null).map((e) => e!).toList(),
);
});
final buildBtDefaultClientConfig = !Util.isWindows()
? () => null
: _buildConfigItem('setAsDefaultBtClient', () {
return appController.downloaderConfig.value.extra.defaultBtClient
? 'on'.tr
: 'off'.tr;
}, (Key key) {
return Container(
alignment: Alignment.centerLeft,
child: Switch(
value:
appController.downloaderConfig.value.extra.defaultBtClient,
onChanged: (bool value) async {
try {
if (value) {
registerDefaultTorrentClient();
} else {
unregisterDefaultTorrentClient();
}
appController.downloaderConfig.update((val) {
val!.extra.defaultBtClient = value;
});
await debounceSave();
} catch (e) {
showErrorMessage(e);
logger.e('register default torrent client fail', e);
}
},
),
);
});

// ui config items start
final buildTheme = _buildConfigItem(
Expand Down Expand Up @@ -947,6 +980,7 @@ class SettingView extends GetView<SettingController> {
buildBtTrackerSubscribeUrls(),
buildBtTrackers(),
buildBtSeedConfig(),
buildBtDefaultClientConfig(),
]),
)),
Text('ui'.tr),
Expand Down
11 changes: 9 additions & 2 deletions ui/flutter/lib/app/routes/app_pages.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import 'package:get/get.dart';
import 'package:gopeed/app/modules/task/views/task_files_view.dart';
import 'package:gopeed/app/modules/task/views/task_view.dart';

import '../modules/create/bindings/create_binding.dart';
import '../modules/create/views/create_view.dart';
import '../modules/extension/bindings/extension_binding.dart';
import '../modules/extension/views/extension_view.dart';
import '../modules/home/bindings/home_binding.dart';
import '../modules/home/views/home_view.dart';
import '../modules/redirect/bindings/redirect_binding.dart';
import '../modules/redirect/views/redirect_view.dart';
import '../modules/root/bindings/root_binding.dart';
import '../modules/root/views/root_view.dart';
import '../modules/setting/bindings/setting_binding.dart';
import '../modules/setting/views/setting_view.dart';
import '../modules/task/bindings/task_binding.dart';
import '../modules/task/bindings/task_files_binding.dart';
import '../modules/task/views/task_files_view.dart';
import '../modules/task/views/task_view.dart';

part 'app_routes.dart';

Expand Down Expand Up @@ -68,6 +70,11 @@ class AppPages {
page: () => CreateView(),
binding: CreateBinding(),
),
GetPage(
name: _Paths.REDIRECT,
page: () => const RedirectView(),
binding: RedirectBinding(),
),
]),
];
}
4 changes: 4 additions & 0 deletions ui/flutter/lib/app/routes/app_routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,26 @@ part of 'app_pages.dart';

abstract class Routes {
Routes._();

static const ROOT = _Paths.ROOT;
static const HOME = _Paths.HOME;
static const CREATE = _Paths.CREATE;
static const TASK = _Paths.HOME + _Paths.TASK;
static const TASK_FILES = TASK + _Paths.TASK_FILES;
static const EXTENSION = _Paths.HOME + _Paths.EXTENSION;
static const SETTING = _Paths.HOME + _Paths.SETTING;
static const REDIRECT = _Paths.REDIRECT;
}

abstract class _Paths {
_Paths._();

static const ROOT = '/';
static const HOME = '/home';
static const CREATE = '/create';
static const TASK = '/task';
static const TASK_FILES = '/files';
static const EXTENSION = '/extension';
static const SETTING = '/setting';
static const REDIRECT = '/redirect';
}
Loading

0 comments on commit acc7ed8

Please sign in to comment.