feat: use rhttp (#1988)

This commit is contained in:
Tien Do Nam
2024-11-03 00:33:22 +01:00
committed by GitHub
parent 74ae622ff2
commit f56d6bde86
17 changed files with 210 additions and 33 deletions
+6
View File
@@ -65,6 +65,8 @@ PODS:
- photo_manager (2.0.0):
- Flutter
- FlutterMacOS
- rhttp (0.0.1):
- Flutter
- SDWebImage (5.19.6):
- SDWebImage/Core (= 5.19.6)
- SDWebImage/Core (5.19.6)
@@ -108,6 +110,7 @@ DEPENDENCIES:
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- rhttp (from `.symlinks/plugins/rhttp/ios`)
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
@@ -155,6 +158,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/ios"
rhttp:
:path: ".symlinks/plugins/rhttp/ios"
share_handler_ios:
:path: ".symlinks/plugins/share_handler_ios/ios"
share_handler_ios_models:
@@ -190,6 +195,7 @@ SPEC CHECKSUMS:
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
rhttp: 367a8162e63311c6dde543169b591cc04454dcd6
SDWebImage: a79252b60f4678812d94316c91da69ec83089c9f
share_handler_ios: ae3584532280673e02aacdf77f2cdfb2c96b9211
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
+6
View File
@@ -43,10 +43,12 @@ import 'package:localsend_app/util/native/device_info_helper.dart';
import 'package:localsend_app/util/native/macos_channel.dart';
import 'package:localsend_app/util/native/platform_check.dart';
import 'package:localsend_app/util/native/tray_helper.dart';
import 'package:localsend_app/util/rhttp.dart';
import 'package:localsend_app/util/ui/dynamic_colors.dart';
import 'package:localsend_app/util/ui/snackbar.dart';
import 'package:logging/logging.dart';
import 'package:refena_flutter/refena_flutter.dart';
import 'package:rhttp/rhttp.dart';
import 'package:share_handler/share_handler.dart';
import 'package:window_manager/window_manager.dart';
@@ -145,7 +147,11 @@ Future<RefenaContainer> preInit(List<String> args) async {
return IsolateController(
initialState: ParentIsolateState.initial(
SyncState(
init: () async {
await Rhttp.init();
},
rootIsolateToken: RootIsolateToken.instance!,
httpClientFactory: RhttpWrapper.create,
securityContext: persistenceService.getSecurityContext(),
deviceInfo: ref.read(deviceInfoProvider),
alias: settings.alias,
+8 -3
View File
@@ -349,11 +349,16 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
}
void _finish({required String sessionId}) {
if (state[sessionId] != null && state[sessionId]!.status != SessionStatus.sending) {
final sessionState = state[sessionId];
if (sessionState == null) {
return;
}
if (state[sessionId]!.status != SessionStatus.sending) {
_logger.info('Transfer was canceled.');
} else {
final hasError = state[sessionId]!.files.values.any((file) => file.status == FileStatus.failed);
if (!hasError && state[sessionId]!.background == true) {
final hasError = sessionState.files.values.any((file) => file.status == FileStatus.failed);
if (!hasError && sessionState.background == true) {
// close session because everything is fine and it is in background
closeSession(sessionId);
_logger.info('Transfer finished and session removed.');
+55
View File
@@ -0,0 +1,55 @@
import 'package:common/isolate.dart';
import 'package:common/model/stored_security_context.dart';
import 'package:rhttp/rhttp.dart';
class RhttpWrapper implements CustomHttpClient {
final RhttpClient _client;
RhttpWrapper._(this._client);
factory RhttpWrapper.create(Duration timeout, StoredSecurityContext securityContext) {
final client = RhttpClient.createSync(
settings: ClientSettings(
timeoutSettings: TimeoutSettings(
timeout: timeout,
),
tlsSettings: TlsSettings(
verifyCertificates: false,
clientCertificate: ClientCertificate(
certificate: securityContext.certificate,
privateKey: securityContext.privateKey,
),
),
),
);
return RhttpWrapper._(client);
}
@override
Future<void> postStream({
required String uri,
required Map<String, String> query,
required Map<String, String> headers,
required Stream<List<int>> stream,
required void Function(double progress) onSendProgress,
required CustomCancelToken cancelToken,
}) async {
final token = CancelToken();
cancelToken.setCancel(token.cancel);
await _client.request(
method: HttpMethod.post,
expectBody: HttpExpectBody.bytes,
url: uri,
query: query,
headers: HttpHeaders.rawMap(headers),
body: HttpBody.stream(
stream,
length: int.parse(headers['Content-Length']!),
),
onSendProgress: (curr, total) {
onSendProgress(curr / total);
},
cancelToken: token,
);
}
}
@@ -17,6 +17,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
rhttp
)
set(PLUGIN_BUNDLED_LIBRARIES)
+6
View File
@@ -30,6 +30,8 @@ PODS:
- photo_manager (2.0.0):
- Flutter
- FlutterMacOS
- rhttp (0.0.1):
- FlutterMacOS
- screen_retriever (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
@@ -64,6 +66,7 @@ DEPENDENCIES:
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- photo_manager (from `Flutter/ephemeral/.symlinks/plugins/photo_manager/macos`)
- rhttp (from `Flutter/ephemeral/.symlinks/plugins/rhttp/macos`)
- screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
@@ -104,6 +107,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
photo_manager:
:path: Flutter/ephemeral/.symlinks/plugins/photo_manager/macos
rhttp:
:path: Flutter/ephemeral/.symlinks/plugins/rhttp/macos
screen_retriever:
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos
shared_preferences_foundation:
@@ -136,6 +141,7 @@ SPEC CHECKSUMS:
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
rhttp: 337afda4c3e4df31087160719ffca6452c225cc2
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
+32
View File
@@ -94,6 +94,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.1"
build_cli_annotations:
dependency: transitive
description:
name: build_cli_annotations
sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172
url: "https://pub.dev"
source: hosted
version: "2.1.0"
build_config:
dependency: transitive
description:
@@ -543,6 +551,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.23"
flutter_rust_bridge:
dependency: transitive
description:
name: flutter_rust_bridge
sha256: "5fe868d3cb8cbc4d83091748552e03f00ccfa41b8e44691bc382611f831d5f8b"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
flutter_test:
dependency: transitive
description: flutter
@@ -553,6 +569,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
freezed_annotation:
dependency: transitive
description:
name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev"
source: hosted
version: "2.4.4"
frontend_server_client:
dependency: transitive
description:
@@ -1251,6 +1275,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
rhttp:
dependency: "direct main"
description:
name: rhttp
sha256: "8dc4e1b50cbbd4422a3da93accfd40ea96227fb7d2264d6856754122aedfb194"
url: "https://pub.dev"
source: hosted
version: "0.9.0"
routerino:
dependency: "direct main"
description:
+1
View File
@@ -50,6 +50,7 @@ dependencies:
pretty_qr_code: 3.3.0
refena_flutter: 2.1.1
refena_inspector_client: 2.0.0
rhttp: 0.9.0
routerino: 0.8.0
saf_stream: 0.7.5
screen_retriever: 0.1.9
@@ -19,6 +19,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
rhttp
)
set(PLUGIN_BUNDLED_LIBRARIES)
+1
View File
@@ -1,3 +1,4 @@
export 'package:common/src/isolate/child/http_provider.dart' show CustomHttpClient, CustomCancelToken;
export 'package:common/src/isolate/child/sync_provider.dart';
export 'package:common/src/isolate/child/upload_isolate.dart' show UriContentStreamResolver;
export 'package:common/src/isolate/parent/actions.dart';
@@ -0,0 +1,45 @@
import 'package:common/src/isolate/child/sync_provider.dart';
import 'package:refena/refena.dart';
/// An abstraction to provide a custom http client.
abstract class CustomHttpClient {
Future<void> postStream({
required String uri,
required Map<String, String> query,
required Map<String, String> headers,
required Stream<List<int>> stream,
required void Function(double) onSendProgress,
required CustomCancelToken cancelToken,
});
}
class CustomCancelToken {
void Function()? _cancel;
void cancel() {
_cancel?.call();
}
void setCancel(void Function() cancel) {
_cancel = cancel;
}
}
class HttpClientCollection {
final CustomHttpClient discovery;
final CustomHttpClient longLiving;
HttpClientCollection({
required this.discovery,
required this.longLiving,
});
}
final httpProvider = ViewProvider((ref) {
final (clientFactory, securityContext, discoveryTimeout) =
ref.watch(syncProvider.select((state) => (state.httpClientFactory, state.securityContext, state.discoveryTimeout)));
return HttpClientCollection(
discovery: clientFactory(Duration(milliseconds: discoveryTimeout), securityContext),
longLiving: clientFactory(const Duration(days: 30), securityContext),
);
});
+2
View File
@@ -49,6 +49,8 @@ Future<void> setupChildIsolateHelper<S, R>({
),
);
await initialData.syncState.init();
if (init != null) {
await init(_isolateContainer);
}
@@ -1,6 +1,7 @@
import 'package:common/model/device_info_result.dart';
import 'package:common/model/dto/multicast_dto.dart';
import 'package:common/model/stored_security_context.dart';
import 'package:common/src/isolate/child/http_provider.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:meta/meta.dart';
import 'package:refena/refena.dart';
@@ -11,7 +12,9 @@ part 'sync_provider.mapper.dart';
/// In other words, the main isolate sends this state to the child isolate.
@MappableClass()
class SyncState with SyncStateMappable {
final Future<void> Function() init;
final Object rootIsolateToken;
final CustomHttpClient Function(Duration timeout, StoredSecurityContext) httpClientFactory;
final StoredSecurityContext securityContext;
final DeviceInfoResult deviceInfo;
final String alias;
@@ -24,7 +27,9 @@ class SyncState with SyncStateMappable {
final bool download;
SyncState({
required this.init,
required this.rootIsolateToken,
required this.httpClientFactory,
required this.securityContext,
required this.deviceInfo,
required this.alias,
@@ -22,8 +22,14 @@ class SyncStateMapper extends ClassMapperBase<SyncState> {
@override
final String id = 'SyncState';
static Function _$init(SyncState v) => (v as dynamic).init as Function;
static dynamic _arg$init(f) => f<Future<void> Function()>();
static const Field<SyncState, Function> _f$init = Field('init', _$init, arg: _arg$init);
static Object _$rootIsolateToken(SyncState v) => v.rootIsolateToken;
static const Field<SyncState, Object> _f$rootIsolateToken = Field('rootIsolateToken', _$rootIsolateToken);
static Function _$httpClientFactory(SyncState v) => (v as dynamic).httpClientFactory as Function;
static dynamic _arg$httpClientFactory(f) => f<CustomHttpClient Function(Duration, StoredSecurityContext)>();
static const Field<SyncState, Function> _f$httpClientFactory = Field('httpClientFactory', _$httpClientFactory, arg: _arg$httpClientFactory);
static StoredSecurityContext _$securityContext(SyncState v) => v.securityContext;
static const Field<SyncState, StoredSecurityContext> _f$securityContext = Field('securityContext', _$securityContext);
static DeviceInfoResult _$deviceInfo(SyncState v) => v.deviceInfo;
@@ -45,7 +51,9 @@ class SyncStateMapper extends ClassMapperBase<SyncState> {
@override
final MappableFields<SyncState> fields = const {
#init: _f$init,
#rootIsolateToken: _f$rootIsolateToken,
#httpClientFactory: _f$httpClientFactory,
#securityContext: _f$securityContext,
#deviceInfo: _f$deviceInfo,
#alias: _f$alias,
@@ -59,7 +67,9 @@ class SyncStateMapper extends ClassMapperBase<SyncState> {
static SyncState _instantiate(DecodingData data) {
return SyncState(
init: data.dec(_f$init),
rootIsolateToken: data.dec(_f$rootIsolateToken),
httpClientFactory: data.dec(_f$httpClientFactory),
securityContext: data.dec(_f$securityContext),
deviceInfo: data.dec(_f$deviceInfo),
alias: data.dec(_f$alias),
@@ -116,7 +126,9 @@ extension SyncStateValueCopy<$R, $Out> on ObjectCopyWith<$R, SyncState, $Out> {
abstract class SyncStateCopyWith<$R, $In extends SyncState, $Out> implements ClassCopyWith<$R, $In, $Out> {
StoredSecurityContextCopyWith<$R, StoredSecurityContext, StoredSecurityContext> get securityContext;
$R call(
{Object? rootIsolateToken,
{Future<void> Function()? init,
Object? rootIsolateToken,
CustomHttpClient Function(Duration, StoredSecurityContext)? httpClientFactory,
StoredSecurityContext? securityContext,
DeviceInfoResult? deviceInfo,
String? alias,
@@ -139,7 +151,9 @@ class _SyncStateCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, SyncState,
$value.securityContext.copyWith.$chain((v) => call(securityContext: v));
@override
$R call(
{Object? rootIsolateToken,
{Future<void> Function()? init,
Object? rootIsolateToken,
CustomHttpClient Function(Duration, StoredSecurityContext)? httpClientFactory,
StoredSecurityContext? securityContext,
DeviceInfoResult? deviceInfo,
String? alias,
@@ -150,7 +164,9 @@ class _SyncStateCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, SyncState,
bool? serverRunning,
bool? download}) =>
$apply(FieldCopyWithData({
if (init != null) #init: init,
if (rootIsolateToken != null) #rootIsolateToken: rootIsolateToken,
if (httpClientFactory != null) #httpClientFactory: httpClientFactory,
if (securityContext != null) #securityContext: securityContext,
if (deviceInfo != null) #deviceInfo: deviceInfo,
if (alias != null) #alias: alias,
@@ -163,7 +179,9 @@ class _SyncStateCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, SyncState,
}));
@override
SyncState $make(CopyWithData data) => SyncState(
init: data.get(#init, or: $value.init),
rootIsolateToken: data.get(#rootIsolateToken, or: $value.rootIsolateToken),
httpClientFactory: data.get(#httpClientFactory, or: $value.httpClientFactory),
securityContext: data.get(#securityContext, or: $value.securityContext),
deviceInfo: data.get(#deviceInfo, or: $value.deviceInfo),
alias: data.get(#alias, or: $value.alias),
@@ -1,15 +1,14 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:common/isolate.dart';
import 'package:common/model/device.dart';
import 'package:common/src/isolate/child/main.dart';
import 'package:common/src/isolate/child/sync_provider.dart';
import 'package:common/src/isolate/dto/isolate_task.dart';
import 'package:common/src/isolate/dto/isolate_task_result.dart';
import 'package:common/src/isolate/dto/send_to_isolate_data.dart';
import 'package:common/src/task/upload/http_upload.dart';
import 'package:common/util/stream.dart';
import 'package:dio/dio.dart';
import 'package:meta/meta.dart';
import 'package:refena/refena.dart';
@@ -53,7 +52,7 @@ class HttpUploadCancelTask implements BaseHttpUploadTask {
/// Map of cancel tokens for each task.
/// Task ID -> CancelToken
final _cancelTokenProvider = Provider((ref) => <int, CancelToken>{});
final _cancelTokenProvider = Provider((ref) => <int, CustomCancelToken>{});
abstract class UriContentStreamResolver {
/// Separate initialization method to create instance in the child isolate.
@@ -106,7 +105,7 @@ Future<void> setupHttpUploadIsolate(
final (streamController, subscription) = fileStream?.digested() ?? (null, null);
try {
final cancelToken = CancelToken();
final cancelToken = CustomCancelToken();
ref.read(_cancelTokenProvider).putIfAbsent(task.id, () => cancelToken);
await ref.read(httpUploadProvider).upload(
+3 -1
View File
@@ -196,6 +196,7 @@ class IsolateHttpUploadAction extends ReduxActionWithResult<IsolateController, P
final progress = _sendTaskAndListenStream(
task: task,
connection: connection,
taskId: taskId,
);
return (
@@ -240,9 +241,10 @@ class IsolateHttpUploadCancelAction extends ReduxAction<IsolateController, Paren
Stream<R> _sendTaskAndListenStream<R, T>({
required T task,
required IsolateConnector<IsolateTaskStreamResult<R>, SendToIsolateData<IsolateTask<T>>> connection,
int? taskId,
}) {
final wrappedTask = IsolateTask(
id: _idProvider.getNextId(),
id: taskId ?? _idProvider.getNextId(),
data: task,
);
+15 -23
View File
@@ -1,18 +1,17 @@
import 'package:common/api_route_builder.dart';
import 'package:common/model/device.dart';
import 'package:common/src/isolate/child/dio_provider.dart';
import 'package:dio/dio.dart';
import 'package:common/src/isolate/child/http_provider.dart';
import 'package:refena/refena.dart';
final httpUploadProvider = ViewProvider((ref) {
final dio = ref.watch(dioProvider).longLiving;
return HttpUploadService(dio);
final client = ref.watch(httpProvider).longLiving;
return HttpUploadService(client);
});
class HttpUploadService {
final Dio _dio;
final CustomHttpClient _client;
HttpUploadService(this._dio);
HttpUploadService(this._client);
Future<void> upload({
required Stream<List<int>> stream,
@@ -23,28 +22,21 @@ class HttpUploadService {
required String fileId,
required String token,
required void Function(double) onSendProgress,
required CancelToken cancelToken,
required CustomCancelToken cancelToken,
}) async {
final stopwatch = Stopwatch()..start();
await _dio.post(
ApiRoute.upload.target(target, query: {
await _client.postStream(
uri: ApiRoute.upload.target(target),
query: {
if (remoteSessionId != null) 'sessionId': remoteSessionId,
'fileId': fileId,
'token': token,
}),
options: Options(
headers: {
'Content-Length': contentLength,
'Content-Type': contentType,
},
),
data: stream,
onSendProgress: (curr, total) {
if (stopwatch.elapsedMilliseconds >= 100) {
stopwatch.reset();
onSendProgress(curr / total);
}
},
headers: {
'Content-Length': contentLength.toString(),
'Content-Type': contentType,
},
stream: stream,
onSendProgress: onSendProgress,
cancelToken: cancelToken,
);
}