feat: add http server isolate
CI / format (push) Has been cancelled
CI / test (push) Has been cancelled
CI / packaging (push) Has been cancelled

This commit is contained in:
Tien Do Nam
2026-07-17 04:06:04 +02:00
parent b913061a76
commit b511ac7056
22 changed files with 3905 additions and 2548 deletions
+8
View File
@@ -1,3 +1,11 @@
export 'package:localsend_app/isolate/src/isolate/child/server_isolate.dart'
show
HttpServerEvent,
HttpServerFileUploadEvent,
HttpServerFileUploadResultEvent,
HttpServerPrepareUploadEvent,
HttpServerRegisterEvent,
HttpServerSessionEndEvent;
export 'package:localsend_app/isolate/src/isolate/child/sync_provider.dart'; export 'package:localsend_app/isolate/src/isolate/child/sync_provider.dart';
export 'package:localsend_app/isolate/src/isolate/child/upload_isolate.dart' export 'package:localsend_app/isolate/src/isolate/child/upload_isolate.dart'
show show
@@ -0,0 +1,235 @@
import 'package:localsend_app/isolate/constants.dart';
import 'package:localsend_app/isolate/model/dto/multicast_dto.dart';
import 'package:localsend_app/isolate/src/isolate/child/main.dart';
import 'package:localsend_app/isolate/src/isolate/child/sync_provider.dart';
import 'package:localsend_app/isolate/src/isolate/dto/send_to_isolate_data.dart';
import 'package:localsend_app/isolate/src/task/server/http_server.dart';
import 'package:localsend_app/rust/api/model.dart' show FileDto;
import 'package:localsend_app/rust/api/server.dart';
import 'package:localsend_app/util/rust.dart';
import 'package:typed_isolates/typed_isolates.dart';
sealed class BaseHttpServerTask {}
/// Starts the HTTP server.
/// The device information is derived from the sync state.
///
/// The server emits [HttpServerEvent]s on the stream of this task
/// until the server is stopped via [HttpServerStopTask].
class HttpServerStartTask implements BaseHttpServerTask {
/// Optional PIN that senders must provide to start an upload session.
final String? pin;
HttpServerStartTask({
required this.pin,
});
}
/// Stops the HTTP server.
class HttpServerStopTask implements BaseHttpServerTask {}
/// Answers a pending [HttpServerPrepareUploadEvent].
class HttpServerPrepareUploadDecisionTask implements BaseHttpServerTask {
/// The file IDs to accept (a subset of the offered files).
/// `null` declines the request.
final List<String>? acceptedFileIds;
HttpServerPrepareUploadDecisionTask({
required this.acceptedFileIds,
});
}
/// Answers a pending [HttpServerFileUploadEvent] with the target the file
/// should be saved to: either a file [path] or a writable [fileDescriptor] (Android).
///
/// The file is written by the Rust server itself.
/// A [HttpServerFileUploadResultEvent] is emitted on the stream of this task
/// once the file has been received completely (or failed).
class HttpServerFileUploadTargetTask implements BaseHttpServerTask {
final String sessionId;
final String fileId;
final String? path;
final int? fileDescriptor;
HttpServerFileUploadTargetTask({
required this.sessionId,
required this.fileId,
required this.path,
required this.fileDescriptor,
});
}
/// A message sent from the server isolate to the main isolate.
sealed class HttpServerEvent {}
/// A device registered itself on this server.
class HttpServerRegisterEvent extends HttpServerEvent {
final String ip;
final RegisterDtoV2 info;
HttpServerRegisterEvent({
required this.ip,
required this.info,
});
}
/// A sender requests to upload files.
/// Must be answered with a [HttpServerPrepareUploadDecisionTask].
class HttpServerPrepareUploadEvent extends HttpServerEvent {
final String ip;
final RegisterDtoV2 info;
final Map<String, FileDto> files;
HttpServerPrepareUploadEvent({
required this.ip,
required this.info,
required this.files,
});
}
/// An accepted file is being uploaded.
/// Must be answered with a [HttpServerFileUploadTargetTask].
class HttpServerFileUploadEvent extends HttpServerEvent {
final String sessionId;
final String fileId;
final FileDto file;
HttpServerFileUploadEvent({
required this.sessionId,
required this.fileId,
required this.file,
});
}
/// The result of a [HttpServerFileUploadTargetTask].
class HttpServerFileUploadResultEvent extends HttpServerEvent {
final String sessionId;
final String fileId;
/// `null` if the file has been saved successfully.
final String? error;
HttpServerFileUploadResultEvent({
required this.sessionId,
required this.fileId,
required this.error,
});
}
/// An upload session ended.
class HttpServerSessionEndEvent extends HttpServerEvent {
final String sessionId;
final SessionEndReasonV2 reason;
HttpServerSessionEndEvent({
required this.sessionId,
required this.reason,
});
}
Future<void> setupHttpServerIsolate(
Stream<SendToIsolateData<IsolateTask<BaseHttpServerTask>>> receiveFromMain,
void Function(IsolateTaskStreamResult<HttpServerEvent>) sendToMain,
InitialData initialData,
) async {
await setupChildIsolateHelper(
debugLabel: 'HttpServerIsolate',
receiveFromMain: receiveFromMain,
sendToMain: sendToMain,
initialData: initialData,
handler: (ref, task) async {
switch (task.data) {
case HttpServerStartTask startTask:
final syncState = ref.read(syncProvider);
final events = await ref
.read(httpServerProvider)
.start(
port: syncState.port,
tls: syncState.protocol == ProtocolType.https
? TlsConfig(
cert: syncState.securityContext.certificate,
privateKey: syncState.securityContext.privateKey,
)
: null,
alias: syncState.alias,
version: protocolVersion,
deviceModel: syncState.deviceInfo.deviceModel,
deviceType: syncState.deviceInfo.deviceType.toRust(),
fingerprint: syncState.securityContext.certificateHash,
pin: startTask.pin,
);
try {
await for (final event in events) {
sendToMain(
IsolateTaskStreamResult.event(
id: task.id,
data: switch (event) {
RsServerEvent_Register(:final ip, :final info) => HttpServerRegisterEvent(ip: ip, info: info),
RsServerEvent_PrepareUpload(:final ip, :final info, :final files) => HttpServerPrepareUploadEvent(
ip: ip,
info: info,
files: files,
),
RsServerEvent_FileUpload(:final sessionId, :final fileId, :final file) => HttpServerFileUploadEvent(
sessionId: sessionId,
fileId: fileId,
file: file,
),
RsServerEvent_SessionEnd(:final sessionId, :final reason) => HttpServerSessionEndEvent(
sessionId: sessionId,
reason: reason,
),
},
),
);
}
} finally {
sendToMain(
IsolateTaskStreamResult.done(
id: task.id,
),
);
}
return;
case HttpServerStopTask _:
await ref.read(httpServerProvider).stop();
return;
case HttpServerPrepareUploadDecisionTask decisionTask:
await ref.read(httpServerProvider).respondPrepareUpload(acceptedFileIds: decisionTask.acceptedFileIds);
return;
case HttpServerFileUploadTargetTask targetTask:
String? error;
try {
await ref
.read(httpServerProvider)
.respondFileUpload(
sessionId: targetTask.sessionId,
fileId: targetTask.fileId,
path: targetTask.path,
fileDescriptor: targetTask.fileDescriptor,
);
} catch (e) {
error = e.humanErrorMessage;
}
sendToMain(
IsolateTaskStreamResult.event(
id: task.id,
data: HttpServerFileUploadResultEvent(
sessionId: targetTask.sessionId,
fileId: targetTask.fileId,
error: error,
),
),
);
sendToMain(
IsolateTaskStreamResult.done(
id: task.id,
),
);
return;
}
},
);
}
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:localsend_app/isolate/model/device.dart'; import 'package:localsend_app/isolate/model/device.dart';
import 'package:localsend_app/isolate/src/isolate/child/http_scan_discovery_isolate.dart'; import 'package:localsend_app/isolate/src/isolate/child/http_scan_discovery_isolate.dart';
import 'package:localsend_app/isolate/src/isolate/child/multicast_discovery_isolate.dart'; import 'package:localsend_app/isolate/src/isolate/child/multicast_discovery_isolate.dart';
import 'package:localsend_app/isolate/src/isolate/child/server_isolate.dart';
import 'package:localsend_app/isolate/src/isolate/child/upload_isolate.dart'; import 'package:localsend_app/isolate/src/isolate/child/upload_isolate.dart';
import 'package:localsend_app/isolate/src/isolate/dto/send_to_isolate_data.dart'; import 'package:localsend_app/isolate/src/isolate/dto/send_to_isolate_data.dart';
import 'package:localsend_app/isolate/src/isolate/parent/parent_isolate_provider.dart'; import 'package:localsend_app/isolate/src/isolate/parent/parent_isolate_provider.dart';
@@ -187,6 +188,146 @@ class IsolateHttpUploadCancelAction extends ReduxAction<IsolateController, Paren
} }
} }
/// Starts the HTTP server and returns the stream of server events.
/// The stream ends when the server is stopped via [IsolateHttpServerStopAction].
class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateController, ParentIsolateState, Stream<HttpServerEvent>> {
final String? pin;
IsolateHttpServerStartAction({
required this.pin,
});
@override
(ParentIsolateState, Stream<HttpServerEvent>) reduce() {
final connection = state.httpServer;
if (connection == null) {
throw StateError('httpServer is not initialized');
}
return (
state,
connection.sendWrappedTaskAndListenStream(
task: HttpServerStartTask(
pin: pin,
),
),
);
}
}
class IsolateHttpServerStopAction extends ReduxAction<IsolateController, ParentIsolateState> {
@override
ParentIsolateState reduce() {
final connection = state.httpServer;
if (connection == null) {
throw StateError('httpServer is not initialized');
}
connection.sendToIsolate(
SendToIsolateData(
syncState: null,
data: IsolateTask(
data: HttpServerStopTask(),
),
),
);
return state;
}
}
/// Answers a pending [HttpServerPrepareUploadEvent].
class IsolateHttpServerPrepareUploadDecisionAction extends ReduxAction<IsolateController, ParentIsolateState> {
/// The file IDs to accept (a subset of the offered files).
/// `null` declines the request.
final List<String>? acceptedFileIds;
IsolateHttpServerPrepareUploadDecisionAction({
required this.acceptedFileIds,
});
@override
ParentIsolateState reduce() {
final connection = state.httpServer;
if (connection == null) {
throw StateError('httpServer is not initialized');
}
connection.sendToIsolate(
SendToIsolateData(
syncState: null,
data: IsolateTask(
data: HttpServerPrepareUploadDecisionTask(
acceptedFileIds: acceptedFileIds,
),
),
),
);
return state;
}
}
/// Answers a pending [HttpServerFileUploadEvent] with the target the file
/// should be saved to (either a [path] or a writable [fileDescriptor]).
/// The returned future completes when the file has been received completely
/// and throws if saving the file failed.
class IsolateHttpServerFileUploadTargetAction extends ReduxActionWithResult<IsolateController, ParentIsolateState, Future<void>> {
final String sessionId;
final String fileId;
final String? path;
final int? fileDescriptor;
IsolateHttpServerFileUploadTargetAction({
required this.sessionId,
required this.fileId,
required this.path,
required this.fileDescriptor,
});
@override
(ParentIsolateState, Future<void>) reduce() {
final connection = state.httpServer;
if (connection == null) {
throw StateError('httpServer is not initialized');
}
final events = connection.sendWrappedTaskAndListenStream(
task: HttpServerFileUploadTargetTask(
sessionId: sessionId,
fileId: fileId,
path: path,
fileDescriptor: fileDescriptor,
),
);
return (state, _awaitResult(events));
}
Future<void> _awaitResult(Stream<HttpServerEvent> events) async {
await for (final event in events) {
if (event case HttpServerFileUploadResultEvent(:final error)) {
if (error != null) {
throw HttpServerFileUploadException(error);
}
return;
}
}
throw HttpServerFileUploadException('The server isolate did not report a result');
}
}
/// Saving a file received by the HTTP server failed.
class HttpServerFileUploadException implements Exception {
final String message;
HttpServerFileUploadException(this.message);
@override
String toString() => message;
}
/// Adds the [SendToIsolateData] envelope on top of the generic /// Adds the [SendToIsolateData] envelope on top of the generic
/// [IsolateTaskConnector.sendTaskAndListenStream] from `typed_isolates`. /// [IsolateTaskConnector.sendTaskAndListenStream] from `typed_isolates`.
extension _WrappedTaskConnector<R, T> on IsolateConnector<IsolateTaskStreamResult<R>, SendToIsolateData<IsolateTask<T>>> { extension _WrappedTaskConnector<R, T> on IsolateConnector<IsolateTaskStreamResult<R>, SendToIsolateData<IsolateTask<T>>> {
@@ -3,6 +3,7 @@ import 'package:localsend_app/isolate/model/device.dart';
import 'package:localsend_app/isolate/src/isolate/child/http_scan_discovery_isolate.dart'; import 'package:localsend_app/isolate/src/isolate/child/http_scan_discovery_isolate.dart';
import 'package:localsend_app/isolate/src/isolate/child/main.dart'; import 'package:localsend_app/isolate/src/isolate/child/main.dart';
import 'package:localsend_app/isolate/src/isolate/child/multicast_discovery_isolate.dart'; import 'package:localsend_app/isolate/src/isolate/child/multicast_discovery_isolate.dart';
import 'package:localsend_app/isolate/src/isolate/child/server_isolate.dart';
import 'package:localsend_app/isolate/src/isolate/child/sync_provider.dart'; import 'package:localsend_app/isolate/src/isolate/child/sync_provider.dart';
import 'package:localsend_app/isolate/src/isolate/child/upload_isolate.dart'; import 'package:localsend_app/isolate/src/isolate/child/upload_isolate.dart';
import 'package:localsend_app/isolate/src/isolate/dto/send_to_isolate_data.dart'; import 'package:localsend_app/isolate/src/isolate/dto/send_to_isolate_data.dart';
@@ -21,12 +22,14 @@ class ParentIsolateState with ParentIsolateStateMappable {
final IsolateConnector<IsolateTaskStreamResult<Device>, SendToIsolateData<IsolateTask<HttpScanTask>>>? httpScanDiscovery; final IsolateConnector<IsolateTaskStreamResult<Device>, SendToIsolateData<IsolateTask<HttpScanTask>>>? httpScanDiscovery;
final IsolateConnector<Device, SendToIsolateData<MulticastTask>>? multicastDiscovery; final IsolateConnector<Device, SendToIsolateData<MulticastTask>>? multicastDiscovery;
final IsolateConnector<IsolateTaskStreamResult<HttpUploadEvent>, SendToIsolateData<IsolateTask<BaseHttpUploadTask>>>? httpUpload; final IsolateConnector<IsolateTaskStreamResult<HttpUploadEvent>, SendToIsolateData<IsolateTask<BaseHttpUploadTask>>>? httpUpload;
final IsolateConnector<IsolateTaskStreamResult<HttpServerEvent>, SendToIsolateData<IsolateTask<BaseHttpServerTask>>>? httpServer;
ParentIsolateState({ ParentIsolateState({
required this.syncState, required this.syncState,
required this.httpScanDiscovery, required this.httpScanDiscovery,
required this.multicastDiscovery, required this.multicastDiscovery,
required this.httpUpload, required this.httpUpload,
required this.httpServer,
}); });
static ParentIsolateState initial(SyncState syncState) => ParentIsolateState( static ParentIsolateState initial(SyncState syncState) => ParentIsolateState(
@@ -34,6 +37,7 @@ class ParentIsolateState with ParentIsolateStateMappable {
httpScanDiscovery: null, httpScanDiscovery: null,
multicastDiscovery: null, multicastDiscovery: null,
httpUpload: null, httpUpload: null,
httpServer: null,
); );
@override @override
@@ -95,6 +99,15 @@ class IsolateSetupAction extends AsyncReduxAction<IsolateController, ParentIsola
), ),
); );
final httpServer =
await TypedIsolates.startIsolate<IsolateTaskStreamResult<HttpServerEvent>, SendToIsolateData<IsolateTask<BaseHttpServerTask>>, InitialData>(
task: setupHttpServerIsolate,
param: InitialData(
syncState: state.syncState,
logLevel: Logger.root.level,
),
);
if (uriContentStreamResolver != null) { if (uriContentStreamResolver != null) {
httpUpload.sendToIsolate( httpUpload.sendToIsolate(
SendToIsolateData( SendToIsolateData(
@@ -111,6 +124,7 @@ class IsolateSetupAction extends AsyncReduxAction<IsolateController, ParentIsola
httpScanDiscovery: httpScanDiscovery, httpScanDiscovery: httpScanDiscovery,
multicastDiscovery: multicastDiscovery, multicastDiscovery: multicastDiscovery,
httpUpload: httpUpload, httpUpload: httpUpload,
httpServer: httpServer,
); );
} }
} }
@@ -121,6 +135,7 @@ class IsolateDisposeAction extends ReduxAction<IsolateController, ParentIsolateS
state.httpScanDiscovery?.isolate.kill(); state.httpScanDiscovery?.isolate.kill();
state.multicastDiscovery?.isolate.kill(); state.multicastDiscovery?.isolate.kill();
state.httpUpload?.isolate.kill(); state.httpUpload?.isolate.kill();
state.httpServer?.isolate.kill();
return state; return state;
} }
} }
@@ -62,6 +62,19 @@ class ParentIsolateStateMapper extends ClassMapperBase<ParentIsolateState> {
> >
> >
_f$httpUpload = Field('httpUpload', _$httpUpload); _f$httpUpload = Field('httpUpload', _$httpUpload);
static IsolateConnector<
IsolateTaskStreamResult<HttpServerEvent>,
SendToIsolateData<IsolateTask<BaseHttpServerTask>>
>?
_$httpServer(ParentIsolateState v) => v.httpServer;
static const Field<
ParentIsolateState,
IsolateConnector<
IsolateTaskStreamResult<HttpServerEvent>,
SendToIsolateData<IsolateTask<BaseHttpServerTask>>
>
>
_f$httpServer = Field('httpServer', _$httpServer);
@override @override
final MappableFields<ParentIsolateState> fields = const { final MappableFields<ParentIsolateState> fields = const {
@@ -69,6 +82,7 @@ class ParentIsolateStateMapper extends ClassMapperBase<ParentIsolateState> {
#httpScanDiscovery: _f$httpScanDiscovery, #httpScanDiscovery: _f$httpScanDiscovery,
#multicastDiscovery: _f$multicastDiscovery, #multicastDiscovery: _f$multicastDiscovery,
#httpUpload: _f$httpUpload, #httpUpload: _f$httpUpload,
#httpServer: _f$httpServer,
}; };
static ParentIsolateState _instantiate(DecodingData data) { static ParentIsolateState _instantiate(DecodingData data) {
@@ -77,6 +91,7 @@ class ParentIsolateStateMapper extends ClassMapperBase<ParentIsolateState> {
httpScanDiscovery: data.dec(_f$httpScanDiscovery), httpScanDiscovery: data.dec(_f$httpScanDiscovery),
multicastDiscovery: data.dec(_f$multicastDiscovery), multicastDiscovery: data.dec(_f$multicastDiscovery),
httpUpload: data.dec(_f$httpUpload), httpUpload: data.dec(_f$httpUpload),
httpServer: data.dec(_f$httpServer),
); );
} }
@@ -166,6 +181,11 @@ abstract class ParentIsolateStateCopyWith<
SendToIsolateData<IsolateTask<BaseHttpUploadTask>> SendToIsolateData<IsolateTask<BaseHttpUploadTask>>
>? >?
httpUpload, httpUpload,
IsolateConnector<
IsolateTaskStreamResult<HttpServerEvent>,
SendToIsolateData<IsolateTask<BaseHttpServerTask>>
>?
httpServer,
}); });
ParentIsolateStateCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( ParentIsolateStateCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t, Then<$Out2, $R2> t,
@@ -189,12 +209,14 @@ class _ParentIsolateStateCopyWithImpl<$R, $Out>
Object? httpScanDiscovery = $none, Object? httpScanDiscovery = $none,
Object? multicastDiscovery = $none, Object? multicastDiscovery = $none,
Object? httpUpload = $none, Object? httpUpload = $none,
Object? httpServer = $none,
}) => $apply( }) => $apply(
FieldCopyWithData({ FieldCopyWithData({
if (syncState != null) #syncState: syncState, if (syncState != null) #syncState: syncState,
if (httpScanDiscovery != $none) #httpScanDiscovery: httpScanDiscovery, if (httpScanDiscovery != $none) #httpScanDiscovery: httpScanDiscovery,
if (multicastDiscovery != $none) #multicastDiscovery: multicastDiscovery, if (multicastDiscovery != $none) #multicastDiscovery: multicastDiscovery,
if (httpUpload != $none) #httpUpload: httpUpload, if (httpUpload != $none) #httpUpload: httpUpload,
if (httpServer != $none) #httpServer: httpServer,
}), }),
); );
@override @override
@@ -209,6 +231,7 @@ class _ParentIsolateStateCopyWithImpl<$R, $Out>
or: $value.multicastDiscovery, or: $value.multicastDiscovery,
), ),
httpUpload: data.get(#httpUpload, or: $value.httpUpload), httpUpload: data.get(#httpUpload, or: $value.httpUpload),
httpServer: data.get(#httpServer, or: $value.httpServer),
); );
@override @override
@@ -0,0 +1,80 @@
import 'package:localsend_app/rust/api/model.dart';
import 'package:localsend_app/rust/api/server.dart';
import 'package:refena_flutter/refena_flutter.dart';
final httpServerProvider = Provider((ref) => HttpServerService());
/// Wraps the Rust HTTP server.
/// Only one server can run at a time.
class HttpServerService {
RsHttpServer? _server;
bool get running => _server != null;
/// Starts the server and returns the stream of server events.
/// The stream ends when the server is stopped.
Future<Stream<RsServerEvent>> start({
required int port,
required TlsConfig? tls,
required String alias,
required String version,
required String? deviceModel,
required DeviceType? deviceType,
required String fingerprint,
required String? pin,
}) async {
if (_server != null) {
throw StateError('Server already running');
}
final server = await startServer(
port: port,
tls: tls,
alias: alias,
version: version,
deviceModel: deviceModel,
deviceType: deviceType,
fingerprint: fingerprint,
pin: pin,
);
_server = server;
return server.listen();
}
/// Answers a pending prepare-upload request.
/// [acceptedFileIds] is the subset of the offered files to accept; `null` declines the request.
Future<void> respondPrepareUpload({required List<String>? acceptedFileIds}) async {
await _requireServer().respondPrepareUpload(acceptedFileIds: acceptedFileIds);
}
/// Answers a pending file upload with the target the file should be saved to
/// (either a [path] or a [fileDescriptor]) and waits until the file has been received completely.
Future<void> respondFileUpload({
required String sessionId,
required String fileId,
required String? path,
required int? fileDescriptor,
}) async {
await _requireServer().respondFileUpload(
sessionId: sessionId,
fileId: fileId,
path: path,
fileDescriptor: fileDescriptor,
);
}
/// Stops the server. The event stream returned by [start] will end.
Future<void> stop() async {
final server = _server;
_server = null;
await server?.stop();
}
RsHttpServer _requireServer() {
final server = _server;
if (server == null) {
throw StateError('Server is not running');
}
return server;
}
}
+2 -4
View File
@@ -6,10 +6,8 @@
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:localsend_app/rust/frb_generated.dart'; import 'package:localsend_app/rust/frb_generated.dart';
Future<void> verifyCert({required String cert, required String publicKey}) => RustLib.instance.api.crateApiCryptoVerifyCert( Future<void> verifyCert({required String cert, required String publicKey}) =>
cert: cert, RustLib.instance.api.crateApiCryptoVerifyCert(cert: cert, publicKey: publicKey);
publicKey: publicKey,
);
Future<KeyPair> generateKeyPair() => RustLib.instance.api.crateApiCryptoGenerateKeyPair(); Future<KeyPair> generateKeyPair() => RustLib.instance.api.crateApiCryptoGenerateKeyPair();
+3 -17
View File
@@ -14,17 +14,8 @@ part 'http.freezed.dart';
// These functions are ignored because they are not marked as `pub`: `resolve_file_content` // These functions are ignored because they are not marked as `pub`: `resolve_file_content`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `from` // These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `from`
RsHttpClient createClient({ RsHttpClient createClient({required String privateKey, required String cert, required LsHttpClientVersion version, int? timeoutMs}) =>
required String privateKey, RustLib.instance.api.crateApiHttpCreateClient(privateKey: privateKey, cert: cert, version: version, timeoutMs: timeoutMs);
required String cert,
required LsHttpClientVersion version,
int? timeoutMs,
}) => RustLib.instance.api.crateApiHttpCreateClient(
privateKey: privateKey,
cert: cert,
version: version,
timeoutMs: timeoutMs,
);
RsCancellationToken createCancellationToken() => RustLib.instance.api.crateApiHttpCreateCancellationToken(); RsCancellationToken createCancellationToken() => RustLib.instance.api.crateApiHttpCreateCancellationToken();
@@ -35,12 +26,7 @@ abstract class RsCancellationToken implements RustOpaqueInterface {
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsHttpClient>> // Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsHttpClient>>
abstract class RsHttpClient implements RustOpaqueInterface { abstract class RsHttpClient implements RustOpaqueInterface {
Future<void> cancel({ Future<void> cancel({required ProtocolType protocol, required String ip, required int port, required String sessionId});
required ProtocolType protocol,
required String ip,
required int port,
required String sessionId,
});
Future<PrepareUploadResult> prepareUpload({ Future<PrepareUploadResult> prepareUpload({
required ProtocolType protocol, required ProtocolType protocol,
+163
View File
@@ -0,0 +1,163 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
import 'package:localsend_app/rust/api/model.dart';
import 'package:localsend_app/rust/frb_generated.dart';
part 'server.freezed.dart';
// These functions are ignored because they are not marked as `pub`: `resolve_upload_target`
/// Starts the HTTP server on the given port (IPv4 and IPv6).
/// The server runs until [RsHttpServer::stop] is called.
///
/// Events are received by listening to [RsHttpServer::listen].
Future<RsHttpServer> startServer({
required int port,
TlsConfig? tls,
required String alias,
required String version,
String? deviceModel,
DeviceType? deviceType,
required String fingerprint,
String? pin,
}) => RustLib.instance.api.crateApiServerStartServer(
port: port,
tls: tls,
alias: alias,
version: version,
deviceModel: deviceModel,
deviceType: deviceType,
fingerprint: fingerprint,
pin: pin,
);
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsHttpServer>>
abstract class RsHttpServer implements RustOpaqueInterface {
/// Emits server events until the server is stopped.
/// Can only be listened to once.
Stream<RsServerEvent> listen();
/// Answers the pending [RsServerEvent::FileUpload] event with the target
/// the file should be saved to (either a path or a file descriptor)
/// and waits until the file has been received completely.
Future<void> respondFileUpload({required String sessionId, required String fileId, String? path, int? fileDescriptor});
/// Answers the pending [RsServerEvent::PrepareUpload] event.
///
/// Passing the accepted file IDs (a subset of the offered files) accepts the request.
/// Passing `None` declines the request.
Future<void> respondPrepareUpload({List<String>? acceptedFileIds});
/// Stops the server.
Future<void> stop();
}
enum ProtocolTypeV2 {
http,
https,
}
class RegisterDtoV2 {
final String alias;
final String version;
final String? deviceModel;
final DeviceType? deviceType;
final String fingerprint;
final int port;
final ProtocolTypeV2 protocol;
final bool download;
const RegisterDtoV2({
required this.alias,
required this.version,
this.deviceModel,
this.deviceType,
required this.fingerprint,
required this.port,
required this.protocol,
required this.download,
});
@override
int get hashCode =>
alias.hashCode ^
version.hashCode ^
deviceModel.hashCode ^
deviceType.hashCode ^
fingerprint.hashCode ^
port.hashCode ^
protocol.hashCode ^
download.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RegisterDtoV2 &&
runtimeType == other.runtimeType &&
alias == other.alias &&
version == other.version &&
deviceModel == other.deviceModel &&
deviceType == other.deviceType &&
fingerprint == other.fingerprint &&
port == other.port &&
protocol == other.protocol &&
download == other.download;
}
@freezed
sealed class RsServerEvent with _$RsServerEvent {
const RsServerEvent._();
/// A device registered itself via `POST /api/localsend/v2/register`.
const factory RsServerEvent.register({
required String ip,
required RegisterDtoV2 info,
}) = RsServerEvent_Register;
/// A sender requests to upload files via `POST /api/localsend/v2/prepare-upload`.
const factory RsServerEvent.prepareUpload({
required String ip,
required RegisterDtoV2 info,
required Map<String, FileDto> files,
}) = RsServerEvent_PrepareUpload;
/// An accepted file is being uploaded via `POST /api/localsend/v2/upload`.
const factory RsServerEvent.fileUpload({
required String sessionId,
required String fileId,
required FileDto file,
}) = RsServerEvent_FileUpload;
/// An upload session ended.
const factory RsServerEvent.sessionEnd({
required String sessionId,
required SessionEndReasonV2 reason,
}) = RsServerEvent_SessionEnd;
}
enum SessionEndReasonV2 {
finished,
cancelled,
}
class TlsConfig {
final String cert;
final String privateKey;
const TlsConfig({
required this.cert,
required this.privateKey,
});
@override
int get hashCode => cert.hashCode ^ privateKey.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) || other is TlsConfig && runtimeType == other.runtimeType && cert == other.cert && privateKey == other.privateKey;
}
+468
View File
@@ -0,0 +1,468 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'server.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$RsServerEvent {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RsServerEvent);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'RsServerEvent()';
}
}
/// @nodoc
class $RsServerEventCopyWith<$Res> {
$RsServerEventCopyWith(RsServerEvent _, $Res Function(RsServerEvent) __);
}
/// Adds pattern-matching-related methods to [RsServerEvent].
extension RsServerEventPatterns on RsServerEvent {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( RsServerEvent_Register value)? register,TResult Function( RsServerEvent_PrepareUpload value)? prepareUpload,TResult Function( RsServerEvent_FileUpload value)? fileUpload,TResult Function( RsServerEvent_SessionEnd value)? sessionEnd,required TResult orElse(),}){
final _that = this;
switch (_that) {
case RsServerEvent_Register() when register != null:
return register(_that);case RsServerEvent_PrepareUpload() when prepareUpload != null:
return prepareUpload(_that);case RsServerEvent_FileUpload() when fileUpload != null:
return fileUpload(_that);case RsServerEvent_SessionEnd() when sessionEnd != null:
return sessionEnd(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( RsServerEvent_Register value) register,required TResult Function( RsServerEvent_PrepareUpload value) prepareUpload,required TResult Function( RsServerEvent_FileUpload value) fileUpload,required TResult Function( RsServerEvent_SessionEnd value) sessionEnd,}){
final _that = this;
switch (_that) {
case RsServerEvent_Register():
return register(_that);case RsServerEvent_PrepareUpload():
return prepareUpload(_that);case RsServerEvent_FileUpload():
return fileUpload(_that);case RsServerEvent_SessionEnd():
return sessionEnd(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( RsServerEvent_Register value)? register,TResult? Function( RsServerEvent_PrepareUpload value)? prepareUpload,TResult? Function( RsServerEvent_FileUpload value)? fileUpload,TResult? Function( RsServerEvent_SessionEnd value)? sessionEnd,}){
final _that = this;
switch (_that) {
case RsServerEvent_Register() when register != null:
return register(_that);case RsServerEvent_PrepareUpload() when prepareUpload != null:
return prepareUpload(_that);case RsServerEvent_FileUpload() when fileUpload != null:
return fileUpload(_that);case RsServerEvent_SessionEnd() when sessionEnd != null:
return sessionEnd(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String ip, RegisterDtoV2 info)? register,TResult Function( String ip, RegisterDtoV2 info, Map<String, FileDto> files)? prepareUpload,TResult Function( String sessionId, String fileId, FileDto file)? fileUpload,TResult Function( String sessionId, SessionEndReasonV2 reason)? sessionEnd,required TResult orElse(),}) {final _that = this;
switch (_that) {
case RsServerEvent_Register() when register != null:
return register(_that.ip,_that.info);case RsServerEvent_PrepareUpload() when prepareUpload != null:
return prepareUpload(_that.ip,_that.info,_that.files);case RsServerEvent_FileUpload() when fileUpload != null:
return fileUpload(_that.sessionId,_that.fileId,_that.file);case RsServerEvent_SessionEnd() when sessionEnd != null:
return sessionEnd(_that.sessionId,_that.reason);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String ip, RegisterDtoV2 info) register,required TResult Function( String ip, RegisterDtoV2 info, Map<String, FileDto> files) prepareUpload,required TResult Function( String sessionId, String fileId, FileDto file) fileUpload,required TResult Function( String sessionId, SessionEndReasonV2 reason) sessionEnd,}) {final _that = this;
switch (_that) {
case RsServerEvent_Register():
return register(_that.ip,_that.info);case RsServerEvent_PrepareUpload():
return prepareUpload(_that.ip,_that.info,_that.files);case RsServerEvent_FileUpload():
return fileUpload(_that.sessionId,_that.fileId,_that.file);case RsServerEvent_SessionEnd():
return sessionEnd(_that.sessionId,_that.reason);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String ip, RegisterDtoV2 info)? register,TResult? Function( String ip, RegisterDtoV2 info, Map<String, FileDto> files)? prepareUpload,TResult? Function( String sessionId, String fileId, FileDto file)? fileUpload,TResult? Function( String sessionId, SessionEndReasonV2 reason)? sessionEnd,}) {final _that = this;
switch (_that) {
case RsServerEvent_Register() when register != null:
return register(_that.ip,_that.info);case RsServerEvent_PrepareUpload() when prepareUpload != null:
return prepareUpload(_that.ip,_that.info,_that.files);case RsServerEvent_FileUpload() when fileUpload != null:
return fileUpload(_that.sessionId,_that.fileId,_that.file);case RsServerEvent_SessionEnd() when sessionEnd != null:
return sessionEnd(_that.sessionId,_that.reason);case _:
return null;
}
}
}
/// @nodoc
class RsServerEvent_Register extends RsServerEvent {
const RsServerEvent_Register({required this.ip, required this.info}): super._();
final String ip;
final RegisterDtoV2 info;
/// Create a copy of RsServerEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$RsServerEvent_RegisterCopyWith<RsServerEvent_Register> get copyWith => _$RsServerEvent_RegisterCopyWithImpl<RsServerEvent_Register>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RsServerEvent_Register&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.info, info) || other.info == info));
}
@override
int get hashCode => Object.hash(runtimeType,ip,info);
@override
String toString() {
return 'RsServerEvent.register(ip: $ip, info: $info)';
}
}
/// @nodoc
abstract mixin class $RsServerEvent_RegisterCopyWith<$Res> implements $RsServerEventCopyWith<$Res> {
factory $RsServerEvent_RegisterCopyWith(RsServerEvent_Register value, $Res Function(RsServerEvent_Register) _then) = _$RsServerEvent_RegisterCopyWithImpl;
@useResult
$Res call({
String ip, RegisterDtoV2 info
});
}
/// @nodoc
class _$RsServerEvent_RegisterCopyWithImpl<$Res>
implements $RsServerEvent_RegisterCopyWith<$Res> {
_$RsServerEvent_RegisterCopyWithImpl(this._self, this._then);
final RsServerEvent_Register _self;
final $Res Function(RsServerEvent_Register) _then;
/// Create a copy of RsServerEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? ip = null,Object? info = null,}) {
return _then(RsServerEvent_Register(
ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
as String,info: null == info ? _self.info : info // ignore: cast_nullable_to_non_nullable
as RegisterDtoV2,
));
}
}
/// @nodoc
class RsServerEvent_PrepareUpload extends RsServerEvent {
const RsServerEvent_PrepareUpload({required this.ip, required this.info, required final Map<String, FileDto> files}): _files = files,super._();
final String ip;
final RegisterDtoV2 info;
final Map<String, FileDto> _files;
Map<String, FileDto> get files {
if (_files is EqualUnmodifiableMapView) return _files;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_files);
}
/// Create a copy of RsServerEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$RsServerEvent_PrepareUploadCopyWith<RsServerEvent_PrepareUpload> get copyWith => _$RsServerEvent_PrepareUploadCopyWithImpl<RsServerEvent_PrepareUpload>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RsServerEvent_PrepareUpload&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.info, info) || other.info == info)&&const DeepCollectionEquality().equals(other._files, _files));
}
@override
int get hashCode => Object.hash(runtimeType,ip,info,const DeepCollectionEquality().hash(_files));
@override
String toString() {
return 'RsServerEvent.prepareUpload(ip: $ip, info: $info, files: $files)';
}
}
/// @nodoc
abstract mixin class $RsServerEvent_PrepareUploadCopyWith<$Res> implements $RsServerEventCopyWith<$Res> {
factory $RsServerEvent_PrepareUploadCopyWith(RsServerEvent_PrepareUpload value, $Res Function(RsServerEvent_PrepareUpload) _then) = _$RsServerEvent_PrepareUploadCopyWithImpl;
@useResult
$Res call({
String ip, RegisterDtoV2 info, Map<String, FileDto> files
});
}
/// @nodoc
class _$RsServerEvent_PrepareUploadCopyWithImpl<$Res>
implements $RsServerEvent_PrepareUploadCopyWith<$Res> {
_$RsServerEvent_PrepareUploadCopyWithImpl(this._self, this._then);
final RsServerEvent_PrepareUpload _self;
final $Res Function(RsServerEvent_PrepareUpload) _then;
/// Create a copy of RsServerEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? ip = null,Object? info = null,Object? files = null,}) {
return _then(RsServerEvent_PrepareUpload(
ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
as String,info: null == info ? _self.info : info // ignore: cast_nullable_to_non_nullable
as RegisterDtoV2,files: null == files ? _self._files : files // ignore: cast_nullable_to_non_nullable
as Map<String, FileDto>,
));
}
}
/// @nodoc
class RsServerEvent_FileUpload extends RsServerEvent {
const RsServerEvent_FileUpload({required this.sessionId, required this.fileId, required this.file}): super._();
final String sessionId;
final String fileId;
final FileDto file;
/// Create a copy of RsServerEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$RsServerEvent_FileUploadCopyWith<RsServerEvent_FileUpload> get copyWith => _$RsServerEvent_FileUploadCopyWithImpl<RsServerEvent_FileUpload>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RsServerEvent_FileUpload&&(identical(other.sessionId, sessionId) || other.sessionId == sessionId)&&(identical(other.fileId, fileId) || other.fileId == fileId)&&(identical(other.file, file) || other.file == file));
}
@override
int get hashCode => Object.hash(runtimeType,sessionId,fileId,file);
@override
String toString() {
return 'RsServerEvent.fileUpload(sessionId: $sessionId, fileId: $fileId, file: $file)';
}
}
/// @nodoc
abstract mixin class $RsServerEvent_FileUploadCopyWith<$Res> implements $RsServerEventCopyWith<$Res> {
factory $RsServerEvent_FileUploadCopyWith(RsServerEvent_FileUpload value, $Res Function(RsServerEvent_FileUpload) _then) = _$RsServerEvent_FileUploadCopyWithImpl;
@useResult
$Res call({
String sessionId, String fileId, FileDto file
});
}
/// @nodoc
class _$RsServerEvent_FileUploadCopyWithImpl<$Res>
implements $RsServerEvent_FileUploadCopyWith<$Res> {
_$RsServerEvent_FileUploadCopyWithImpl(this._self, this._then);
final RsServerEvent_FileUpload _self;
final $Res Function(RsServerEvent_FileUpload) _then;
/// Create a copy of RsServerEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? sessionId = null,Object? fileId = null,Object? file = null,}) {
return _then(RsServerEvent_FileUpload(
sessionId: null == sessionId ? _self.sessionId : sessionId // ignore: cast_nullable_to_non_nullable
as String,fileId: null == fileId ? _self.fileId : fileId // ignore: cast_nullable_to_non_nullable
as String,file: null == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
as FileDto,
));
}
}
/// @nodoc
class RsServerEvent_SessionEnd extends RsServerEvent {
const RsServerEvent_SessionEnd({required this.sessionId, required this.reason}): super._();
final String sessionId;
final SessionEndReasonV2 reason;
/// Create a copy of RsServerEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$RsServerEvent_SessionEndCopyWith<RsServerEvent_SessionEnd> get copyWith => _$RsServerEvent_SessionEndCopyWithImpl<RsServerEvent_SessionEnd>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RsServerEvent_SessionEnd&&(identical(other.sessionId, sessionId) || other.sessionId == sessionId)&&(identical(other.reason, reason) || other.reason == reason));
}
@override
int get hashCode => Object.hash(runtimeType,sessionId,reason);
@override
String toString() {
return 'RsServerEvent.sessionEnd(sessionId: $sessionId, reason: $reason)';
}
}
/// @nodoc
abstract mixin class $RsServerEvent_SessionEndCopyWith<$Res> implements $RsServerEventCopyWith<$Res> {
factory $RsServerEvent_SessionEndCopyWith(RsServerEvent_SessionEnd value, $Res Function(RsServerEvent_SessionEnd) _then) = _$RsServerEvent_SessionEndCopyWithImpl;
@useResult
$Res call({
String sessionId, SessionEndReasonV2 reason
});
}
/// @nodoc
class _$RsServerEvent_SessionEndCopyWithImpl<$Res>
implements $RsServerEvent_SessionEndCopyWith<$Res> {
_$RsServerEvent_SessionEndCopyWithImpl(this._self, this._then);
final RsServerEvent_SessionEnd _self;
final $Res Function(RsServerEvent_SessionEnd) _then;
/// Create a copy of RsServerEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? sessionId = null,Object? reason = null,}) {
return _then(RsServerEvent_SessionEnd(
sessionId: null == sessionId ? _self.sessionId : sessionId // ignore: cast_nullable_to_non_nullable
as String,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as SessionEndReasonV2,
));
}
}
// dart format on
+1 -6
View File
@@ -18,12 +18,7 @@ Stream<WsServerMessage> connect({
required ProposingClientInfo info, required ProposingClientInfo info,
required String privateKey, required String privateKey,
required FutureOr<void> Function(LsSignalingConnection) onConnection, required FutureOr<void> Function(LsSignalingConnection) onConnection,
}) => RustLib.instance.api.crateApiWebrtcConnect( }) => RustLib.instance.api.crateApiWebrtcConnect(uri: uri, info: info, privateKey: privateKey, onConnection: onConnection);
uri: uri,
info: info,
privateKey: privateKey,
onConnection: onConnection,
);
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<LsSignalingConnection>> // Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<LsSignalingConnection>>
abstract class LsSignalingConnection implements RustOpaqueInterface { abstract class LsSignalingConnection implements RustOpaqueInterface {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -6
View File
@@ -7,12 +7,10 @@ pub fn generate_key_pair() -> anyhow::Result<KeyPair> {
let private_key = localsend::crypto::token::export_private_key(&signing_key)?; let private_key = localsend::crypto::token::export_private_key(&signing_key)?;
let public_key = localsend::crypto::token::export_public_key(&signing_key)?; let public_key = localsend::crypto::token::export_public_key(&signing_key)?;
Ok( Ok(KeyPair {
KeyPair { private_key: private_key.to_string(),
private_key: private_key.to_string(), public_key,
public_key, })
}
)
} }
pub struct KeyPair { pub struct KeyPair {
+1
View File
@@ -2,5 +2,6 @@ pub mod crypto;
pub mod http; pub mod http;
pub mod logging; pub mod logging;
pub mod model; pub mod model;
pub mod server;
pub mod stream; pub mod stream;
pub mod webrtc; pub mod webrtc;
+5 -4
View File
@@ -1,9 +1,10 @@
use flutter_rust_bridge::frb; use flutter_rust_bridge::frb;
pub use localsend::http::dto::{ProtocolType, RegisterDto, RegisterResponseDto, PrepareUploadRequestDto, PrepareUploadResponseDto}; pub use localsend::http::dto::{
pub use localsend::model::discovery::{DeviceType}; PrepareUploadRequestDto, PrepareUploadResponseDto, ProtocolType, RegisterDto,
pub use localsend::model::transfer::{ RegisterResponseDto,
FileDto, FileMetadata
}; };
pub use localsend::model::discovery::DeviceType;
pub use localsend::model::transfer::{FileDto, FileMetadata};
use std::collections::HashMap; use std::collections::HashMap;
#[frb(mirror(RegisterDto))] #[frb(mirror(RegisterDto))]
+276
View File
@@ -0,0 +1,276 @@
use crate::frb_generated::StreamSink;
use flutter_rust_bridge::frb;
pub use localsend::http::dto_v2::{ProtocolTypeV2, RegisterDtoV2};
use localsend::http::server::ServerConfigV2;
pub use localsend::http::server::TlsConfig;
use localsend::http::server::common::save::FileUploadTarget;
pub use localsend::http::server::v2::SessionEndReasonV2;
use localsend::http::server::v2::{PrepareUploadDecisionV2, ServerEventV2};
use localsend::http::state::ClientInfo;
use localsend::model::discovery::DeviceType;
use localsend::model::transfer::FileDto;
use std::collections::HashMap;
use tokio::sync::{Mutex, mpsc, oneshot};
/// Events emitted by the HTTP server that must be handled by the application.
///
/// [RsServerEvent::PrepareUpload] must be answered with [RsHttpServer::respond_prepare_upload]
/// and [RsServerEvent::FileUpload] with [RsHttpServer::respond_file_upload].
pub enum RsServerEvent {
/// A device registered itself via `POST /api/localsend/v2/register`.
Register { ip: String, info: RegisterDtoV2 },
/// A sender requests to upload files via `POST /api/localsend/v2/prepare-upload`.
PrepareUpload {
ip: String,
info: RegisterDtoV2,
files: HashMap<String, FileDto>,
},
/// An accepted file is being uploaded via `POST /api/localsend/v2/upload`.
FileUpload {
session_id: String,
file_id: String,
file: FileDto,
},
/// An upload session ended.
SessionEnd {
session_id: String,
reason: SessionEndReasonV2,
},
}
pub struct RsHttpServer {
event_rx: Mutex<Option<mpsc::Receiver<ServerEventV2>>>,
stop_tx: Mutex<Option<oneshot::Sender<()>>>,
pending_decision: Mutex<Option<oneshot::Sender<PrepareUploadDecisionV2>>>,
pending_uploads: Mutex<HashMap<(String, String), oneshot::Sender<FileUploadTarget>>>,
}
/// Starts the HTTP server on the given port (IPv4 and IPv6).
/// The server runs until [RsHttpServer::stop] is called.
///
/// Events are received by listening to [RsHttpServer::listen].
pub async fn start_server(
port: u16,
tls: Option<TlsConfig>,
alias: String,
version: String,
device_model: Option<String>,
device_type: Option<DeviceType>,
fingerprint: String,
pin: Option<String>,
) -> anyhow::Result<RsHttpServer> {
let (event_tx, event_rx) = mpsc::channel::<ServerEventV2>(16);
let (stop_tx, stop_rx) = oneshot::channel::<()>();
localsend::http::server::start_with_port(
port,
tls,
ClientInfo {
alias,
version,
device_model,
device_type,
token: fingerprint,
},
None,
Some(ServerConfigV2 { pin, event_tx }),
None,
stop_rx,
)
.await?;
Ok(RsHttpServer {
event_rx: Mutex::new(Some(event_rx)),
stop_tx: Mutex::new(Some(stop_tx)),
pending_decision: Mutex::new(None),
pending_uploads: Mutex::new(HashMap::new()),
})
}
impl RsHttpServer {
/// Emits server events until the server is stopped.
/// Can only be listened to once.
pub async fn listen(&self, sink: StreamSink<RsServerEvent>) {
let Some(mut event_rx) = self.event_rx.lock().await.take() else {
let _ = sink.add_error(anyhow::anyhow!("Server events already listened to"));
return;
};
while let Some(event) = event_rx.recv().await {
match event {
ServerEventV2::Register { ip, info } => {
let _ = sink.add(RsServerEvent::Register {
ip: ip.to_string(),
info,
});
}
ServerEventV2::PrepareUpload {
ip,
info,
files,
decision_tx,
} => {
*self.pending_decision.lock().await = Some(decision_tx);
let _ = sink.add(RsServerEvent::PrepareUpload {
ip: ip.to_string(),
info,
files,
});
}
ServerEventV2::FileUpload {
session_id,
file_id,
file,
target_tx,
} => {
self.pending_uploads
.lock()
.await
.insert((session_id.clone(), file_id.clone()), target_tx);
let _ = sink.add(RsServerEvent::FileUpload {
session_id,
file_id,
file,
});
}
ServerEventV2::SessionEnd { session_id, reason } => {
// Drop stale upload responders of this session (their requests already ended).
self.pending_uploads
.lock()
.await
.retain(|(sid, _), _| sid != &session_id);
let _ = sink.add(RsServerEvent::SessionEnd { session_id, reason });
}
}
}
}
/// Answers the pending [RsServerEvent::PrepareUpload] event.
///
/// Passing the accepted file IDs (a subset of the offered files) accepts the request.
/// Passing `None` declines the request.
pub async fn respond_prepare_upload(
&self,
accepted_file_ids: Option<Vec<String>>,
) -> anyhow::Result<()> {
let Some(decision_tx) = self.pending_decision.lock().await.take() else {
return Err(anyhow::anyhow!("No pending prepare-upload request"));
};
let decision = match accepted_file_ids {
Some(ids) => PrepareUploadDecisionV2::Accept(ids.into_iter().collect()),
None => PrepareUploadDecisionV2::Decline,
};
decision_tx
.send(decision)
.map_err(|_| anyhow::anyhow!("Prepare-upload request already ended"))?;
Ok(())
}
/// Answers the pending [RsServerEvent::FileUpload] event with the target
/// the file should be saved to (either a path or a file descriptor)
/// and waits until the file has been received completely.
pub async fn respond_file_upload(
&self,
session_id: String,
file_id: String,
path: Option<String>,
file_descriptor: Option<i32>,
) -> anyhow::Result<()> {
let Some(target_tx) = self
.pending_uploads
.lock()
.await
.remove(&(session_id, file_id))
else {
return Err(anyhow::anyhow!("No pending file upload for this file"));
};
let (result_tx, result_rx) = oneshot::channel::<Result<(), String>>();
let target = resolve_upload_target(path, file_descriptor, result_tx)?;
target_tx
.send(target)
.map_err(|_| anyhow::anyhow!("Upload request already ended"))?;
match result_rx.await {
Ok(Ok(())) => Ok(()),
Ok(Err(err)) => Err(anyhow::anyhow!(err)),
Err(_) => Err(anyhow::anyhow!("Upload request aborted")),
}
}
/// Stops the server.
pub async fn stop(&self) {
if let Some(stop_tx) = self.stop_tx.lock().await.take() {
let _ = stop_tx.send(());
}
}
}
fn resolve_upload_target(
path: Option<String>,
file_descriptor: Option<i32>,
result_tx: oneshot::Sender<Result<(), String>>,
) -> anyhow::Result<FileUploadTarget> {
match (path, file_descriptor) {
(Some(path), None) => Ok(FileUploadTarget::Path {
path: path.into(),
result_tx,
}),
(None, Some(file_descriptor)) => {
#[cfg(target_os = "android")]
{
Ok(FileUploadTarget::Fd {
fd: file_descriptor,
result_tx,
})
}
#[cfg(not(target_os = "android"))]
{
let _ = (file_descriptor, result_tx);
Err(anyhow::anyhow!(
"File descriptors are only supported on Android"
))
}
}
_ => Err(anyhow::anyhow!(
"Exactly one upload target must be provided"
)),
}
}
#[frb(mirror(TlsConfig))]
pub struct _TlsConfig {
pub cert: String,
pub private_key: String,
}
#[frb(mirror(ProtocolTypeV2))]
pub enum _ProtocolTypeV2 {
Http,
Https,
}
#[frb(mirror(RegisterDtoV2))]
pub struct _RegisterDtoV2 {
pub alias: String,
pub version: String,
pub device_model: Option<String>,
pub device_type: Option<DeviceType>,
pub fingerprint: String,
pub port: u16,
pub protocol: ProtocolTypeV2,
pub download: bool,
}
#[frb(mirror(SessionEndReasonV2))]
pub enum _SessionEndReasonV2 {
Finished,
Cancelled,
}
+2 -2
View File
@@ -1,6 +1,6 @@
use crate::frb_generated::StreamSink; use crate::frb_generated::StreamSink;
use bytes::Bytes; use bytes::Bytes;
use flutter_rust_bridge::{frb, DartFnFuture}; use flutter_rust_bridge::{DartFnFuture, frb};
use localsend::crypto::token::SigningTokenKey; use localsend::crypto::token::SigningTokenKey;
use localsend::model::discovery::DeviceType; use localsend::model::discovery::DeviceType;
use localsend::model::transfer::FileDto; use localsend::model::transfer::FileDto;
@@ -14,7 +14,7 @@ pub use localsend::webrtc::webrtc::{
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::sync::{mpsc, oneshot, Mutex}; use tokio::sync::{Mutex, mpsc, oneshot};
use tokio::time; use tokio::time;
use uuid::Uuid; use uuid::Uuid;
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
use super::{ClientError, ResponseExt, ResultWithPublicKey}; use super::{ClientError, ResponseExt, ResultWithPublicKey};
use crate::http;
use crate::http::client::url::{ApiVersion, TargetUrl}; use crate::http::client::url::{ApiVersion, TargetUrl};
use crate::http::dto::ProtocolType; use crate::http::dto::ProtocolType;
use crate::http;
use crate::{crypto, util}; use crate::{crypto, util};
use lru::LruCache; use lru::LruCache;
use reqwest::{Response, StatusCode}; use reqwest::{Response, StatusCode};
+1 -1
View File
@@ -36,7 +36,7 @@ impl FileContent {
FileContent::Stream(rx) => { FileContent::Stream(rx) => {
tracing::info!("Reading file content via byte stream from application"); tracing::info!("Reading file content via byte stream from application");
rx rx
}, }
FileContent::Path(path) => { FileContent::Path(path) => {
tracing::info!("Reading file content from path: {}", path.display()); tracing::info!("Reading file content from path: {}", path.display());
let (tx, rx) = mpsc::channel(FILE_CHANNEL_CAPACITY); let (tx, rx) = mpsc::channel(FILE_CHANNEL_CAPACITY);