diff --git a/app/lib/isolate/src/isolate/child/server_isolate.dart b/app/lib/isolate/src/isolate/child/server_isolate.dart index d72f48de..09c7e7f1 100644 --- a/app/lib/isolate/src/isolate/child/server_isolate.dart +++ b/app/lib/isolate/src/isolate/child/server_isolate.dart @@ -20,8 +20,18 @@ class HttpServerStartTask implements BaseHttpServerTask { /// Optional PIN that senders must provide to start an upload session. final String? pin; + /// Enables web send (download API) so web browsers can download the offered files. + /// `null` disables web send. + final WebSendParams? webSend; + + /// Enables the internal `show` endpoint, guarded by this token, that lets another + /// application instance request this one to show itself. `null` disables it. + final String? showToken; + HttpServerStartTask({ required this.pin, + required this.webSend, + required this.showToken, }); } @@ -59,6 +69,37 @@ class HttpServerFileUploadTargetTask implements BaseHttpServerTask { }); } +/// Answers a pending [HttpServerWebPrepareDownloadEvent]. +class HttpServerPrepareDownloadDecisionTask implements BaseHttpServerTask { + final String sessionId; + + /// `true` accepts the download request, `false` declines it. + final bool accept; + + HttpServerPrepareDownloadDecisionTask({ + required this.sessionId, + required this.accept, + }); +} + +/// Answers a pending [HttpServerWebFileDownloadEvent] with the source the file +/// content should be read from: either a file [path] or a readable [fileDescriptor] (Android). +/// +/// The file is read and streamed by the Rust server itself. +class HttpServerFileDownloadTargetTask implements BaseHttpServerTask { + final String sessionId; + final String fileId; + final String? path; + final int? fileDescriptor; + + HttpServerFileDownloadTargetTask({ + 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 {} @@ -127,6 +168,44 @@ class HttpServerSessionEndEvent extends HttpServerEvent { }); } +/// A web client requests to download the shared files. +/// Must be answered with a [HttpServerPrepareDownloadDecisionTask]. +class HttpServerWebPrepareDownloadEvent extends HttpServerEvent { + final String ip; + final String sessionId; + final String? userAgent; + + HttpServerWebPrepareDownloadEvent({ + required this.ip, + required this.sessionId, + required this.userAgent, + }); +} + +/// A web client downloads an offered file. +/// Must be answered with a [HttpServerFileDownloadTargetTask]. +class HttpServerWebFileDownloadEvent extends HttpServerEvent { + final String sessionId; + final String fileId; + final FileDto file; + + HttpServerWebFileDownloadEvent({ + required this.sessionId, + required this.fileId, + required this.file, + }); +} + +/// Another application instance requested the running application to show itself. +class HttpServerShowEvent extends HttpServerEvent { + /// Command-line arguments forwarded by the other application instance. + final List args; + + HttpServerShowEvent({ + required this.args, + }); +} + Future setupHttpServerIsolate( Stream>> receiveFromMain, void Function(IsolateTaskStreamResult) sendToMain, @@ -157,6 +236,8 @@ Future setupHttpServerIsolate( deviceType: syncState.deviceInfo.deviceType.toRust(), fingerprint: syncState.securityContext.certificateHash, pin: startTask.pin, + webSend: startTask.webSend, + showToken: startTask.showToken, ); try { @@ -180,6 +261,17 @@ Future setupHttpServerIsolate( sessionId: sessionId, reason: reason, ), + RsServerEvent_WebPrepareDownload(:final ip, :final sessionId, :final userAgent) => HttpServerWebPrepareDownloadEvent( + ip: ip, + sessionId: sessionId, + userAgent: userAgent, + ), + RsServerEvent_WebFileDownload(:final sessionId, :final fileId, :final file) => HttpServerWebFileDownloadEvent( + sessionId: sessionId, + fileId: fileId, + file: file, + ), + RsServerEvent_Show(:final args) => HttpServerShowEvent(args: args), }, ), ); @@ -229,6 +321,24 @@ Future setupHttpServerIsolate( ), ); return; + case HttpServerPrepareDownloadDecisionTask decisionTask: + await ref + .read(httpServerProvider) + .respondPrepareDownload( + sessionId: decisionTask.sessionId, + accept: decisionTask.accept, + ); + return; + case HttpServerFileDownloadTargetTask targetTask: + await ref + .read(httpServerProvider) + .respondFileDownload( + sessionId: targetTask.sessionId, + fileId: targetTask.fileId, + path: targetTask.path, + fileDescriptor: targetTask.fileDescriptor, + ); + return; } }, ); diff --git a/app/lib/isolate/src/isolate/parent/actions.dart b/app/lib/isolate/src/isolate/parent/actions.dart index ca57a4d2..b21d2647 100644 --- a/app/lib/isolate/src/isolate/parent/actions.dart +++ b/app/lib/isolate/src/isolate/parent/actions.dart @@ -7,6 +7,7 @@ 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/dto/send_to_isolate_data.dart'; import 'package:localsend_app/isolate/src/isolate/parent/parent_isolate_provider.dart'; +import 'package:localsend_app/rust/api/server.dart' show WebSendParams; import 'package:refena_flutter/refena_flutter.dart'; import 'package:typed_isolates/id.dart'; import 'package:typed_isolates/typed_isolates.dart'; @@ -193,8 +194,18 @@ class IsolateHttpUploadCancelAction extends ReduxAction> { final String? pin; + /// Enables web send (download API) so web browsers can download the offered files. + /// `null` disables web send. + final WebSendParams? webSend; + + /// Enables the internal `show` endpoint, guarded by this token, that lets another + /// application instance request this one to show itself. `null` disables it. + final String? showToken; + IsolateHttpServerStartAction({ required this.pin, + required this.webSend, + required this.showToken, }); @override @@ -209,6 +220,8 @@ class IsolateHttpServerStartAction extends ReduxActionWithResult { + final String sessionId; + + /// `true` accepts the download request, `false` declines it. + final bool accept; + + IsolateHttpServerPrepareDownloadDecisionAction({ + required this.sessionId, + required this.accept, + }); + + @override + ParentIsolateState reduce() { + final connection = state.httpServer; + if (connection == null) { + throw StateError('httpServer is not initialized'); + } + + connection.sendToIsolate( + SendToIsolateData( + syncState: null, + data: IsolateTask( + data: HttpServerPrepareDownloadDecisionTask( + sessionId: sessionId, + accept: accept, + ), + ), + ), + ); + + return state; + } +} + +/// Answers a pending [HttpServerWebFileDownloadEvent] with the source the file +/// content should be read from (either a [path] or a readable [fileDescriptor]). +class IsolateHttpServerFileDownloadTargetAction extends ReduxAction { + final String sessionId; + final String fileId; + final String? path; + final int? fileDescriptor; + + IsolateHttpServerFileDownloadTargetAction({ + required this.sessionId, + required this.fileId, + required this.path, + required this.fileDescriptor, + }); + + @override + ParentIsolateState reduce() { + final connection = state.httpServer; + if (connection == null) { + throw StateError('httpServer is not initialized'); + } + + connection.sendToIsolate( + SendToIsolateData( + syncState: null, + data: IsolateTask( + data: HttpServerFileDownloadTargetTask( + sessionId: sessionId, + fileId: fileId, + path: path, + fileDescriptor: fileDescriptor, + ), + ), + ), + ); + + return state; + } +} + /// Saving a file received by the HTTP server failed. class HttpServerFileUploadException implements Exception { final String message; diff --git a/app/lib/isolate/src/task/server/http_server.dart b/app/lib/isolate/src/task/server/http_server.dart index 3bce3d09..59e9e570 100644 --- a/app/lib/isolate/src/task/server/http_server.dart +++ b/app/lib/isolate/src/task/server/http_server.dart @@ -22,6 +22,8 @@ class HttpServerService { required DeviceType? deviceType, required String fingerprint, required String? pin, + required WebSendParams? webSend, + required String? showToken, }) async { if (_server != null) { throw StateError('Server already running'); @@ -36,6 +38,8 @@ class HttpServerService { deviceType: deviceType, fingerprint: fingerprint, pin: pin, + webSend: webSend, + showToken: showToken, ); _server = server; return server.listen(); @@ -63,6 +67,28 @@ class HttpServerService { ); } + /// Answers a pending web prepare-download request. + /// [accept] grants the download; `false` declines it. + Future respondPrepareDownload({required String sessionId, required bool accept}) async { + await _requireServer().respondPrepareDownload(sessionId: sessionId, accept: accept); + } + + /// Answers a pending web file download with the source the file content should be + /// read from (either a [path] or a [fileDescriptor]). The server streams the content. + Future respondFileDownload({ + required String sessionId, + required String fileId, + required String? path, + required int? fileDescriptor, + }) async { + await _requireServer().respondFileDownload( + sessionId: sessionId, + fileId: fileId, + path: path, + fileDescriptor: fileDescriptor, + ); + } + /// Stops the server. The event stream returned by [start] will end. Future stop() async { final server = _server; diff --git a/app/lib/rust/api/server.dart b/app/lib/rust/api/server.dart index 1ca01342..f0e1a841 100644 --- a/app/lib/rust/api/server.dart +++ b/app/lib/rust/api/server.dart @@ -10,11 +10,18 @@ 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` +// These functions are ignored because they are not marked as `pub`: `handle_server_event`, `handle_web_event`, `recv_opt`, `resolve_file_content`, `resolve_upload_target` /// Starts the HTTP server on the given port (IPv4 and IPv6). /// The server runs until [RsHttpServer::stop] is called. /// +/// Passing [web_send] additionally enables the web send (download API) so that +/// web browsers can download the offered files. +/// +/// Passing [show_token] enables the internal `show` endpoint that lets another +/// application instance request this one to show itself (emitted as +/// [RsServerEvent::Show]). The token guards the endpoint against other clients. +/// /// Events are received by listening to [RsHttpServer::listen]. Future startServer({ required int port, @@ -25,6 +32,8 @@ Future startServer({ DeviceType? deviceType, required String fingerprint, String? pin, + WebSendParams? webSend, + String? showToken, }) => RustLib.instance.api.crateApiServerStartServer( port: port, tls: tls, @@ -34,19 +43,35 @@ Future startServer({ deviceType: deviceType, fingerprint: fingerprint, pin: pin, + webSend: webSend, + showToken: showToken, ); // Rust type: RustOpaqueMoi> abstract class RsHttpServer implements RustOpaqueInterface { /// Emits server events until the server is stopped. /// Can only be listened to once. + /// + /// The v2 protocol, the web send (download API), and the internal endpoint + /// events are all emitted on the same stream. Stream listen(); + /// Answers the pending [RsServerEvent::WebFileDownload] event with the source + /// the file content should be read from (either a path or a file descriptor). + /// + /// The server reads the content and streams it to the web client. + Future respondFileDownload({required String sessionId, required String fileId, String? path, int? fileDescriptor}); + /// 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 respondFileUpload({required String sessionId, required String fileId, String? path, int? fileDescriptor}); + /// Answers the pending [RsServerEvent::WebPrepareDownload] event. + /// + /// Passing `true` accepts the download request, `false` declines it. + Future respondPrepareDownload({required String sessionId, required bool accept}); + /// Answers the pending [RsServerEvent::PrepareUpload] event. /// /// Passing the accepted file IDs (a subset of the offered files) accepts the request. @@ -138,6 +163,31 @@ sealed class RsServerEvent with _$RsServerEvent { required String sessionId, required SessionEndReasonV2 reason, }) = RsServerEvent_SessionEnd; + + /// A web client requests to download the shared files via `POST /api/localsend/v2/prepare-download`. + /// + /// Must be answered with [RsHttpServer::respond_prepare_download]. + const factory RsServerEvent.webPrepareDownload({ + required String ip, + required String sessionId, + String? userAgent, + }) = RsServerEvent_WebPrepareDownload; + + /// A web client downloads an offered file via `GET /api/localsend/v2/download`. + /// + /// Must be answered with [RsHttpServer::respond_file_download]. + const factory RsServerEvent.webFileDownload({ + required String sessionId, + required String fileId, + required FileDto file, + }) = RsServerEvent_WebFileDownload; + + /// Another application instance requested the running application to show itself + /// via `POST /api/localsend/v2/show`. + const factory RsServerEvent.show_({ + /// Command-line arguments forwarded by the other application instance. + required List args, + }) = RsServerEvent_Show; } enum SessionEndReasonV2 { @@ -161,3 +211,80 @@ class TlsConfig { bool operator ==(Object other) => identical(this, other) || other is TlsConfig && runtimeType == other.runtimeType && cert == other.cert && privateKey == other.privateKey; } + +class WebSendI18n { + final String waiting; + final String enterPin; + final String invalidPin; + final String tooManyAttempts; + final String rejected; + final String files; + final String fileName; + final String size; + + const WebSendI18n({ + required this.waiting, + required this.enterPin, + required this.invalidPin, + required this.tooManyAttempts, + required this.rejected, + required this.files, + required this.fileName, + required this.size, + }); + + @override + int get hashCode => + waiting.hashCode ^ + enterPin.hashCode ^ + invalidPin.hashCode ^ + tooManyAttempts.hashCode ^ + rejected.hashCode ^ + files.hashCode ^ + fileName.hashCode ^ + size.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is WebSendI18n && + runtimeType == other.runtimeType && + waiting == other.waiting && + enterPin == other.enterPin && + invalidPin == other.invalidPin && + tooManyAttempts == other.tooManyAttempts && + rejected == other.rejected && + files == other.files && + fileName == other.fileName && + size == other.size; +} + +/// Configuration for web send: files offered for download by web browsers. +/// +/// Web send can be enabled independently of the v2 protocol endpoints. When +/// omitted, the download API responds with 403 and only the v2 endpoints run. +class WebSendParams { + /// The metadata of the files offered for download, mapped by file ID. + /// The content is requested per download via [RsServerEvent::WebFileDownload]. + final Map files; + + /// Optional PIN that web clients must provide via the `pin` query parameter. + final String? pin; + + /// Translations for the web page, served via `/i18n.json`. + final WebSendI18n i18N; + + const WebSendParams({ + required this.files, + this.pin, + required this.i18N, + }); + + @override + int get hashCode => files.hashCode ^ pin.hashCode ^ i18N.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is WebSendParams && runtimeType == other.runtimeType && files == other.files && pin == other.pin && i18N == other.i18N; +} diff --git a/app/lib/rust/api/server.freezed.dart b/app/lib/rust/api/server.freezed.dart index 642209ff..6ed90af7 100644 --- a/app/lib/rust/api/server.freezed.dart +++ b/app/lib/rust/api/server.freezed.dart @@ -55,14 +55,17 @@ extension RsServerEventPatterns on RsServerEvent { /// } /// ``` -@optionalTypeArgs TResult maybeMap({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(),}){ +@optionalTypeArgs TResult maybeMap({TResult Function( RsServerEvent_Register value)? register,TResult Function( RsServerEvent_PrepareUpload value)? prepareUpload,TResult Function( RsServerEvent_FileUpload value)? fileUpload,TResult Function( RsServerEvent_SessionEnd value)? sessionEnd,TResult Function( RsServerEvent_WebPrepareDownload value)? webPrepareDownload,TResult Function( RsServerEvent_WebFileDownload value)? webFileDownload,TResult Function( RsServerEvent_Show value)? show_,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 sessionEnd(_that);case RsServerEvent_WebPrepareDownload() when webPrepareDownload != null: +return webPrepareDownload(_that);case RsServerEvent_WebFileDownload() when webFileDownload != null: +return webFileDownload(_that);case RsServerEvent_Show() when show_ != null: +return show_(_that);case _: return orElse(); } @@ -80,14 +83,17 @@ return sessionEnd(_that);case _: /// } /// ``` -@optionalTypeArgs TResult map({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,}){ +@optionalTypeArgs TResult map({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,required TResult Function( RsServerEvent_WebPrepareDownload value) webPrepareDownload,required TResult Function( RsServerEvent_WebFileDownload value) webFileDownload,required TResult Function( RsServerEvent_Show value) show_,}){ 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);} +return sessionEnd(_that);case RsServerEvent_WebPrepareDownload(): +return webPrepareDownload(_that);case RsServerEvent_WebFileDownload(): +return webFileDownload(_that);case RsServerEvent_Show(): +return show_(_that);} } /// A variant of `map` that fallback to returning `null`. /// @@ -101,14 +107,17 @@ return sessionEnd(_that);} /// } /// ``` -@optionalTypeArgs TResult? mapOrNull({TResult? Function( RsServerEvent_Register value)? register,TResult? Function( RsServerEvent_PrepareUpload value)? prepareUpload,TResult? Function( RsServerEvent_FileUpload value)? fileUpload,TResult? Function( RsServerEvent_SessionEnd value)? sessionEnd,}){ +@optionalTypeArgs TResult? mapOrNull({TResult? Function( RsServerEvent_Register value)? register,TResult? Function( RsServerEvent_PrepareUpload value)? prepareUpload,TResult? Function( RsServerEvent_FileUpload value)? fileUpload,TResult? Function( RsServerEvent_SessionEnd value)? sessionEnd,TResult? Function( RsServerEvent_WebPrepareDownload value)? webPrepareDownload,TResult? Function( RsServerEvent_WebFileDownload value)? webFileDownload,TResult? Function( RsServerEvent_Show value)? show_,}){ 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 sessionEnd(_that);case RsServerEvent_WebPrepareDownload() when webPrepareDownload != null: +return webPrepareDownload(_that);case RsServerEvent_WebFileDownload() when webFileDownload != null: +return webFileDownload(_that);case RsServerEvent_Show() when show_ != null: +return show_(_that);case _: return null; } @@ -125,13 +134,16 @@ return sessionEnd(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen({TResult Function( String ip, RegisterDtoV2 info)? register,TResult Function( String ip, RegisterDtoV2 info, Map files)? prepareUpload,TResult Function( String sessionId, String fileId, FileDto file)? fileUpload,TResult Function( String sessionId, SessionEndReasonV2 reason)? sessionEnd,required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen({TResult Function( String ip, RegisterDtoV2 info)? register,TResult Function( String ip, RegisterDtoV2 info, Map files)? prepareUpload,TResult Function( String sessionId, String fileId, FileDto file)? fileUpload,TResult Function( String sessionId, SessionEndReasonV2 reason)? sessionEnd,TResult Function( String ip, String sessionId, String? userAgent)? webPrepareDownload,TResult Function( String sessionId, String fileId, FileDto file)? webFileDownload,TResult Function( List args)? show_,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 sessionEnd(_that.sessionId,_that.reason);case RsServerEvent_WebPrepareDownload() when webPrepareDownload != null: +return webPrepareDownload(_that.ip,_that.sessionId,_that.userAgent);case RsServerEvent_WebFileDownload() when webFileDownload != null: +return webFileDownload(_that.sessionId,_that.fileId,_that.file);case RsServerEvent_Show() when show_ != null: +return show_(_that.args);case _: return orElse(); } @@ -149,13 +161,16 @@ return sessionEnd(_that.sessionId,_that.reason);case _: /// } /// ``` -@optionalTypeArgs TResult when({required TResult Function( String ip, RegisterDtoV2 info) register,required TResult Function( String ip, RegisterDtoV2 info, Map files) prepareUpload,required TResult Function( String sessionId, String fileId, FileDto file) fileUpload,required TResult Function( String sessionId, SessionEndReasonV2 reason) sessionEnd,}) {final _that = this; +@optionalTypeArgs TResult when({required TResult Function( String ip, RegisterDtoV2 info) register,required TResult Function( String ip, RegisterDtoV2 info, Map files) prepareUpload,required TResult Function( String sessionId, String fileId, FileDto file) fileUpload,required TResult Function( String sessionId, SessionEndReasonV2 reason) sessionEnd,required TResult Function( String ip, String sessionId, String? userAgent) webPrepareDownload,required TResult Function( String sessionId, String fileId, FileDto file) webFileDownload,required TResult Function( List args) show_,}) {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);} +return sessionEnd(_that.sessionId,_that.reason);case RsServerEvent_WebPrepareDownload(): +return webPrepareDownload(_that.ip,_that.sessionId,_that.userAgent);case RsServerEvent_WebFileDownload(): +return webFileDownload(_that.sessionId,_that.fileId,_that.file);case RsServerEvent_Show(): +return show_(_that.args);} } /// A variant of `when` that fallback to returning `null` /// @@ -169,13 +184,16 @@ return sessionEnd(_that.sessionId,_that.reason);} /// } /// ``` -@optionalTypeArgs TResult? whenOrNull({TResult? Function( String ip, RegisterDtoV2 info)? register,TResult? Function( String ip, RegisterDtoV2 info, Map files)? prepareUpload,TResult? Function( String sessionId, String fileId, FileDto file)? fileUpload,TResult? Function( String sessionId, SessionEndReasonV2 reason)? sessionEnd,}) {final _that = this; +@optionalTypeArgs TResult? whenOrNull({TResult? Function( String ip, RegisterDtoV2 info)? register,TResult? Function( String ip, RegisterDtoV2 info, Map files)? prepareUpload,TResult? Function( String sessionId, String fileId, FileDto file)? fileUpload,TResult? Function( String sessionId, SessionEndReasonV2 reason)? sessionEnd,TResult? Function( String ip, String sessionId, String? userAgent)? webPrepareDownload,TResult? Function( String sessionId, String fileId, FileDto file)? webFileDownload,TResult? Function( List args)? show_,}) {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 sessionEnd(_that.sessionId,_that.reason);case RsServerEvent_WebPrepareDownload() when webPrepareDownload != null: +return webPrepareDownload(_that.ip,_that.sessionId,_that.userAgent);case RsServerEvent_WebFileDownload() when webFileDownload != null: +return webFileDownload(_that.sessionId,_that.fileId,_that.file);case RsServerEvent_Show() when show_ != null: +return show_(_that.args);case _: return null; } @@ -463,6 +481,220 @@ as SessionEndReasonV2, } +} + +/// @nodoc + + +class RsServerEvent_WebPrepareDownload extends RsServerEvent { + const RsServerEvent_WebPrepareDownload({required this.ip, required this.sessionId, this.userAgent}): super._(); + + + final String ip; + final String sessionId; + final String? userAgent; + +/// 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_WebPrepareDownloadCopyWith get copyWith => _$RsServerEvent_WebPrepareDownloadCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is RsServerEvent_WebPrepareDownload&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.sessionId, sessionId) || other.sessionId == sessionId)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)); +} + + +@override +int get hashCode => Object.hash(runtimeType,ip,sessionId,userAgent); + +@override +String toString() { + return 'RsServerEvent.webPrepareDownload(ip: $ip, sessionId: $sessionId, userAgent: $userAgent)'; +} + + +} + +/// @nodoc +abstract mixin class $RsServerEvent_WebPrepareDownloadCopyWith<$Res> implements $RsServerEventCopyWith<$Res> { + factory $RsServerEvent_WebPrepareDownloadCopyWith(RsServerEvent_WebPrepareDownload value, $Res Function(RsServerEvent_WebPrepareDownload) _then) = _$RsServerEvent_WebPrepareDownloadCopyWithImpl; +@useResult +$Res call({ + String ip, String sessionId, String? userAgent +}); + + + + +} +/// @nodoc +class _$RsServerEvent_WebPrepareDownloadCopyWithImpl<$Res> + implements $RsServerEvent_WebPrepareDownloadCopyWith<$Res> { + _$RsServerEvent_WebPrepareDownloadCopyWithImpl(this._self, this._then); + + final RsServerEvent_WebPrepareDownload _self; + final $Res Function(RsServerEvent_WebPrepareDownload) _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? sessionId = null,Object? userAgent = freezed,}) { + return _then(RsServerEvent_WebPrepareDownload( +ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable +as String,sessionId: null == sessionId ? _self.sessionId : sessionId // ignore: cast_nullable_to_non_nullable +as String,userAgent: freezed == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +/// @nodoc + + +class RsServerEvent_WebFileDownload extends RsServerEvent { + const RsServerEvent_WebFileDownload({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_WebFileDownloadCopyWith get copyWith => _$RsServerEvent_WebFileDownloadCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is RsServerEvent_WebFileDownload&&(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.webFileDownload(sessionId: $sessionId, fileId: $fileId, file: $file)'; +} + + +} + +/// @nodoc +abstract mixin class $RsServerEvent_WebFileDownloadCopyWith<$Res> implements $RsServerEventCopyWith<$Res> { + factory $RsServerEvent_WebFileDownloadCopyWith(RsServerEvent_WebFileDownload value, $Res Function(RsServerEvent_WebFileDownload) _then) = _$RsServerEvent_WebFileDownloadCopyWithImpl; +@useResult +$Res call({ + String sessionId, String fileId, FileDto file +}); + + + + +} +/// @nodoc +class _$RsServerEvent_WebFileDownloadCopyWithImpl<$Res> + implements $RsServerEvent_WebFileDownloadCopyWith<$Res> { + _$RsServerEvent_WebFileDownloadCopyWithImpl(this._self, this._then); + + final RsServerEvent_WebFileDownload _self; + final $Res Function(RsServerEvent_WebFileDownload) _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_WebFileDownload( +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_Show extends RsServerEvent { + const RsServerEvent_Show({required final List args}): _args = args,super._(); + + +/// Command-line arguments forwarded by the other application instance. + final List _args; +/// Command-line arguments forwarded by the other application instance. + List get args { + if (_args is EqualUnmodifiableListView) return _args; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_args); +} + + +/// 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_ShowCopyWith get copyWith => _$RsServerEvent_ShowCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is RsServerEvent_Show&&const DeepCollectionEquality().equals(other._args, _args)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_args)); + +@override +String toString() { + return 'RsServerEvent.show_(args: $args)'; +} + + +} + +/// @nodoc +abstract mixin class $RsServerEvent_ShowCopyWith<$Res> implements $RsServerEventCopyWith<$Res> { + factory $RsServerEvent_ShowCopyWith(RsServerEvent_Show value, $Res Function(RsServerEvent_Show) _then) = _$RsServerEvent_ShowCopyWithImpl; +@useResult +$Res call({ + List args +}); + + + + +} +/// @nodoc +class _$RsServerEvent_ShowCopyWithImpl<$Res> + implements $RsServerEvent_ShowCopyWith<$Res> { + _$RsServerEvent_ShowCopyWithImpl(this._self, this._then); + + final RsServerEvent_Show _self; + final $Res Function(RsServerEvent_Show) _then; + +/// Create a copy of RsServerEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? args = null,}) { + return _then(RsServerEvent_Show( +args: null == args ? _self._args : args // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + } // dart format on diff --git a/app/lib/rust/frb_generated.dart b/app/lib/rust/frb_generated.dart index 1f0adb98..0e8d9df0 100644 --- a/app/lib/rust/frb_generated.dart +++ b/app/lib/rust/frb_generated.dart @@ -72,7 +72,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => -1220219761; + int get rustContentHash => -1029282510; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( stem: 'rust_lib_localsend_app', @@ -154,6 +154,14 @@ abstract class RustLibApi extends BaseApi { Stream crateApiServerRsHttpServerListen({required RsHttpServer that}); + Future crateApiServerRsHttpServerRespondFileDownload({ + required RsHttpServer that, + required String sessionId, + required String fileId, + String? path, + int? fileDescriptor, + }); + Future crateApiServerRsHttpServerRespondFileUpload({ required RsHttpServer that, required String sessionId, @@ -162,6 +170,8 @@ abstract class RustLibApi extends BaseApi { int? fileDescriptor, }); + Future crateApiServerRsHttpServerRespondPrepareDownload({required RsHttpServer that, required String sessionId, required bool accept}); + Future crateApiServerRsHttpServerRespondPrepareUpload({required RsHttpServer that, List? acceptedFileIds}); Future crateApiServerRsHttpServerStop({required RsHttpServer that}); @@ -224,6 +234,8 @@ abstract class RustLibApi extends BaseApi { DeviceType? deviceType, required String fingerprint, String? pin, + WebSendParams? webSend, + String? showToken, }); Future crateApiCryptoVerifyCert({required String cert, required String publicKey}); @@ -689,7 +701,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateApiServerRsHttpServerRespondFileUpload({ + Future crateApiServerRsHttpServerRespondFileDownload({ required RsHttpServer that, required String sessionId, required String fileId, @@ -711,6 +723,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), + constMeta: kCrateApiServerRsHttpServerRespondFileDownloadConstMeta, + argValues: [that, sessionId, fileId, path, fileDescriptor], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiServerRsHttpServerRespondFileDownloadConstMeta => const TaskConstMeta( + debugName: 'RsHttpServer_respond_file_download', + argNames: ['that', 'sessionId', 'fileId', 'path', 'fileDescriptor'], + ); + + @override + Future crateApiServerRsHttpServerRespondFileUpload({ + required RsHttpServer that, + required String sessionId, + required String fileId, + String? path, + int? fileDescriptor, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(that, serializer); + sse_encode_String(sessionId, serializer); + sse_encode_String(fileId, serializer); + sse_encode_opt_String(path, serializer); + sse_encode_opt_box_autoadd_i_32(fileDescriptor, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), constMeta: kCrateApiServerRsHttpServerRespondFileUploadConstMeta, argValues: [that, sessionId, fileId, path, fileDescriptor], apiImpl: this, @@ -723,6 +770,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ['that', 'sessionId', 'fileId', 'path', 'fileDescriptor'], ); + @override + Future crateApiServerRsHttpServerRespondPrepareDownload({required RsHttpServer that, required String sessionId, required bool accept}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(that, serializer); + sse_encode_String(sessionId, serializer); + sse_encode_bool(accept, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiServerRsHttpServerRespondPrepareDownloadConstMeta, + argValues: [that, sessionId, accept], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiServerRsHttpServerRespondPrepareDownloadConstMeta => const TaskConstMeta( + debugName: 'RsHttpServer_respond_prepare_download', + argNames: ['that', 'sessionId', 'accept'], + ); + @override Future crateApiServerRsHttpServerRespondPrepareUpload({required RsHttpServer that, List? acceptedFileIds}) { return handler.executeNormal( @@ -731,7 +805,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(that, serializer); sse_encode_opt_list_String(acceptedFileIds, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -756,7 +830,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(that, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -781,7 +855,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCFileReceiver(that, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -809,7 +883,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCFileReceiver(that, serializer); sse_encode_StreamSink_list_prim_u_8_strict_Sse(sink, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -837,7 +911,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCFileSender(that, serializer); sse_encode_list_prim_u_8_loose(data, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -862,7 +936,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -890,7 +964,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer); sse_encode_StreamSink_rtc_file_error_Sse(sink, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -917,7 +991,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_list_file_dto, @@ -945,7 +1019,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer); sse_encode_StreamSink_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCFileReceiver_Sse(sink, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -975,7 +1049,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer); sse_encode_StreamSink_rtc_status_Sse(sink, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1003,7 +1077,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer); sse_encode_box_autoadd_rtc_send_file_response(status, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1029,7 +1103,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer); sse_encode_String(pin, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1055,7 +1129,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer); sse_encode_Set_String_None(selection, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 27, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1083,7 +1157,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCSendController(that, serializer); sse_encode_StreamSink_rtc_file_error_Sse(sink, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1110,7 +1184,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCSendController(that, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 27, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_Set_String_None, @@ -1138,7 +1212,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCSendController(that, serializer); sse_encode_StreamSink_rtc_status_Sse(sink, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1166,7 +1240,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCSendController(that, serializer); sse_encode_String(fileId, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 31, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCFileSender, @@ -1192,7 +1266,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCSendController(that, serializer); sse_encode_String(pin, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 32, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1231,7 +1305,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { onConnection, serializer, ); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 31, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 33, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1257,7 +1331,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 32)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 34)!; }, codec: SseCodec( decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken, @@ -1285,7 +1359,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(cert, serializer); sse_encode_ls_http_client_version(version, serializer); sse_encode_opt_box_autoadd_u_32(timeoutMs, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 33)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 35)!; }, codec: SseCodec( decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient, @@ -1309,7 +1383,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 34, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 36, port: port_); }, codec: SseCodec( decodeSuccessData: @@ -1334,7 +1408,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 35, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1358,7 +1432,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 36, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 38, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_key_pair, @@ -1386,6 +1460,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { DeviceType? deviceType, required String fingerprint, String? pin, + WebSendParams? webSend, + String? showToken, }) { return handler.executeNormal( NormalTask( @@ -1399,14 +1475,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_box_autoadd_device_type(deviceType, serializer); sse_encode_String(fingerprint, serializer); sse_encode_opt_String(pin, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37, port: port_); + sse_encode_opt_box_autoadd_web_send_params(webSend, serializer); + sse_encode_opt_String(showToken, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 39, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateApiServerStartServerConstMeta, - argValues: [port, tls, alias, version, deviceModel, deviceType, fingerprint, pin], + argValues: [port, tls, alias, version, deviceModel, deviceType, fingerprint, pin, webSend, showToken], apiImpl: this, ), ); @@ -1414,7 +1492,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiServerStartServerConstMeta => const TaskConstMeta( debugName: 'start_server', - argNames: ['port', 'tls', 'alias', 'version', 'deviceModel', 'deviceType', 'fingerprint', 'pin'], + argNames: ['port', 'tls', 'alias', 'version', 'deviceModel', 'deviceType', 'fingerprint', 'pin', 'webSend', 'showToken'], ); @override @@ -1425,7 +1503,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(cert, serializer); sse_encode_String(publicKey, serializer); - pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 38, port: port_); + pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 40, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1918,6 +1996,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as int; } + @protected + WebSendParams dco_decode_box_autoadd_web_send_params(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_web_send_params(raw); + } + @protected WsServerSdpMessage dco_decode_box_autoadd_ws_server_sdp_message(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2137,6 +2221,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_box_autoadd_u_32(raw); } + @protected + WebSendParams? dco_decode_opt_box_autoadd_web_send_params(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_web_send_params(raw); + } + @protected List? dco_decode_opt_list_String(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2370,6 +2460,22 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sessionId: dco_decode_String(raw[1]), reason: dco_decode_session_end_reason_v_2(raw[2]), ); + case 4: + return RsServerEvent_WebPrepareDownload( + ip: dco_decode_String(raw[1]), + sessionId: dco_decode_String(raw[2]), + userAgent: dco_decode_opt_String(raw[3]), + ); + case 5: + return RsServerEvent_WebFileDownload( + sessionId: dco_decode_String(raw[1]), + fileId: dco_decode_String(raw[2]), + file: dco_decode_box_autoadd_file_dto(raw[3]), + ); + case 6: + return RsServerEvent_Show( + args: dco_decode_list_String(raw[1]), + ); default: throw Exception('unreachable'); } @@ -2478,6 +2584,35 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dcoDecodeU64(raw); } + @protected + WebSendI18n dco_decode_web_send_i_18_n(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 8) throw Exception('unexpected arr length: expect 8 but see ${arr.length}'); + return WebSendI18n( + waiting: dco_decode_String(arr[0]), + enterPin: dco_decode_String(arr[1]), + invalidPin: dco_decode_String(arr[2]), + tooManyAttempts: dco_decode_String(arr[3]), + rejected: dco_decode_String(arr[4]), + files: dco_decode_String(arr[5]), + fileName: dco_decode_String(arr[6]), + size: dco_decode_String(arr[7]), + ); + } + + @protected + WebSendParams dco_decode_web_send_params(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 3) throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + return WebSendParams( + files: dco_decode_Map_String_file_dto_None(arr[0]), + pin: dco_decode_opt_String(arr[1]), + i18N: dco_decode_web_send_i_18_n(arr[2]), + ); + } + @protected WsServerMessage dco_decode_ws_server_message(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2937,6 +3072,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_u_32(deserializer)); } + @protected + WebSendParams sse_decode_box_autoadd_web_send_params(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_web_send_params(deserializer)); + } + @protected WsServerSdpMessage sse_decode_box_autoadd_ws_server_sdp_message(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -3230,6 +3371,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + WebSendParams? sse_decode_opt_box_autoadd_web_send_params(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_web_send_params(deserializer)); + } else { + return null; + } + } + @protected List? sse_decode_opt_list_String(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -3448,6 +3600,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_sessionId = sse_decode_String(deserializer); var var_reason = sse_decode_session_end_reason_v_2(deserializer); return RsServerEvent_SessionEnd(sessionId: var_sessionId, reason: var_reason); + case 4: + var var_ip = sse_decode_String(deserializer); + var var_sessionId = sse_decode_String(deserializer); + var var_userAgent = sse_decode_opt_String(deserializer); + return RsServerEvent_WebPrepareDownload(ip: var_ip, sessionId: var_sessionId, userAgent: var_userAgent); + case 5: + var var_sessionId = sse_decode_String(deserializer); + var var_fileId = sse_decode_String(deserializer); + var var_file = sse_decode_box_autoadd_file_dto(deserializer); + return RsServerEvent_WebFileDownload(sessionId: var_sessionId, fileId: var_fileId, file: var_file); + case 6: + var var_args = sse_decode_list_String(deserializer); + return RsServerEvent_Show(args: var_args); default: throw UnimplementedError(''); } @@ -3548,6 +3713,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getBigUint64(); } + @protected + WebSendI18n sse_decode_web_send_i_18_n(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_waiting = sse_decode_String(deserializer); + var var_enterPin = sse_decode_String(deserializer); + var var_invalidPin = sse_decode_String(deserializer); + var var_tooManyAttempts = sse_decode_String(deserializer); + var var_rejected = sse_decode_String(deserializer); + var var_files = sse_decode_String(deserializer); + var var_fileName = sse_decode_String(deserializer); + var var_size = sse_decode_String(deserializer); + return WebSendI18n( + waiting: var_waiting, + enterPin: var_enterPin, + invalidPin: var_invalidPin, + tooManyAttempts: var_tooManyAttempts, + rejected: var_rejected, + files: var_files, + fileName: var_fileName, + size: var_size, + ); + } + + @protected + WebSendParams sse_decode_web_send_params(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_files = sse_decode_Map_String_file_dto_None(deserializer); + var var_pin = sse_decode_opt_String(deserializer); + var var_i18N = sse_decode_web_send_i_18_n(deserializer); + return WebSendParams(files: var_files, pin: var_pin, i18N: var_i18N); + } + @protected WsServerMessage sse_decode_ws_server_message(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4102,6 +4299,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_32(self, serializer); } + @protected + void sse_encode_box_autoadd_web_send_params(WebSendParams self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_web_send_params(self, serializer); + } + @protected void sse_encode_box_autoadd_ws_server_sdp_message(WsServerSdpMessage self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4354,6 +4557,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_web_send_params(WebSendParams? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_web_send_params(self, serializer); + } + } + @protected void sse_encode_opt_list_String(List? self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4527,6 +4740,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_i_32(3, serializer); sse_encode_String(sessionId, serializer); sse_encode_session_end_reason_v_2(reason, serializer); + case RsServerEvent_WebPrepareDownload(ip: final ip, sessionId: final sessionId, userAgent: final userAgent): + sse_encode_i_32(4, serializer); + sse_encode_String(ip, serializer); + sse_encode_String(sessionId, serializer); + sse_encode_opt_String(userAgent, serializer); + case RsServerEvent_WebFileDownload(sessionId: final sessionId, fileId: final fileId, file: final file): + sse_encode_i_32(5, serializer); + sse_encode_String(sessionId, serializer); + sse_encode_String(fileId, serializer); + sse_encode_box_autoadd_file_dto(file, serializer); + case RsServerEvent_Show(args: final args): + sse_encode_i_32(6, serializer); + sse_encode_list_String(args, serializer); } } @@ -4617,6 +4843,27 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putBigUint64(self); } + @protected + void sse_encode_web_send_i_18_n(WebSendI18n self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.waiting, serializer); + sse_encode_String(self.enterPin, serializer); + sse_encode_String(self.invalidPin, serializer); + sse_encode_String(self.tooManyAttempts, serializer); + sse_encode_String(self.rejected, serializer); + sse_encode_String(self.files, serializer); + sse_encode_String(self.fileName, serializer); + sse_encode_String(self.size, serializer); + } + + @protected + void sse_encode_web_send_params(WebSendParams self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Map_String_file_dto_None(self.files, serializer); + sse_encode_opt_String(self.pin, serializer); + sse_encode_web_send_i_18_n(self.i18N, serializer); + } + @protected void sse_encode_ws_server_message(WsServerMessage self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4852,16 +5099,32 @@ class RsHttpServerImpl extends RustOpaque implements RsHttpServer { /// Emits server events until the server is stopped. /// Can only be listened to once. + /// + /// The v2 protocol, the web send (download API), and the internal endpoint + /// events are all emitted on the same stream. Stream listen() => RustLib.instance.api.crateApiServerRsHttpServerListen( that: this, ); + /// Answers the pending [RsServerEvent::WebFileDownload] event with the source + /// the file content should be read from (either a path or a file descriptor). + /// + /// The server reads the content and streams it to the web client. + Future respondFileDownload({required String sessionId, required String fileId, String? path, int? fileDescriptor}) => RustLib.instance.api + .crateApiServerRsHttpServerRespondFileDownload(that: this, sessionId: sessionId, fileId: fileId, path: path, fileDescriptor: fileDescriptor); + /// 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 respondFileUpload({required String sessionId, required String fileId, String? path, int? fileDescriptor}) => RustLib.instance.api .crateApiServerRsHttpServerRespondFileUpload(that: this, sessionId: sessionId, fileId: fileId, path: path, fileDescriptor: fileDescriptor); + /// Answers the pending [RsServerEvent::WebPrepareDownload] event. + /// + /// Passing `true` accepts the download request, `false` declines it. + Future respondPrepareDownload({required String sessionId, required bool accept}) => + RustLib.instance.api.crateApiServerRsHttpServerRespondPrepareDownload(that: this, sessionId: sessionId, accept: accept); + /// Answers the pending [RsServerEvent::PrepareUpload] event. /// /// Passing the accepted file IDs (a subset of the offered files) accepts the request. diff --git a/app/lib/rust/frb_generated.io.dart b/app/lib/rust/frb_generated.io.dart index ca7f907a..bbb1dbcf 100644 --- a/app/lib/rust/frb_generated.io.dart +++ b/app/lib/rust/frb_generated.io.dart @@ -252,6 +252,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_box_autoadd_u_32(dynamic raw); + @protected + WebSendParams dco_decode_box_autoadd_web_send_params(dynamic raw); + @protected WsServerSdpMessage dco_decode_box_autoadd_ws_server_sdp_message(dynamic raw); @@ -340,6 +343,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int? dco_decode_opt_box_autoadd_u_32(dynamic raw); + @protected + WebSendParams? dco_decode_opt_box_autoadd_web_send_params(dynamic raw); + @protected List? dco_decode_opt_list_String(dynamic raw); @@ -427,6 +433,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BigInt dco_decode_usize(dynamic raw); + @protected + WebSendI18n dco_decode_web_send_i_18_n(dynamic raw); + + @protected + WebSendParams dco_decode_web_send_params(dynamic raw); + @protected WsServerMessage dco_decode_ws_server_message(dynamic raw); @@ -647,6 +659,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); + @protected + WebSendParams sse_decode_box_autoadd_web_send_params(SseDeserializer deserializer); + @protected WsServerSdpMessage sse_decode_box_autoadd_ws_server_sdp_message(SseDeserializer deserializer); @@ -737,6 +752,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); + @protected + WebSendParams? sse_decode_opt_box_autoadd_web_send_params(SseDeserializer deserializer); + @protected List? sse_decode_opt_list_String(SseDeserializer deserializer); @@ -824,6 +842,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BigInt sse_decode_usize(SseDeserializer deserializer); + @protected + WebSendI18n sse_decode_web_send_i_18_n(SseDeserializer deserializer); + + @protected + WebSendParams sse_decode_web_send_params(SseDeserializer deserializer); + @protected WsServerMessage sse_decode_ws_server_message(SseDeserializer deserializer); @@ -1089,6 +1113,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_web_send_params(WebSendParams self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_ws_server_sdp_message(WsServerSdpMessage self, SseSerializer serializer); @@ -1179,6 +1206,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_web_send_params(WebSendParams? self, SseSerializer serializer); + @protected void sse_encode_opt_list_String(List? self, SseSerializer serializer); @@ -1267,6 +1297,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_usize(BigInt self, SseSerializer serializer); + @protected + void sse_encode_web_send_i_18_n(WebSendI18n self, SseSerializer serializer); + + @protected + void sse_encode_web_send_params(WebSendParams self, SseSerializer serializer); + @protected void sse_encode_ws_server_message(WsServerMessage self, SseSerializer serializer); diff --git a/app/lib/rust/frb_generated.web.dart b/app/lib/rust/frb_generated.web.dart index 558b5f63..19786d6d 100644 --- a/app/lib/rust/frb_generated.web.dart +++ b/app/lib/rust/frb_generated.web.dart @@ -254,6 +254,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_box_autoadd_u_32(dynamic raw); + @protected + WebSendParams dco_decode_box_autoadd_web_send_params(dynamic raw); + @protected WsServerSdpMessage dco_decode_box_autoadd_ws_server_sdp_message(dynamic raw); @@ -342,6 +345,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int? dco_decode_opt_box_autoadd_u_32(dynamic raw); + @protected + WebSendParams? dco_decode_opt_box_autoadd_web_send_params(dynamic raw); + @protected List? dco_decode_opt_list_String(dynamic raw); @@ -429,6 +435,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BigInt dco_decode_usize(dynamic raw); + @protected + WebSendI18n dco_decode_web_send_i_18_n(dynamic raw); + + @protected + WebSendParams dco_decode_web_send_params(dynamic raw); + @protected WsServerMessage dco_decode_ws_server_message(dynamic raw); @@ -649,6 +661,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); + @protected + WebSendParams sse_decode_box_autoadd_web_send_params(SseDeserializer deserializer); + @protected WsServerSdpMessage sse_decode_box_autoadd_ws_server_sdp_message(SseDeserializer deserializer); @@ -739,6 +754,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); + @protected + WebSendParams? sse_decode_opt_box_autoadd_web_send_params(SseDeserializer deserializer); + @protected List? sse_decode_opt_list_String(SseDeserializer deserializer); @@ -826,6 +844,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BigInt sse_decode_usize(SseDeserializer deserializer); + @protected + WebSendI18n sse_decode_web_send_i_18_n(SseDeserializer deserializer); + + @protected + WebSendParams sse_decode_web_send_params(SseDeserializer deserializer); + @protected WsServerMessage sse_decode_ws_server_message(SseDeserializer deserializer); @@ -1091,6 +1115,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_web_send_params(WebSendParams self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_ws_server_sdp_message(WsServerSdpMessage self, SseSerializer serializer); @@ -1181,6 +1208,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_web_send_params(WebSendParams? self, SseSerializer serializer); + @protected void sse_encode_opt_list_String(List? self, SseSerializer serializer); @@ -1269,6 +1299,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_usize(BigInt self, SseSerializer serializer); + @protected + void sse_encode_web_send_i_18_n(WebSendI18n self, SseSerializer serializer); + + @protected + void sse_encode_web_send_params(WebSendParams self, SseSerializer serializer); + @protected void sse_encode_ws_server_message(WsServerMessage self, SseSerializer serializer); diff --git a/app/rust/src/api/server.rs b/app/rust/src/api/server.rs index a593f9ab..ba2606bd 100644 --- a/app/rust/src/api/server.rs +++ b/app/rust/src/api/server.rs @@ -4,11 +4,14 @@ 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; +use localsend::http::server::internal::{InternalConfig, InternalEvent}; pub use localsend::http::server::v2::SessionEndReasonV2; use localsend::http::server::v2::{PrepareUploadDecisionV2, ServerEventV2}; +pub use localsend::http::server::web::WebSendI18n; +use localsend::http::server::web::{WebSendConfig, WebSendEvent}; use localsend::http::state::ClientInfo; use localsend::model::discovery::DeviceType; -use localsend::model::transfer::FileDto; +use localsend::model::transfer::{FileContent, FileDto}; use std::collections::HashMap; use tokio::sync::{Mutex, mpsc, oneshot}; @@ -39,6 +42,31 @@ pub enum RsServerEvent { session_id: String, reason: SessionEndReasonV2, }, + + /// A web client requests to download the shared files via `POST /api/localsend/v2/prepare-download`. + /// + /// Must be answered with [RsHttpServer::respond_prepare_download]. + WebPrepareDownload { + ip: String, + session_id: String, + user_agent: Option, + }, + + /// A web client downloads an offered file via `GET /api/localsend/v2/download`. + /// + /// Must be answered with [RsHttpServer::respond_file_download]. + WebFileDownload { + session_id: String, + file_id: String, + file: FileDto, + }, + + /// Another application instance requested the running application to show itself + /// via `POST /api/localsend/v2/show`. + Show { + /// Command-line arguments forwarded by the other application instance. + args: Vec, + }, } pub struct RsHttpServer { @@ -46,11 +74,38 @@ pub struct RsHttpServer { stop_tx: Mutex>>, pending_decision: Mutex>>, pending_uploads: Mutex>>, + web_event_rx: Mutex>>, + pending_download_decisions: Mutex>>, + pending_downloads: Mutex>>, + internal_event_rx: Mutex>>, +} + +/// Configuration for web send: files offered for download by web browsers. +/// +/// Web send can be enabled independently of the v2 protocol endpoints. When +/// omitted, the download API responds with 403 and only the v2 endpoints run. +pub struct WebSendParams { + /// The metadata of the files offered for download, mapped by file ID. + /// The content is requested per download via [RsServerEvent::WebFileDownload]. + pub files: HashMap, + + /// Optional PIN that web clients must provide via the `pin` query parameter. + pub pin: Option, + + /// Translations for the web page, served via `/i18n.json`. + pub i18n: WebSendI18n, } /// Starts the HTTP server on the given port (IPv4 and IPv6). /// The server runs until [RsHttpServer::stop] is called. /// +/// Passing [web_send] additionally enables the web send (download API) so that +/// web browsers can download the offered files. +/// +/// Passing [show_token] enables the internal `show` endpoint that lets another +/// application instance request this one to show itself (emitted as +/// [RsServerEvent::Show]). The token guards the endpoint against other clients. +/// /// Events are received by listening to [RsHttpServer::listen]. pub async fn start_server( port: u16, @@ -61,10 +116,38 @@ pub async fn start_server( device_type: Option, fingerprint: String, pin: Option, + web_send: Option, + show_token: Option, ) -> anyhow::Result { let (event_tx, event_rx) = mpsc::channel::(16); let (stop_tx, stop_rx) = oneshot::channel::<()>(); + let (web_send_config, web_event_rx) = match web_send { + Some(web_send) => { + let (web_event_tx, web_event_rx) = mpsc::channel::(16); + let config = WebSendConfig { + files: web_send.files, + pin: web_send.pin, + i18n: web_send.i18n, + event_tx: web_event_tx, + }; + (Some(config), Some(web_event_rx)) + } + None => (None, None), + }; + + let (internal_config, internal_event_rx) = match show_token { + Some(show_token) => { + let (internal_event_tx, internal_event_rx) = mpsc::channel::(16); + let config = InternalConfig { + show_token, + event_tx: internal_event_tx, + }; + (Some(config), Some(internal_event_rx)) + } + None => (None, None), + }; + localsend::http::server::start_with_port( port, tls, @@ -75,9 +158,9 @@ pub async fn start_server( device_type, token: fingerprint, }, - None, + internal_config, Some(ServerConfigV2 { pin, event_tx }), - None, + web_send_config, stop_rx, ) .await?; @@ -87,63 +170,139 @@ pub async fn start_server( stop_tx: Mutex::new(Some(stop_tx)), pending_decision: Mutex::new(None), pending_uploads: Mutex::new(HashMap::new()), + web_event_rx: Mutex::new(web_event_rx), + pending_download_decisions: Mutex::new(HashMap::new()), + pending_downloads: Mutex::new(HashMap::new()), + internal_event_rx: Mutex::new(internal_event_rx), }) } impl RsHttpServer { /// Emits server events until the server is stopped. /// Can only be listened to once. + /// + /// The v2 protocol, the web send (download API), and the internal endpoint + /// events are all emitted on the same stream. pub async fn listen(&self, sink: StreamSink) { let Some(mut event_rx) = self.event_rx.lock().await.take() else { let _ = sink.add_error(anyhow::anyhow!("Server events already listened to")); return; }; + let mut web_event_rx = self.web_event_rx.lock().await.take(); + let mut internal_event_rx = self.internal_event_rx.lock().await.take(); - while let Some(event) = event_rx.recv().await { - match event { - ServerEventV2::Register { ip, info } => { - let _ = sink.add(RsServerEvent::Register { - ip: ip.to_string(), - info, - }); + let mut v2_open = true; + loop { + tokio::select! { + event = event_rx.recv(), if v2_open => { + match event { + Some(event) => self.handle_server_event(&sink, event).await, + None => v2_open = false, + } } - ServerEventV2::PrepareUpload { - ip, + event = recv_opt(&mut web_event_rx) => { + match event { + Some(event) => self.handle_web_event(&sink, event).await, + None => web_event_rx = None, + } + } + event = recv_opt(&mut internal_event_rx) => { + match event { + Some(InternalEvent::Show { args }) => { + let _ = sink.add(RsServerEvent::Show { args }); + } + None => internal_event_rx = None, + } + } + } + + if !v2_open && web_event_rx.is_none() && internal_event_rx.is_none() { + break; + } + } + } + + async fn handle_server_event(&self, sink: &StreamSink, event: ServerEventV2) { + 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, - decision_tx, - } => { - *self.pending_decision.lock().await = Some(decision_tx); - let _ = sink.add(RsServerEvent::PrepareUpload { - ip: ip.to_string(), - info, - files, - }); - } - ServerEventV2::FileUpload { + }); + } + 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, - 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 }); - } + }); + } + 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 }); + } + } + } + + async fn handle_web_event(&self, sink: &StreamSink, event: WebSendEvent) { + match event { + WebSendEvent::PrepareDownload { + ip, + session_id, + user_agent, + decision_tx, + } => { + self.pending_download_decisions + .lock() + .await + .insert(session_id.clone(), decision_tx); + let _ = sink.add(RsServerEvent::WebPrepareDownload { + ip: ip.to_string(), + session_id, + user_agent, + }); + } + WebSendEvent::FileDownload { + session_id, + file_id, + file, + content_tx, + } => { + self.pending_downloads + .lock() + .await + .insert((session_id.clone(), file_id.clone()), content_tx); + let _ = sink.add(RsServerEvent::WebFileDownload { + session_id, + file_id, + file, + }); } } } @@ -205,6 +364,59 @@ impl RsHttpServer { } } + /// Answers the pending [RsServerEvent::WebPrepareDownload] event. + /// + /// Passing `true` accepts the download request, `false` declines it. + pub async fn respond_prepare_download( + &self, + session_id: String, + accept: bool, + ) -> anyhow::Result<()> { + let Some(decision_tx) = self + .pending_download_decisions + .lock() + .await + .remove(&session_id) + else { + return Err(anyhow::anyhow!("No pending prepare-download request")); + }; + + decision_tx + .send(accept) + .map_err(|_| anyhow::anyhow!("Prepare-download request already ended"))?; + + Ok(()) + } + + /// Answers the pending [RsServerEvent::WebFileDownload] event with the source + /// the file content should be read from (either a path or a file descriptor). + /// + /// The server reads the content and streams it to the web client. + pub async fn respond_file_download( + &self, + session_id: String, + file_id: String, + path: Option, + file_descriptor: Option, + ) -> anyhow::Result<()> { + let Some(content_tx) = self + .pending_downloads + .lock() + .await + .remove(&(session_id, file_id)) + else { + return Err(anyhow::anyhow!("No pending file download for this file")); + }; + + let content = resolve_file_content(path, file_descriptor)?; + + content_tx + .send(content) + .map_err(|_| anyhow::anyhow!("Download request already ended"))?; + + Ok(()) + } + /// Stops the server. pub async fn stop(&self) { if let Some(stop_tx) = self.stop_tx.lock().await.take() { @@ -213,6 +425,15 @@ impl RsHttpServer { } } +/// Receives the next event from an optional channel, or pends forever when the +/// channel is absent (i.e. that feature is disabled). +async fn recv_opt(rx: &mut Option>) -> Option { + match rx { + Some(rx) => rx.recv().await, + None => std::future::pending::>().await, + } +} + fn resolve_upload_target( path: Option, file_descriptor: Option, @@ -245,6 +466,43 @@ fn resolve_upload_target( } } +fn resolve_file_content( + path: Option, + file_descriptor: Option, +) -> anyhow::Result { + match (path, file_descriptor) { + (Some(path), None) => Ok(FileContent::Path(path.into())), + (None, Some(file_descriptor)) => { + #[cfg(target_os = "android")] + { + Ok(FileContent::Fd(file_descriptor)) + } + #[cfg(not(target_os = "android"))] + { + let _ = file_descriptor; + Err(anyhow::anyhow!( + "File descriptors are only supported on Android" + )) + } + } + _ => Err(anyhow::anyhow!( + "Exactly one download source must be provided" + )), + } +} + +#[frb(mirror(WebSendI18n))] +pub struct _WebSendI18n { + pub waiting: String, + pub enter_pin: String, + pub invalid_pin: String, + pub too_many_attempts: String, + pub rejected: String, + pub files: String, + pub file_name: String, + pub size: String, +} + #[frb(mirror(TlsConfig))] pub struct _TlsConfig { pub cert: String, diff --git a/app/rust/src/frb_generated.rs b/app/rust/src/frb_generated.rs index 93fb0025..5df47eec 100644 --- a/app/rust/src/frb_generated.rs +++ b/app/rust/src/frb_generated.rs @@ -42,7 +42,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1220219761; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1029282510; // Section: executor @@ -773,6 +773,72 @@ fn wire__crate__api__server__RsHttpServer_listen_impl( }, ) } +fn wire__crate__api__server__RsHttpServer_respond_file_download_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "RsHttpServer_respond_file_download", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_session_id = ::sse_decode(&mut deserializer); + let api_file_id = ::sse_decode(&mut deserializer); + let api_path = >::sse_decode(&mut deserializer); + let api_file_descriptor = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = crate::api::server::RsHttpServer::respond_file_download( + &*api_that_guard, + api_session_id, + api_file_id, + api_path, + api_file_descriptor, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__server__RsHttpServer_respond_file_upload_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -839,6 +905,68 @@ fn wire__crate__api__server__RsHttpServer_respond_file_upload_impl( }, ) } +fn wire__crate__api__server__RsHttpServer_respond_prepare_download_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "RsHttpServer_respond_prepare_download", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_session_id = ::sse_decode(&mut deserializer); + let api_accept = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = crate::api::server::RsHttpServer::respond_prepare_download( + &*api_that_guard, + api_session_id, + api_accept, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__server__RsHttpServer_respond_prepare_upload_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -2146,6 +2274,9 @@ fn wire__crate__api__server__start_server_impl( >::sse_decode(&mut deserializer); let api_fingerprint = ::sse_decode(&mut deserializer); let api_pin = >::sse_decode(&mut deserializer); + let api_web_send = + >::sse_decode(&mut deserializer); + let api_show_token = >::sse_decode(&mut deserializer); deserializer.end(); move |context| async move { transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( @@ -2159,6 +2290,8 @@ fn wire__crate__api__server__start_server_impl( api_device_type, api_fingerprint, api_pin, + api_web_send, + api_show_token, ) .await?; Ok(output_ok) @@ -2322,6 +2455,17 @@ const _: fn() = || { let _: String = TlsConfig.cert; let _: String = TlsConfig.private_key; } + { + let WebSendI18n = None::.unwrap(); + let _: String = WebSendI18n.waiting; + let _: String = WebSendI18n.enter_pin; + let _: String = WebSendI18n.invalid_pin; + let _: String = WebSendI18n.too_many_attempts; + let _: String = WebSendI18n.rejected; + let _: String = WebSendI18n.files; + let _: String = WebSendI18n.file_name; + let _: String = WebSendI18n.size; + } match None::.unwrap() { crate::api::webrtc::WsServerMessage::Hello { client, peers } => { let _: crate::api::webrtc::ClientInfo = client; @@ -3087,6 +3231,19 @@ impl SseDecode for Option { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode( + deserializer, + )); + } else { + return None; + } + } +} + impl SseDecode for Option> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -3378,6 +3535,30 @@ impl SseDecode for crate::api::server::RsServerEvent { reason: var_reason, }; } + 4 => { + let mut var_ip = ::sse_decode(deserializer); + let mut var_sessionId = ::sse_decode(deserializer); + let mut var_userAgent = >::sse_decode(deserializer); + return crate::api::server::RsServerEvent::WebPrepareDownload { + ip: var_ip, + session_id: var_sessionId, + user_agent: var_userAgent, + }; + } + 5 => { + let mut var_sessionId = ::sse_decode(deserializer); + let mut var_fileId = ::sse_decode(deserializer); + let mut var_file = ::sse_decode(deserializer); + return crate::api::server::RsServerEvent::WebFileDownload { + session_id: var_sessionId, + file_id: var_fileId, + file: var_file, + }; + } + 6 => { + let mut var_args = >::sse_decode(deserializer); + return crate::api::server::RsServerEvent::Show { args: var_args }; + } _ => { unimplemented!(""); } @@ -3512,6 +3693,47 @@ impl SseDecode for usize { } } +impl SseDecode for crate::api::server::WebSendI18n { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_waiting = ::sse_decode(deserializer); + let mut var_enterPin = ::sse_decode(deserializer); + let mut var_invalidPin = ::sse_decode(deserializer); + let mut var_tooManyAttempts = ::sse_decode(deserializer); + let mut var_rejected = ::sse_decode(deserializer); + let mut var_files = ::sse_decode(deserializer); + let mut var_fileName = ::sse_decode(deserializer); + let mut var_size = ::sse_decode(deserializer); + return crate::api::server::WebSendI18n { + waiting: var_waiting, + enter_pin: var_enterPin, + invalid_pin: var_invalidPin, + too_many_attempts: var_tooManyAttempts, + rejected: var_rejected, + files: var_files, + file_name: var_fileName, + size: var_size, + }; + } +} + +impl SseDecode for crate::api::server::WebSendParams { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_files = + >::sse_decode( + deserializer, + ); + let mut var_pin = >::sse_decode(deserializer); + let mut var_i18N = ::sse_decode(deserializer); + return crate::api::server::WebSendParams { + files: var_files, + pin: var_pin, + i18n: var_i18N, + }; + } +} + impl SseDecode for crate::api::webrtc::WsServerMessage { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -3617,118 +3839,130 @@ fn pde_ffi_dispatcher_primary_impl( 9 => wire__crate__api__http__RsHttpClient_register_impl(port, ptr, rust_vec_len, data_len), 10 => wire__crate__api__http__RsHttpClient_upload_impl(port, ptr, rust_vec_len, data_len), 11 => wire__crate__api__server__RsHttpServer_listen_impl(port, ptr, rust_vec_len, data_len), - 12 => wire__crate__api__server__RsHttpServer_respond_file_upload_impl( + 12 => wire__crate__api__server__RsHttpServer_respond_file_download_impl( port, ptr, rust_vec_len, data_len, ), - 13 => wire__crate__api__server__RsHttpServer_respond_prepare_upload_impl( + 13 => wire__crate__api__server__RsHttpServer_respond_file_upload_impl( port, ptr, rust_vec_len, data_len, ), - 14 => wire__crate__api__server__RsHttpServer_stop_impl(port, ptr, rust_vec_len, data_len), - 15 => wire__crate__api__webrtc__RtcFileReceiver_get_file_id_impl( + 14 => wire__crate__api__server__RsHttpServer_respond_prepare_download_impl( port, ptr, rust_vec_len, data_len, ), - 16 => wire__crate__api__webrtc__RtcFileReceiver_receive_impl( + 15 => wire__crate__api__server__RsHttpServer_respond_prepare_upload_impl( port, ptr, rust_vec_len, data_len, ), - 17 => wire__crate__api__webrtc__RtcFileSender_send_impl(port, ptr, rust_vec_len, data_len), - 18 => wire__crate__api__webrtc__RtcReceiveController_decline_impl( + 16 => wire__crate__api__server__RsHttpServer_stop_impl(port, ptr, rust_vec_len, data_len), + 17 => wire__crate__api__webrtc__RtcFileReceiver_get_file_id_impl( port, ptr, rust_vec_len, data_len, ), - 19 => wire__crate__api__webrtc__RtcReceiveController_listen_error_impl( + 18 => wire__crate__api__webrtc__RtcFileReceiver_receive_impl( port, ptr, rust_vec_len, data_len, ), - 20 => wire__crate__api__webrtc__RtcReceiveController_listen_files_impl( + 19 => wire__crate__api__webrtc__RtcFileSender_send_impl(port, ptr, rust_vec_len, data_len), + 20 => wire__crate__api__webrtc__RtcReceiveController_decline_impl( port, ptr, rust_vec_len, data_len, ), - 21 => wire__crate__api__webrtc__RtcReceiveController_listen_receiving_impl( + 21 => wire__crate__api__webrtc__RtcReceiveController_listen_error_impl( port, ptr, rust_vec_len, data_len, ), - 22 => wire__crate__api__webrtc__RtcReceiveController_listen_status_impl( + 22 => wire__crate__api__webrtc__RtcReceiveController_listen_files_impl( port, ptr, rust_vec_len, data_len, ), - 23 => wire__crate__api__webrtc__RtcReceiveController_send_file_status_impl( + 23 => wire__crate__api__webrtc__RtcReceiveController_listen_receiving_impl( port, ptr, rust_vec_len, data_len, ), - 24 => wire__crate__api__webrtc__RtcReceiveController_send_pin_impl( + 24 => wire__crate__api__webrtc__RtcReceiveController_listen_status_impl( port, ptr, rust_vec_len, data_len, ), - 25 => wire__crate__api__webrtc__RtcReceiveController_send_selection_impl( + 25 => wire__crate__api__webrtc__RtcReceiveController_send_file_status_impl( port, ptr, rust_vec_len, data_len, ), - 26 => wire__crate__api__webrtc__RtcSendController_listen_error_impl( + 26 => wire__crate__api__webrtc__RtcReceiveController_send_pin_impl( port, ptr, rust_vec_len, data_len, ), - 27 => wire__crate__api__webrtc__RtcSendController_listen_selected_files_impl( + 27 => wire__crate__api__webrtc__RtcReceiveController_send_selection_impl( port, ptr, rust_vec_len, data_len, ), - 28 => wire__crate__api__webrtc__RtcSendController_listen_status_impl( + 28 => wire__crate__api__webrtc__RtcSendController_listen_error_impl( port, ptr, rust_vec_len, data_len, ), - 29 => wire__crate__api__webrtc__RtcSendController_send_file_impl( + 29 => wire__crate__api__webrtc__RtcSendController_listen_selected_files_impl( port, ptr, rust_vec_len, data_len, ), - 30 => wire__crate__api__webrtc__RtcSendController_send_pin_impl( + 30 => wire__crate__api__webrtc__RtcSendController_listen_status_impl( port, ptr, rust_vec_len, data_len, ), - 31 => wire__crate__api__webrtc__connect_impl(port, ptr, rust_vec_len, data_len), - 34 => wire__crate__api__stream__create_stream_impl(port, ptr, rust_vec_len, data_len), - 35 => { + 31 => wire__crate__api__webrtc__RtcSendController_send_file_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 32 => wire__crate__api__webrtc__RtcSendController_send_pin_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 33 => wire__crate__api__webrtc__connect_impl(port, ptr, rust_vec_len, data_len), + 36 => wire__crate__api__stream__create_stream_impl(port, ptr, rust_vec_len, data_len), + 37 => { wire__crate__api__logging__enable_debug_logging_impl(port, ptr, rust_vec_len, data_len) } - 36 => wire__crate__api__crypto__generate_key_pair_impl(port, ptr, rust_vec_len, data_len), - 37 => wire__crate__api__server__start_server_impl(port, ptr, rust_vec_len, data_len), - 38 => wire__crate__api__crypto__verify_cert_impl(port, ptr, rust_vec_len, data_len), + 38 => wire__crate__api__crypto__generate_key_pair_impl(port, ptr, rust_vec_len, data_len), + 39 => wire__crate__api__server__start_server_impl(port, ptr, rust_vec_len, data_len), + 40 => wire__crate__api__crypto__verify_cert_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -3743,8 +3977,8 @@ fn pde_ffi_dispatcher_sync_impl( match func_id { 2 => wire__crate__api__stream__Dart2RustStreamSink_close_impl(ptr, rust_vec_len, data_len), 6 => wire__crate__api__http__RsCancellationToken_cancel_impl(ptr, rust_vec_len, data_len), - 32 => wire__crate__api__http__create_cancellation_token_impl(ptr, rust_vec_len, data_len), - 33 => wire__crate__api__http__create_client_impl(ptr, rust_vec_len, data_len), + 34 => wire__crate__api__http__create_cancellation_token_impl(ptr, rust_vec_len, data_len), + 35 => wire__crate__api__http__create_client_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -4422,6 +4656,31 @@ impl flutter_rust_bridge::IntoDart for crate::api::server::RsServerEvent { reason.into_into_dart().into_dart(), ] .into_dart(), + crate::api::server::RsServerEvent::WebPrepareDownload { + ip, + session_id, + user_agent, + } => [ + 4.into_dart(), + ip.into_into_dart().into_dart(), + session_id.into_into_dart().into_dart(), + user_agent.into_into_dart().into_dart(), + ] + .into_dart(), + crate::api::server::RsServerEvent::WebFileDownload { + session_id, + file_id, + file, + } => [ + 5.into_dart(), + session_id.into_into_dart().into_dart(), + file_id.into_into_dart().into_dart(), + file.into_into_dart().into_dart(), + ] + .into_dart(), + crate::api::server::RsServerEvent::Show { args } => { + [6.into_dart(), args.into_into_dart().into_dart()].into_dart() + } _ => { unimplemented!(""); } @@ -4556,6 +4815,55 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.0.waiting.into_into_dart().into_dart(), + self.0.enter_pin.into_into_dart().into_dart(), + self.0.invalid_pin.into_into_dart().into_dart(), + self.0.too_many_attempts.into_into_dart().into_dart(), + self.0.rejected.into_into_dart().into_dart(), + self.0.files.into_into_dart().into_dart(), + self.0.file_name.into_into_dart().into_dart(), + self.0.size.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for FrbWrapper +{ +} +impl flutter_rust_bridge::IntoIntoDart> + for crate::api::server::WebSendI18n +{ + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::server::WebSendParams { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.files.into_into_dart().into_dart(), + self.pin.into_into_dart().into_dart(), + self.i18n.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::server::WebSendParams +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::server::WebSendParams +{ + fn into_into_dart(self) -> crate::api::server::WebSendParams { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for FrbWrapper { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { match self.0 { @@ -5220,6 +5528,16 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -5445,6 +5763,30 @@ impl SseEncode for crate::api::server::RsServerEvent { ::sse_encode(session_id, serializer); ::sse_encode(reason, serializer); } + crate::api::server::RsServerEvent::WebPrepareDownload { + ip, + session_id, + user_agent, + } => { + ::sse_encode(4, serializer); + ::sse_encode(ip, serializer); + ::sse_encode(session_id, serializer); + >::sse_encode(user_agent, serializer); + } + crate::api::server::RsServerEvent::WebFileDownload { + session_id, + file_id, + file, + } => { + ::sse_encode(5, serializer); + ::sse_encode(session_id, serializer); + ::sse_encode(file_id, serializer); + ::sse_encode(file, serializer); + } + crate::api::server::RsServerEvent::Show { args } => { + ::sse_encode(6, serializer); + >::sse_encode(args, serializer); + } _ => { unimplemented!(""); } @@ -5572,6 +5914,31 @@ impl SseEncode for usize { } } +impl SseEncode for crate::api::server::WebSendI18n { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.waiting, serializer); + ::sse_encode(self.enter_pin, serializer); + ::sse_encode(self.invalid_pin, serializer); + ::sse_encode(self.too_many_attempts, serializer); + ::sse_encode(self.rejected, serializer); + ::sse_encode(self.files, serializer); + ::sse_encode(self.file_name, serializer); + ::sse_encode(self.size, serializer); + } +} + +impl SseEncode for crate::api::server::WebSendParams { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode( + self.files, serializer, + ); + >::sse_encode(self.pin, serializer); + ::sse_encode(self.i18n, serializer); + } +} + impl SseEncode for crate::api::webrtc::WsServerMessage { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { diff --git a/packages/core/tests/v2_server.rs b/packages/core/tests/v2_server.rs index ea467d19..8512d829 100644 --- a/packages/core/tests/v2_server.rs +++ b/packages/core/tests/v2_server.rs @@ -1,6 +1,7 @@ #![cfg(feature = "http")] use bytes::Bytes; +use futures_util::StreamExt; use localsend::http::client::{ClientError, LsHttpClientV2}; use localsend::http::dto::ProtocolType; use localsend::http::dto_v2::{PrepareUploadRequestDtoV2, ProtocolTypeV2, RegisterDtoV2}; @@ -14,7 +15,6 @@ use std::path::PathBuf; use std::sync::atomic::{AtomicU16, AtomicU64, Ordering}; use std::sync::Arc; use std::time::Duration; -use futures_util::StreamExt; use tokio::sync::{mpsc, oneshot, Mutex}; use tokio_stream::wrappers::ReceiverStream; use tokio_util::sync::CancellationToken; @@ -223,12 +223,11 @@ async fn upload_bytes( // The caller now owns building the request body; track cumulative bytes // sent so the progress assertion below still holds. let progress = sent.clone(); - let body = localsend::reqwest::Body::wrap_stream(ReceiverStream::new(rx).map( - move |chunk: Bytes| { + let body = + localsend::reqwest::Body::wrap_stream(ReceiverStream::new(rx).map(move |chunk: Bytes| { progress.fetch_add(chunk.len() as u64, Ordering::Relaxed); Ok::(chunk) - }, - )); + })); let result = client .upload( ProtocolType::Http,