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