mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
feat: add http server FFI bindings
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
rust_input: crate::api
|
||||
rust_root: rust/
|
||||
dart_output: lib/rust
|
||||
dart_format_line_length: 150
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export 'package:localsend_app/isolate/src/isolate/child/http_server_events.dart';
|
||||
export 'package:localsend_app/isolate/src/isolate/child/http_server_isolate.dart';
|
||||
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,123 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:localsend_app/isolate/model/dto/file_dto.dart';
|
||||
import 'package:localsend_app/isolate/model/dto/register_dto.dart';
|
||||
|
||||
sealed class HttpServerEvent {
|
||||
const HttpServerEvent();
|
||||
}
|
||||
|
||||
class HttpServerStartedEvent extends HttpServerEvent {
|
||||
const HttpServerStartedEvent();
|
||||
}
|
||||
|
||||
class HttpServerStoppedEvent extends HttpServerEvent {
|
||||
const HttpServerStoppedEvent();
|
||||
}
|
||||
|
||||
class HttpServerErrorEvent extends HttpServerEvent {
|
||||
final String error;
|
||||
|
||||
const HttpServerErrorEvent({required this.error});
|
||||
}
|
||||
|
||||
class HttpServerShowEvent extends HttpServerEvent {
|
||||
final List<String> args;
|
||||
|
||||
const HttpServerShowEvent({required this.args});
|
||||
}
|
||||
|
||||
class HttpServerRegisterEvent extends HttpServerEvent {
|
||||
final String ip;
|
||||
final RegisterDto info;
|
||||
|
||||
const HttpServerRegisterEvent({required this.ip, required this.info});
|
||||
}
|
||||
|
||||
class HttpServerPrepareUploadEvent extends HttpServerEvent {
|
||||
final int requestId;
|
||||
final String ip;
|
||||
final RegisterDto info;
|
||||
final Map<String, FileDto> files;
|
||||
|
||||
const HttpServerPrepareUploadEvent({
|
||||
required this.requestId,
|
||||
required this.ip,
|
||||
required this.info,
|
||||
required this.files,
|
||||
});
|
||||
}
|
||||
|
||||
class HttpServerFileUploadEvent extends HttpServerEvent {
|
||||
final int requestId;
|
||||
final String sessionId;
|
||||
final String fileId;
|
||||
final FileDto file;
|
||||
|
||||
const HttpServerFileUploadEvent({
|
||||
required this.requestId,
|
||||
required this.sessionId,
|
||||
required this.fileId,
|
||||
required this.file,
|
||||
});
|
||||
}
|
||||
|
||||
class HttpServerFileUploadChunkEvent extends HttpServerEvent {
|
||||
final int requestId;
|
||||
final Uint8List data;
|
||||
|
||||
const HttpServerFileUploadChunkEvent({
|
||||
required this.requestId,
|
||||
required this.data,
|
||||
});
|
||||
}
|
||||
|
||||
class HttpServerFileUploadFinishedEvent extends HttpServerEvent {
|
||||
final int requestId;
|
||||
final String? error;
|
||||
|
||||
const HttpServerFileUploadFinishedEvent({
|
||||
required this.requestId,
|
||||
required this.error,
|
||||
});
|
||||
}
|
||||
|
||||
enum HttpServerSessionEndReason { finished, cancelled }
|
||||
|
||||
class HttpServerSessionEndEvent extends HttpServerEvent {
|
||||
final String sessionId;
|
||||
final HttpServerSessionEndReason reason;
|
||||
|
||||
const HttpServerSessionEndEvent({
|
||||
required this.sessionId,
|
||||
required this.reason,
|
||||
});
|
||||
}
|
||||
|
||||
class HttpServerPrepareDownloadEvent extends HttpServerEvent {
|
||||
final int requestId;
|
||||
final String ip;
|
||||
final String sessionId;
|
||||
final String? userAgent;
|
||||
|
||||
const HttpServerPrepareDownloadEvent({
|
||||
required this.requestId,
|
||||
required this.ip,
|
||||
required this.sessionId,
|
||||
required this.userAgent,
|
||||
});
|
||||
}
|
||||
|
||||
class HttpServerFileDownloadEvent extends HttpServerEvent {
|
||||
final int requestId;
|
||||
final String sessionId;
|
||||
final String fileId;
|
||||
final FileDto file;
|
||||
|
||||
const HttpServerFileDownloadEvent({
|
||||
required this.requestId,
|
||||
required this.sessionId,
|
||||
required this.fileId,
|
||||
required this.file,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:localsend_app/isolate/constants.dart';
|
||||
import 'package:localsend_app/isolate/model/device.dart' as app;
|
||||
import 'package:localsend_app/isolate/model/dto/file_dto.dart' as app;
|
||||
import 'package:localsend_app/isolate/model/dto/multicast_dto.dart' as app;
|
||||
import 'package:localsend_app/isolate/model/dto/register_dto.dart' as app;
|
||||
import 'package:localsend_app/isolate/model/stored_security_context.dart';
|
||||
import 'package:localsend_app/isolate/src/isolate/child/http_server_events.dart';
|
||||
import 'package:localsend_app/isolate/src/isolate/child/main.dart';
|
||||
import 'package:localsend_app/isolate/src/isolate/dto/send_to_isolate_data.dart';
|
||||
import 'package:localsend_app/rust/api/http_server.dart' as rust;
|
||||
import 'package:localsend_app/rust/api/model.dart' as rust;
|
||||
import 'package:localsend_app/rust/api/stream.dart';
|
||||
|
||||
sealed class HttpServerTask {
|
||||
const HttpServerTask();
|
||||
}
|
||||
|
||||
class HttpServerStartTask extends HttpServerTask {
|
||||
final String alias;
|
||||
final int port;
|
||||
final bool https;
|
||||
final String? deviceModel;
|
||||
final app.DeviceType deviceType;
|
||||
final String fingerprint;
|
||||
final StoredSecurityContext securityContext;
|
||||
final String? showToken;
|
||||
final String? receivePin;
|
||||
final HttpServerWebSendConfig? webSend;
|
||||
|
||||
const HttpServerStartTask({
|
||||
required this.alias,
|
||||
required this.port,
|
||||
required this.https,
|
||||
required this.deviceModel,
|
||||
required this.deviceType,
|
||||
required this.fingerprint,
|
||||
required this.securityContext,
|
||||
required this.showToken,
|
||||
required this.receivePin,
|
||||
required this.webSend,
|
||||
});
|
||||
}
|
||||
|
||||
class HttpServerWebSendConfig {
|
||||
final Map<String, app.FileDto> files;
|
||||
final String? pin;
|
||||
final HttpServerWebSendI18n i18n;
|
||||
|
||||
const HttpServerWebSendConfig({
|
||||
required this.files,
|
||||
required this.pin,
|
||||
required this.i18n,
|
||||
});
|
||||
}
|
||||
|
||||
class HttpServerWebSendI18n {
|
||||
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 HttpServerWebSendI18n({
|
||||
required this.waiting,
|
||||
required this.enterPin,
|
||||
required this.invalidPin,
|
||||
required this.tooManyAttempts,
|
||||
required this.rejected,
|
||||
required this.files,
|
||||
required this.fileName,
|
||||
required this.size,
|
||||
});
|
||||
}
|
||||
|
||||
class HttpServerStopTask extends HttpServerTask {
|
||||
const HttpServerStopTask();
|
||||
}
|
||||
|
||||
class HttpServerRespondPrepareUploadTask extends HttpServerTask {
|
||||
final int requestId;
|
||||
final Set<String>? fileIds;
|
||||
|
||||
const HttpServerRespondPrepareUploadTask({
|
||||
required this.requestId,
|
||||
required this.fileIds,
|
||||
});
|
||||
}
|
||||
|
||||
sealed class HttpServerFileUploadTarget {
|
||||
const HttpServerFileUploadTarget();
|
||||
}
|
||||
|
||||
class HttpServerFileUploadPathTarget extends HttpServerFileUploadTarget {
|
||||
final String path;
|
||||
|
||||
const HttpServerFileUploadPathTarget({required this.path});
|
||||
}
|
||||
|
||||
class HttpServerFileUploadDescriptorTarget extends HttpServerFileUploadTarget {
|
||||
final int fileDescriptor;
|
||||
|
||||
const HttpServerFileUploadDescriptorTarget({required this.fileDescriptor});
|
||||
}
|
||||
|
||||
class HttpServerFileUploadStreamTarget extends HttpServerFileUploadTarget {
|
||||
const HttpServerFileUploadStreamTarget();
|
||||
}
|
||||
|
||||
class HttpServerSetFileUploadTargetTask extends HttpServerTask {
|
||||
final int requestId;
|
||||
final HttpServerFileUploadTarget target;
|
||||
|
||||
const HttpServerSetFileUploadTargetTask({
|
||||
required this.requestId,
|
||||
required this.target,
|
||||
});
|
||||
}
|
||||
|
||||
class HttpServerRespondPrepareDownloadTask extends HttpServerTask {
|
||||
final int requestId;
|
||||
final bool accepted;
|
||||
|
||||
const HttpServerRespondPrepareDownloadTask({
|
||||
required this.requestId,
|
||||
required this.accepted,
|
||||
});
|
||||
}
|
||||
|
||||
sealed class HttpServerFileDownloadContent {
|
||||
const HttpServerFileDownloadContent();
|
||||
}
|
||||
|
||||
class HttpServerFileDownloadPathContent extends HttpServerFileDownloadContent {
|
||||
final String path;
|
||||
|
||||
const HttpServerFileDownloadPathContent({required this.path});
|
||||
}
|
||||
|
||||
class HttpServerFileDownloadDescriptorContent extends HttpServerFileDownloadContent {
|
||||
final int fileDescriptor;
|
||||
|
||||
const HttpServerFileDownloadDescriptorContent({required this.fileDescriptor});
|
||||
}
|
||||
|
||||
class HttpServerFileDownloadBytesContent extends HttpServerFileDownloadContent {
|
||||
final Uint8List bytes;
|
||||
|
||||
const HttpServerFileDownloadBytesContent({required this.bytes});
|
||||
}
|
||||
|
||||
class HttpServerFileDownloadStreamContent extends HttpServerFileDownloadContent {
|
||||
final Stream<List<int>> stream;
|
||||
|
||||
const HttpServerFileDownloadStreamContent({required this.stream});
|
||||
}
|
||||
|
||||
class HttpServerSetFileDownloadContentTask extends HttpServerTask {
|
||||
final int requestId;
|
||||
final HttpServerFileDownloadContent content;
|
||||
|
||||
const HttpServerSetFileDownloadContentTask({
|
||||
required this.requestId,
|
||||
required this.content,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> setupHttpServerIsolate(
|
||||
Stream<SendToIsolateData<HttpServerTask>> receiveFromMain,
|
||||
void Function(HttpServerEvent) sendToMain,
|
||||
InitialData initialData,
|
||||
) async {
|
||||
final bindings = _HttpServerBindings(sendToMain);
|
||||
await setupChildIsolateHelper(
|
||||
debugLabel: 'HttpServerIsolate',
|
||||
receiveFromMain: receiveFromMain,
|
||||
sendToMain: sendToMain,
|
||||
initialData: initialData,
|
||||
handler: (_, task) => bindings.handle(task),
|
||||
);
|
||||
}
|
||||
|
||||
class _HttpServerBindings {
|
||||
final void Function(HttpServerEvent) sendToMain;
|
||||
final Map<int, rust.RsHttpServerPrepareUploadRequest> _prepareUploads = {};
|
||||
final Map<int, rust.RsHttpServerFileUploadRequest> _fileUploads = {};
|
||||
final Map<int, rust.RsHttpServerPrepareDownloadRequest> _prepareDownloads = {};
|
||||
final Map<int, rust.RsHttpServerFileDownloadRequest> _fileDownloads = {};
|
||||
|
||||
rust.RsHttpServer? _server;
|
||||
StreamSubscription<rust.RsHttpServerEvent>? _eventSubscription;
|
||||
Future<void>? _startOperation;
|
||||
int _nextRequestId = 0;
|
||||
|
||||
_HttpServerBindings(this.sendToMain);
|
||||
|
||||
Future<void> handle(HttpServerTask task) async {
|
||||
try {
|
||||
switch (task) {
|
||||
case HttpServerStartTask():
|
||||
if (_startOperation != null || _server != null) {
|
||||
sendToMain(const HttpServerErrorEvent(error: 'HTTP server already started'));
|
||||
return;
|
||||
}
|
||||
final operation = _start(task);
|
||||
_startOperation = operation;
|
||||
try {
|
||||
await operation;
|
||||
} finally {
|
||||
if (identical(_startOperation, operation)) {
|
||||
_startOperation = null;
|
||||
}
|
||||
}
|
||||
case HttpServerStopTask():
|
||||
await _startOperation;
|
||||
await _stop();
|
||||
case HttpServerRespondPrepareUploadTask():
|
||||
await _respondPrepareUpload(task);
|
||||
case HttpServerSetFileUploadTargetTask():
|
||||
await _setFileUploadTarget(task);
|
||||
case HttpServerRespondPrepareDownloadTask():
|
||||
await _respondPrepareDownload(task);
|
||||
case HttpServerSetFileDownloadContentTask():
|
||||
await _setFileDownloadContent(task);
|
||||
}
|
||||
} catch (error) {
|
||||
sendToMain(HttpServerErrorEvent(error: error.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _start(HttpServerStartTask task) async {
|
||||
final server = rust.createHttpServer();
|
||||
_server = server;
|
||||
_eventSubscription = server.listen().listen(
|
||||
_handleEvent,
|
||||
onError: (Object error, StackTrace stackTrace) {
|
||||
sendToMain(HttpServerErrorEvent(error: error.toString()));
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await server.start(
|
||||
port: task.port,
|
||||
tls: task.https
|
||||
? rust.RsHttpServerTlsConfig(
|
||||
cert: task.securityContext.certificate,
|
||||
privateKey: task.securityContext.privateKey,
|
||||
)
|
||||
: null,
|
||||
info: rust.RsHttpServerInfo(
|
||||
alias: task.alias,
|
||||
version: protocolVersion,
|
||||
deviceModel: task.deviceModel,
|
||||
deviceType: rust.DeviceType.values.byName(task.deviceType.name),
|
||||
token: task.fingerprint,
|
||||
),
|
||||
internal: task.showToken == null ? null : rust.RsHttpServerInternalConfig(showToken: task.showToken!),
|
||||
v2: rust.RsHttpServerV2Config(pin: task.receivePin),
|
||||
webSend: task.webSend?._toRust(),
|
||||
);
|
||||
sendToMain(const HttpServerStartedEvent());
|
||||
} catch (error) {
|
||||
await _disposeServer();
|
||||
sendToMain(HttpServerErrorEvent(error: error.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _stop() async {
|
||||
if (_server == null) {
|
||||
return;
|
||||
}
|
||||
await _disposeServer();
|
||||
sendToMain(const HttpServerStoppedEvent());
|
||||
}
|
||||
|
||||
Future<void> _disposeServer() async {
|
||||
_server?.stop();
|
||||
_server = null;
|
||||
await _eventSubscription?.cancel();
|
||||
_eventSubscription = null;
|
||||
_prepareUploads.clear();
|
||||
_fileUploads.clear();
|
||||
_prepareDownloads.clear();
|
||||
_fileDownloads.clear();
|
||||
}
|
||||
|
||||
void _handleEvent(rust.RsHttpServerEvent event) {
|
||||
switch (event.kind()) {
|
||||
case rust.RsHttpServerEventKind.show_:
|
||||
sendToMain(HttpServerShowEvent(args: event.args()!));
|
||||
case rust.RsHttpServerEventKind.register:
|
||||
sendToMain(HttpServerRegisterEvent(ip: event.ip()!, info: event.info()!._toApp()));
|
||||
case rust.RsHttpServerEventKind.prepareUpload:
|
||||
final requestId = _newRequestId();
|
||||
_prepareUploads[requestId] = event.takePrepareUploadRequest()!;
|
||||
sendToMain(
|
||||
HttpServerPrepareUploadEvent(
|
||||
requestId: requestId,
|
||||
ip: event.ip()!,
|
||||
info: event.info()!._toApp(),
|
||||
files: event.files()!.map((id, file) => MapEntry(id, file._toApp())),
|
||||
),
|
||||
);
|
||||
case rust.RsHttpServerEventKind.fileUpload:
|
||||
final requestId = _newRequestId();
|
||||
_fileUploads[requestId] = event.takeFileUploadRequest()!;
|
||||
sendToMain(
|
||||
HttpServerFileUploadEvent(
|
||||
requestId: requestId,
|
||||
sessionId: event.sessionId()!,
|
||||
fileId: event.fileId()!,
|
||||
file: event.file()!._toApp(),
|
||||
),
|
||||
);
|
||||
case rust.RsHttpServerEventKind.sessionEnd:
|
||||
sendToMain(
|
||||
HttpServerSessionEndEvent(
|
||||
sessionId: event.sessionId()!,
|
||||
reason: HttpServerSessionEndReason.values.byName(event.reason()!.name),
|
||||
),
|
||||
);
|
||||
case rust.RsHttpServerEventKind.prepareDownload:
|
||||
final requestId = _newRequestId();
|
||||
_prepareDownloads[requestId] = event.takePrepareDownloadRequest()!;
|
||||
sendToMain(
|
||||
HttpServerPrepareDownloadEvent(
|
||||
requestId: requestId,
|
||||
ip: event.ip()!,
|
||||
sessionId: event.sessionId()!,
|
||||
userAgent: event.userAgent(),
|
||||
),
|
||||
);
|
||||
case rust.RsHttpServerEventKind.fileDownload:
|
||||
final requestId = _newRequestId();
|
||||
_fileDownloads[requestId] = event.takeFileDownloadRequest()!;
|
||||
sendToMain(
|
||||
HttpServerFileDownloadEvent(
|
||||
requestId: requestId,
|
||||
sessionId: event.sessionId()!,
|
||||
fileId: event.fileId()!,
|
||||
file: event.file()!._toApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int _newRequestId() => _nextRequestId++;
|
||||
|
||||
Future<void> _respondPrepareUpload(HttpServerRespondPrepareUploadTask task) async {
|
||||
final request = _prepareUploads.remove(task.requestId);
|
||||
if (request == null) {
|
||||
return;
|
||||
}
|
||||
final fileIds = task.fileIds;
|
||||
if (fileIds == null) {
|
||||
await request.decline();
|
||||
} else {
|
||||
await request.accept(fileIds: fileIds);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _respondPrepareDownload(HttpServerRespondPrepareDownloadTask task) async {
|
||||
final request = _prepareDownloads.remove(task.requestId);
|
||||
if (request == null) {
|
||||
return;
|
||||
}
|
||||
if (task.accepted) {
|
||||
await request.accept();
|
||||
} else {
|
||||
await request.decline();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setFileUploadTarget(HttpServerSetFileUploadTargetTask task) async {
|
||||
final request = _fileUploads.remove(task.requestId);
|
||||
if (request == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (task.target) {
|
||||
case HttpServerFileUploadPathTarget(:final path):
|
||||
await _reportFileUploadResult(task.requestId, request.saveToPath(path: path));
|
||||
case HttpServerFileUploadDescriptorTarget(:final fileDescriptor):
|
||||
await _reportFileUploadResult(task.requestId, request.saveToFileDescriptor(fd: fileDescriptor));
|
||||
case HttpServerFileUploadStreamTarget():
|
||||
var failed = false;
|
||||
request.receive().listen(
|
||||
(data) => sendToMain(HttpServerFileUploadChunkEvent(requestId: task.requestId, data: data)),
|
||||
onError: (Object error, StackTrace stackTrace) {
|
||||
failed = true;
|
||||
sendToMain(HttpServerFileUploadFinishedEvent(requestId: task.requestId, error: error.toString()));
|
||||
},
|
||||
onDone: () {
|
||||
if (!failed) {
|
||||
sendToMain(HttpServerFileUploadFinishedEvent(requestId: task.requestId, error: null));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _reportFileUploadResult(int requestId, Future<void> result) async {
|
||||
try {
|
||||
await result;
|
||||
sendToMain(HttpServerFileUploadFinishedEvent(requestId: requestId, error: null));
|
||||
} catch (error) {
|
||||
sendToMain(HttpServerFileUploadFinishedEvent(requestId: requestId, error: error.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setFileDownloadContent(HttpServerSetFileDownloadContentTask task) async {
|
||||
final request = _fileDownloads.remove(task.requestId);
|
||||
if (request == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (task.content) {
|
||||
case HttpServerFileDownloadPathContent(:final path):
|
||||
await request.providePath(path: path);
|
||||
case HttpServerFileDownloadDescriptorContent(:final fileDescriptor):
|
||||
await request.provideFileDescriptor(fd: fileDescriptor);
|
||||
case HttpServerFileDownloadBytesContent(:final bytes):
|
||||
await request.provideBytes(data: bytes);
|
||||
case HttpServerFileDownloadStreamContent(:final stream):
|
||||
final (sink, receiver) = await createStream();
|
||||
await request.provideStream(stream: receiver);
|
||||
unawaited(
|
||||
_pipeStream(stream, sink).catchError((Object error) {
|
||||
sink.close();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pipeStream(Stream<List<int>> stream, Dart2RustStreamSink sink) async {
|
||||
try {
|
||||
await for (final data in stream) {
|
||||
await sink.add(data: data);
|
||||
}
|
||||
} finally {
|
||||
sink.close();
|
||||
}
|
||||
}
|
||||
|
||||
extension on HttpServerWebSendConfig {
|
||||
rust.RsHttpServerWebSendConfig _toRust() => rust.RsHttpServerWebSendConfig(
|
||||
files: files.map((id, file) => MapEntry(id, file._toRust())),
|
||||
pin: pin,
|
||||
i18N: rust.RsHttpServerWebSendI18n(
|
||||
waiting: i18n.waiting,
|
||||
enterPin: i18n.enterPin,
|
||||
invalidPin: i18n.invalidPin,
|
||||
tooManyAttempts: i18n.tooManyAttempts,
|
||||
rejected: i18n.rejected,
|
||||
files: i18n.files,
|
||||
fileName: i18n.fileName,
|
||||
size: i18n.size,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
extension on rust.RegisterDto {
|
||||
app.RegisterDto _toApp() => app.RegisterDto(
|
||||
alias: alias,
|
||||
version: version,
|
||||
deviceModel: deviceModel,
|
||||
deviceType: deviceType == null ? null : app.DeviceType.values.byName(deviceType!.name),
|
||||
fingerprint: token,
|
||||
port: port,
|
||||
protocol: app.ProtocolType.values.byName(protocol.name),
|
||||
download: hasWebInterface,
|
||||
);
|
||||
}
|
||||
|
||||
extension on rust.FileDto {
|
||||
app.FileDto _toApp() => app.FileDto(
|
||||
id: id,
|
||||
fileName: fileName,
|
||||
size: size.toInt(),
|
||||
fileType: app.decodeFromMime(fileType),
|
||||
hash: sha256,
|
||||
preview: preview,
|
||||
metadata: metadata == null
|
||||
? null
|
||||
: app.FileMetadata(
|
||||
lastModified: metadata!.modified == null ? null : DateTime.tryParse(metadata!.modified!),
|
||||
lastAccessed: metadata!.accessed == null ? null : DateTime.tryParse(metadata!.accessed!),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
extension on app.FileDto {
|
||||
rust.FileDto _toRust() => rust.FileDto(
|
||||
id: id,
|
||||
fileName: fileName,
|
||||
size: BigInt.from(size),
|
||||
fileType: lookupMime(),
|
||||
sha256: hash,
|
||||
preview: preview,
|
||||
metadata: metadata == null
|
||||
? null
|
||||
: rust.FileMetadata(
|
||||
modified: metadata!.lastModified?.toIso8601String(),
|
||||
accessed: metadata!.lastAccessed?.toIso8601String(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// 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:localsend_app/rust/api/model.dart';
|
||||
import 'package:localsend_app/rust/api/stream.dart';
|
||||
import 'package:localsend_app/rust/frb_generated.dart';
|
||||
|
||||
// These functions are ignored because they are not marked as `pub`: `flatten_file_result`, `register_dto_from_v2`, `respond`, `respond`, `send_content`, `spawn_internal_event_forwarder`, `spawn_v2_event_forwarder`, `spawn_web_send_event_forwarder`, `take_target`
|
||||
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `clone`, `clone`, `drop`, `eq`, `eq`, `fmt`, `fmt`
|
||||
|
||||
RsHttpServer createHttpServer() => RustLib.instance.api.crateApiHttpServerCreateHttpServer();
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsHttpServer>>
|
||||
abstract class RsHttpServer implements RustOpaqueInterface {
|
||||
Stream<RsHttpServerEvent> listen();
|
||||
|
||||
Future<void> start({
|
||||
required int port,
|
||||
RsHttpServerTlsConfig? tls,
|
||||
required RsHttpServerInfo info,
|
||||
RsHttpServerInternalConfig? internal,
|
||||
RsHttpServerV2Config? v2,
|
||||
RsHttpServerWebSendConfig? webSend,
|
||||
});
|
||||
|
||||
void stop();
|
||||
}
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsHttpServerEvent>>
|
||||
abstract class RsHttpServerEvent implements RustOpaqueInterface {
|
||||
List<String>? args();
|
||||
|
||||
FileDto? file();
|
||||
|
||||
String? fileId();
|
||||
|
||||
Map<String, FileDto>? files();
|
||||
|
||||
RegisterDto? info();
|
||||
|
||||
String? ip();
|
||||
|
||||
RsHttpServerEventKind kind();
|
||||
|
||||
RsHttpServerSessionEndReason? reason();
|
||||
|
||||
String? sessionId();
|
||||
|
||||
RsHttpServerFileDownloadRequest? takeFileDownloadRequest();
|
||||
|
||||
RsHttpServerFileUploadRequest? takeFileUploadRequest();
|
||||
|
||||
RsHttpServerPrepareDownloadRequest? takePrepareDownloadRequest();
|
||||
|
||||
RsHttpServerPrepareUploadRequest? takePrepareUploadRequest();
|
||||
|
||||
String? userAgent();
|
||||
}
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsHttpServerFileDownloadRequest>>
|
||||
abstract class RsHttpServerFileDownloadRequest implements RustOpaqueInterface {
|
||||
Future<void> provideBytes({required List<int> data});
|
||||
|
||||
Future<void> provideFileDescriptor({required int fd});
|
||||
|
||||
Future<void> providePath({required String path});
|
||||
|
||||
Future<void> provideStream({required Dart2RustStreamReceiver stream});
|
||||
}
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsHttpServerFileUploadRequest>>
|
||||
abstract class RsHttpServerFileUploadRequest implements RustOpaqueInterface {
|
||||
Stream<Uint8List> receive();
|
||||
|
||||
Future<void> saveToFileDescriptor({required int fd});
|
||||
|
||||
Future<void> saveToPath({required String path});
|
||||
}
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsHttpServerPrepareDownloadRequest>>
|
||||
abstract class RsHttpServerPrepareDownloadRequest implements RustOpaqueInterface {
|
||||
Future<void> accept();
|
||||
|
||||
Future<void> decline();
|
||||
}
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsHttpServerPrepareUploadRequest>>
|
||||
abstract class RsHttpServerPrepareUploadRequest implements RustOpaqueInterface {
|
||||
Future<void> accept({required Set<String> fileIds});
|
||||
|
||||
Future<void> decline();
|
||||
}
|
||||
|
||||
enum RsHttpServerEventKind {
|
||||
show_,
|
||||
register,
|
||||
prepareUpload,
|
||||
fileUpload,
|
||||
sessionEnd,
|
||||
prepareDownload,
|
||||
fileDownload,
|
||||
}
|
||||
|
||||
class RsHttpServerInfo {
|
||||
final String alias;
|
||||
final String version;
|
||||
final String? deviceModel;
|
||||
final DeviceType? deviceType;
|
||||
final String token;
|
||||
|
||||
const RsHttpServerInfo({
|
||||
required this.alias,
|
||||
required this.version,
|
||||
this.deviceModel,
|
||||
this.deviceType,
|
||||
required this.token,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode => alias.hashCode ^ version.hashCode ^ deviceModel.hashCode ^ deviceType.hashCode ^ token.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is RsHttpServerInfo &&
|
||||
runtimeType == other.runtimeType &&
|
||||
alias == other.alias &&
|
||||
version == other.version &&
|
||||
deviceModel == other.deviceModel &&
|
||||
deviceType == other.deviceType &&
|
||||
token == other.token;
|
||||
}
|
||||
|
||||
class RsHttpServerInternalConfig {
|
||||
final String showToken;
|
||||
|
||||
const RsHttpServerInternalConfig({
|
||||
required this.showToken,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode => showToken.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) || other is RsHttpServerInternalConfig && runtimeType == other.runtimeType && showToken == other.showToken;
|
||||
}
|
||||
|
||||
enum RsHttpServerSessionEndReason {
|
||||
finished,
|
||||
cancelled,
|
||||
}
|
||||
|
||||
class RsHttpServerTlsConfig {
|
||||
final String cert;
|
||||
final String privateKey;
|
||||
|
||||
const RsHttpServerTlsConfig({
|
||||
required this.cert,
|
||||
required this.privateKey,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode => cert.hashCode ^ privateKey.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is RsHttpServerTlsConfig && runtimeType == other.runtimeType && cert == other.cert && privateKey == other.privateKey;
|
||||
}
|
||||
|
||||
class RsHttpServerV2Config {
|
||||
final String? pin;
|
||||
|
||||
const RsHttpServerV2Config({
|
||||
this.pin,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode => pin.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is RsHttpServerV2Config && runtimeType == other.runtimeType && pin == other.pin;
|
||||
}
|
||||
|
||||
class RsHttpServerWebSendConfig {
|
||||
final Map<String, FileDto> files;
|
||||
final String? pin;
|
||||
final RsHttpServerWebSendI18n i18N;
|
||||
|
||||
const RsHttpServerWebSendConfig({
|
||||
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 RsHttpServerWebSendConfig && runtimeType == other.runtimeType && files == other.files && pin == other.pin && i18N == other.i18N;
|
||||
}
|
||||
|
||||
class RsHttpServerWebSendI18n {
|
||||
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 RsHttpServerWebSendI18n({
|
||||
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 RsHttpServerWebSendI18n &&
|
||||
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;
|
||||
}
|
||||
+2784
-1360
File diff suppressed because it is too large
Load Diff
+957
-453
File diff suppressed because it is too large
Load Diff
+895
-597
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,740 @@
|
||||
use crate::api::stream::Dart2RustStreamReceiver;
|
||||
use crate::frb_generated::StreamSink;
|
||||
use bytes::Bytes;
|
||||
use flutter_rust_bridge::frb;
|
||||
use localsend::http::dto::{ProtocolType, RegisterDto};
|
||||
use localsend::http::dto_v2::{ProtocolTypeV2, RegisterDtoV2};
|
||||
use localsend::http::server::common::save::FileUploadTarget;
|
||||
use localsend::http::server::internal::{InternalConfig, InternalEvent};
|
||||
use localsend::http::server::v2::{PrepareUploadDecisionV2, ServerEventV2, SessionEndReasonV2};
|
||||
use localsend::http::server::web::{WebSendConfig, WebSendEvent, WebSendI18n};
|
||||
use localsend::http::server::{ServerConfigV2, TlsConfig};
|
||||
use localsend::http::state::ClientInfo;
|
||||
use localsend::model::transfer::{FileContent, FileDto};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use tokio::sync::{Mutex, mpsc, oneshot};
|
||||
|
||||
const EVENT_CHANNEL_CAPACITY: usize = 32;
|
||||
const FILE_CHANNEL_CAPACITY: usize = 16;
|
||||
|
||||
pub struct RsHttpServerTlsConfig {
|
||||
pub cert: String,
|
||||
pub private_key: String,
|
||||
}
|
||||
|
||||
pub struct RsHttpServerInfo {
|
||||
pub alias: String,
|
||||
pub version: String,
|
||||
pub device_model: Option<String>,
|
||||
pub device_type: Option<localsend::model::discovery::DeviceType>,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
pub struct RsHttpServerInternalConfig {
|
||||
pub show_token: String,
|
||||
}
|
||||
|
||||
pub struct RsHttpServerV2Config {
|
||||
pub pin: Option<String>,
|
||||
}
|
||||
|
||||
pub struct RsHttpServerWebSendI18n {
|
||||
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,
|
||||
}
|
||||
|
||||
pub struct RsHttpServerWebSendConfig {
|
||||
pub files: HashMap<String, FileDto>,
|
||||
pub pin: Option<String>,
|
||||
pub i18n: RsHttpServerWebSendI18n,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum RsHttpServerSessionEndReason {
|
||||
Finished,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[frb(opaque)]
|
||||
pub enum RsHttpServerEvent {
|
||||
Show {
|
||||
args: Vec<String>,
|
||||
},
|
||||
Register {
|
||||
ip: String,
|
||||
info: RegisterDto,
|
||||
},
|
||||
PrepareUpload {
|
||||
ip: String,
|
||||
info: RegisterDto,
|
||||
files: HashMap<String, FileDto>,
|
||||
request: Option<RsHttpServerPrepareUploadRequest>,
|
||||
},
|
||||
FileUpload {
|
||||
session_id: String,
|
||||
file_id: String,
|
||||
file: FileDto,
|
||||
request: Option<RsHttpServerFileUploadRequest>,
|
||||
},
|
||||
SessionEnd {
|
||||
session_id: String,
|
||||
reason: RsHttpServerSessionEndReason,
|
||||
},
|
||||
PrepareDownload {
|
||||
ip: String,
|
||||
session_id: String,
|
||||
user_agent: Option<String>,
|
||||
request: Option<RsHttpServerPrepareDownloadRequest>,
|
||||
},
|
||||
FileDownload {
|
||||
session_id: String,
|
||||
file_id: String,
|
||||
file: FileDto,
|
||||
request: Option<RsHttpServerFileDownloadRequest>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum RsHttpServerEventKind {
|
||||
Show,
|
||||
Register,
|
||||
PrepareUpload,
|
||||
FileUpload,
|
||||
SessionEnd,
|
||||
PrepareDownload,
|
||||
FileDownload,
|
||||
}
|
||||
|
||||
impl RsHttpServerEvent {
|
||||
#[frb(sync)]
|
||||
pub fn kind(&self) -> RsHttpServerEventKind {
|
||||
match self {
|
||||
Self::Show { .. } => RsHttpServerEventKind::Show,
|
||||
Self::Register { .. } => RsHttpServerEventKind::Register,
|
||||
Self::PrepareUpload { .. } => RsHttpServerEventKind::PrepareUpload,
|
||||
Self::FileUpload { .. } => RsHttpServerEventKind::FileUpload,
|
||||
Self::SessionEnd { .. } => RsHttpServerEventKind::SessionEnd,
|
||||
Self::PrepareDownload { .. } => RsHttpServerEventKind::PrepareDownload,
|
||||
Self::FileDownload { .. } => RsHttpServerEventKind::FileDownload,
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn args(&self) -> Option<Vec<String>> {
|
||||
match self {
|
||||
Self::Show { args } => Some(args.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn ip(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Register { ip, .. }
|
||||
| Self::PrepareUpload { ip, .. }
|
||||
| Self::PrepareDownload { ip, .. } => Some(ip.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn info(&self) -> Option<RegisterDto> {
|
||||
match self {
|
||||
Self::Register { info, .. } | Self::PrepareUpload { info, .. } => Some(info.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn files(&self) -> Option<HashMap<String, FileDto>> {
|
||||
match self {
|
||||
Self::PrepareUpload { files, .. } => Some(files.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn session_id(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::FileUpload { session_id, .. }
|
||||
| Self::SessionEnd { session_id, .. }
|
||||
| Self::PrepareDownload { session_id, .. }
|
||||
| Self::FileDownload { session_id, .. } => Some(session_id.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn file_id(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::FileUpload { file_id, .. } | Self::FileDownload { file_id, .. } => {
|
||||
Some(file_id.clone())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn file(&self) -> Option<FileDto> {
|
||||
match self {
|
||||
Self::FileUpload { file, .. } | Self::FileDownload { file, .. } => Some(file.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn reason(&self) -> Option<RsHttpServerSessionEndReason> {
|
||||
match self {
|
||||
Self::SessionEnd { reason, .. } => Some(*reason),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn user_agent(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::PrepareDownload { user_agent, .. } => user_agent.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn take_prepare_upload_request(&mut self) -> Option<RsHttpServerPrepareUploadRequest> {
|
||||
match self {
|
||||
Self::PrepareUpload { request, .. } => request.take(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn take_file_upload_request(&mut self) -> Option<RsHttpServerFileUploadRequest> {
|
||||
match self {
|
||||
Self::FileUpload { request, .. } => request.take(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn take_prepare_download_request(&mut self) -> Option<RsHttpServerPrepareDownloadRequest> {
|
||||
match self {
|
||||
Self::PrepareDownload { request, .. } => request.take(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn take_file_download_request(&mut self) -> Option<RsHttpServerFileDownloadRequest> {
|
||||
match self {
|
||||
Self::FileDownload { request, .. } => request.take(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(opaque)]
|
||||
pub struct RsHttpServer {
|
||||
event_tx: StdMutex<Option<mpsc::Sender<RsHttpServerEvent>>>,
|
||||
event_rx: Mutex<Option<mpsc::Receiver<RsHttpServerEvent>>>,
|
||||
stop_tx: StdMutex<Option<oneshot::Sender<()>>>,
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn create_http_server() -> RsHttpServer {
|
||||
let (event_tx, event_rx) = mpsc::channel(EVENT_CHANNEL_CAPACITY);
|
||||
RsHttpServer {
|
||||
event_tx: StdMutex::new(Some(event_tx)),
|
||||
event_rx: Mutex::new(Some(event_rx)),
|
||||
stop_tx: StdMutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
impl RsHttpServer {
|
||||
pub async fn start(
|
||||
&self,
|
||||
port: u16,
|
||||
tls: Option<RsHttpServerTlsConfig>,
|
||||
info: RsHttpServerInfo,
|
||||
internal: Option<RsHttpServerInternalConfig>,
|
||||
v2: Option<RsHttpServerV2Config>,
|
||||
web_send: Option<RsHttpServerWebSendConfig>,
|
||||
) -> anyhow::Result<()> {
|
||||
if self.stop_tx.lock().unwrap().is_some() {
|
||||
return Err(anyhow::anyhow!("HTTP server already started"));
|
||||
}
|
||||
|
||||
let event_tx = self
|
||||
.event_tx
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow::anyhow!("HTTP server already stopped"))?;
|
||||
|
||||
let internal_config = internal.map(|config| {
|
||||
let (tx, rx) = mpsc::channel(EVENT_CHANNEL_CAPACITY);
|
||||
spawn_internal_event_forwarder(rx, event_tx.clone());
|
||||
InternalConfig {
|
||||
show_token: config.show_token,
|
||||
event_tx: tx,
|
||||
}
|
||||
});
|
||||
|
||||
let v2_config = v2.map(|config| {
|
||||
let (tx, rx) = mpsc::channel(EVENT_CHANNEL_CAPACITY);
|
||||
spawn_v2_event_forwarder(rx, event_tx.clone());
|
||||
ServerConfigV2 {
|
||||
pin: config.pin,
|
||||
event_tx: tx,
|
||||
}
|
||||
});
|
||||
|
||||
let web_send_config = web_send.map(|config| {
|
||||
let (tx, rx) = mpsc::channel(EVENT_CHANNEL_CAPACITY);
|
||||
spawn_web_send_event_forwarder(rx, event_tx);
|
||||
WebSendConfig {
|
||||
files: config.files,
|
||||
pin: config.pin,
|
||||
i18n: WebSendI18n {
|
||||
waiting: config.i18n.waiting,
|
||||
enter_pin: config.i18n.enter_pin,
|
||||
invalid_pin: config.i18n.invalid_pin,
|
||||
too_many_attempts: config.i18n.too_many_attempts,
|
||||
rejected: config.i18n.rejected,
|
||||
files: config.i18n.files,
|
||||
file_name: config.i18n.file_name,
|
||||
size: config.i18n.size,
|
||||
},
|
||||
event_tx: tx,
|
||||
}
|
||||
});
|
||||
|
||||
let (stop_tx, stop_rx) = oneshot::channel();
|
||||
localsend::http::server::start_with_port(
|
||||
port,
|
||||
tls.map(|tls| TlsConfig {
|
||||
cert: tls.cert,
|
||||
private_key: tls.private_key,
|
||||
}),
|
||||
ClientInfo {
|
||||
alias: info.alias,
|
||||
version: info.version,
|
||||
device_model: info.device_model,
|
||||
device_type: info.device_type,
|
||||
token: info.token,
|
||||
},
|
||||
internal_config,
|
||||
v2_config,
|
||||
web_send_config,
|
||||
stop_rx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
*self.stop_tx.lock().unwrap() = Some(stop_tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn listen(&self, sink: StreamSink<RsHttpServerEvent>) {
|
||||
let Some(mut event_rx) = self.event_rx.lock().await.take() else {
|
||||
let _ = sink.add_error(anyhow::anyhow!("HTTP server events already listened to"));
|
||||
return;
|
||||
};
|
||||
|
||||
while let Some(event) = event_rx.recv().await {
|
||||
if sink.add(event).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn stop(&self) {
|
||||
if let Some(stop_tx) = self.stop_tx.lock().unwrap().take() {
|
||||
let _ = stop_tx.send(());
|
||||
}
|
||||
self.event_tx.lock().unwrap().take();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RsHttpServer {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(stop_tx) = self.stop_tx.get_mut() {
|
||||
if let Some(stop_tx) = stop_tx.take() {
|
||||
let _ = stop_tx.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(opaque)]
|
||||
pub struct RsHttpServerPrepareUploadRequest {
|
||||
decision_tx: Mutex<Option<oneshot::Sender<PrepareUploadDecisionV2>>>,
|
||||
}
|
||||
|
||||
impl RsHttpServerPrepareUploadRequest {
|
||||
pub async fn accept(&self, file_ids: HashSet<String>) -> anyhow::Result<()> {
|
||||
self.respond(PrepareUploadDecisionV2::Accept(file_ids))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn decline(&self) -> anyhow::Result<()> {
|
||||
self.respond(PrepareUploadDecisionV2::Decline).await
|
||||
}
|
||||
|
||||
async fn respond(&self, decision: PrepareUploadDecisionV2) -> anyhow::Result<()> {
|
||||
let decision_tx = self
|
||||
.decision_tx
|
||||
.lock()
|
||||
.await
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("Prepare-upload request already answered"))?;
|
||||
decision_tx
|
||||
.send(decision)
|
||||
.map_err(|_| anyhow::anyhow!("Prepare-upload request closed"))
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(opaque)]
|
||||
pub struct RsHttpServerFileUploadRequest {
|
||||
file_size: u64,
|
||||
target_tx: Mutex<Option<oneshot::Sender<FileUploadTarget>>>,
|
||||
}
|
||||
|
||||
impl RsHttpServerFileUploadRequest {
|
||||
pub async fn save_to_path(&self, path: String) -> anyhow::Result<()> {
|
||||
let target_tx = self.take_target().await?;
|
||||
let (result_tx, result_rx) = oneshot::channel();
|
||||
target_tx
|
||||
.send(FileUploadTarget::Path {
|
||||
path: PathBuf::from(path),
|
||||
result_tx,
|
||||
})
|
||||
.map_err(|_| anyhow::anyhow!("File-upload request closed"))?;
|
||||
flatten_file_result(result_rx).await
|
||||
}
|
||||
|
||||
pub async fn save_to_file_descriptor(&self, fd: i32) -> anyhow::Result<()> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let target_tx = self.take_target().await?;
|
||||
let (result_tx, result_rx) = oneshot::channel();
|
||||
target_tx
|
||||
.send(FileUploadTarget::Fd { fd, result_tx })
|
||||
.map_err(|_| anyhow::anyhow!("File-upload request closed"))?;
|
||||
return flatten_file_result(result_rx).await;
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let _ = fd;
|
||||
Err(anyhow::anyhow!(
|
||||
"File descriptors are only supported on Android"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn receive(&self, sink: StreamSink<Vec<u8>>) -> anyhow::Result<()> {
|
||||
let target_tx = self.take_target().await?;
|
||||
let (binary_tx, mut binary_rx) = mpsc::channel::<Bytes>(FILE_CHANNEL_CAPACITY);
|
||||
let (result_tx, result_rx) = oneshot::channel::<Result<(), String>>();
|
||||
target_tx
|
||||
.send(FileUploadTarget::Stream {
|
||||
binary_tx,
|
||||
result_rx,
|
||||
})
|
||||
.map_err(|_| anyhow::anyhow!("File-upload request closed"))?;
|
||||
|
||||
let mut received = 0_u64;
|
||||
let result = 'receive: {
|
||||
while let Some(chunk) = binary_rx.recv().await {
|
||||
received += chunk.len() as u64;
|
||||
if received > self.file_size {
|
||||
break 'receive Err(format!(
|
||||
"Expected {} bytes, received at least {received}",
|
||||
self.file_size
|
||||
));
|
||||
}
|
||||
if sink.add(chunk.to_vec()).is_err() {
|
||||
break 'receive Err("File-upload stream listener closed".to_string());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let result = result.and_then(|()| {
|
||||
if received == self.file_size {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"Expected {} bytes, received {received}",
|
||||
self.file_size
|
||||
))
|
||||
}
|
||||
});
|
||||
let _ = result_tx.send(result.clone());
|
||||
result.map_err(anyhow::Error::msg)
|
||||
}
|
||||
|
||||
async fn take_target(&self) -> anyhow::Result<oneshot::Sender<FileUploadTarget>> {
|
||||
self.target_tx
|
||||
.lock()
|
||||
.await
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("File-upload request already answered"))
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(opaque)]
|
||||
pub struct RsHttpServerPrepareDownloadRequest {
|
||||
decision_tx: Mutex<Option<oneshot::Sender<bool>>>,
|
||||
}
|
||||
|
||||
impl RsHttpServerPrepareDownloadRequest {
|
||||
pub async fn accept(&self) -> anyhow::Result<()> {
|
||||
self.respond(true).await
|
||||
}
|
||||
|
||||
pub async fn decline(&self) -> anyhow::Result<()> {
|
||||
self.respond(false).await
|
||||
}
|
||||
|
||||
async fn respond(&self, accepted: bool) -> anyhow::Result<()> {
|
||||
let decision_tx = self
|
||||
.decision_tx
|
||||
.lock()
|
||||
.await
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("Prepare-download request already answered"))?;
|
||||
decision_tx
|
||||
.send(accepted)
|
||||
.map_err(|_| anyhow::anyhow!("Prepare-download request closed"))
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(opaque)]
|
||||
pub struct RsHttpServerFileDownloadRequest {
|
||||
content_tx: Mutex<Option<oneshot::Sender<FileContent>>>,
|
||||
}
|
||||
|
||||
impl RsHttpServerFileDownloadRequest {
|
||||
pub async fn provide_path(&self, path: String) -> anyhow::Result<()> {
|
||||
self.send_content(FileContent::Path(PathBuf::from(path)))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn provide_file_descriptor(&self, fd: i32) -> anyhow::Result<()> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
return self.send_content(FileContent::Fd(fd)).await;
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let _ = fd;
|
||||
Err(anyhow::anyhow!(
|
||||
"File descriptors are only supported on Android"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn provide_bytes(&self, data: Vec<u8>) -> anyhow::Result<()> {
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
tx.send(Bytes::from(data))
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("Failed to buffer download content"))?;
|
||||
drop(tx);
|
||||
self.send_content(FileContent::Stream(rx)).await
|
||||
}
|
||||
|
||||
pub async fn provide_stream(&self, stream: Dart2RustStreamReceiver) -> anyhow::Result<()> {
|
||||
self.send_content(FileContent::Stream(stream.receiver))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn send_content(&self, content: FileContent) -> anyhow::Result<()> {
|
||||
let content_tx = self
|
||||
.content_tx
|
||||
.lock()
|
||||
.await
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("File-download request already answered"))?;
|
||||
content_tx
|
||||
.send(content)
|
||||
.map_err(|_| anyhow::anyhow!("File-download request closed"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn flatten_file_result(
|
||||
result_rx: oneshot::Receiver<Result<(), String>>,
|
||||
) -> anyhow::Result<()> {
|
||||
result_rx
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("File-upload result channel closed"))?
|
||||
.map_err(anyhow::Error::msg)
|
||||
}
|
||||
|
||||
fn spawn_internal_event_forwarder(
|
||||
mut rx: mpsc::Receiver<InternalEvent>,
|
||||
event_tx: mpsc::Sender<RsHttpServerEvent>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
let event = match event {
|
||||
InternalEvent::Show { args } => RsHttpServerEvent::Show { args },
|
||||
};
|
||||
if event_tx.send(event).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn spawn_v2_event_forwarder(
|
||||
mut rx: mpsc::Receiver<ServerEventV2>,
|
||||
event_tx: mpsc::Sender<RsHttpServerEvent>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
let event = match event {
|
||||
ServerEventV2::Register { ip, info } => RsHttpServerEvent::Register {
|
||||
ip: ip.to_string(),
|
||||
info: register_dto_from_v2(info),
|
||||
},
|
||||
ServerEventV2::PrepareUpload {
|
||||
ip,
|
||||
info,
|
||||
files,
|
||||
decision_tx,
|
||||
} => RsHttpServerEvent::PrepareUpload {
|
||||
ip: ip.to_string(),
|
||||
info: register_dto_from_v2(info),
|
||||
files,
|
||||
request: Some(RsHttpServerPrepareUploadRequest {
|
||||
decision_tx: Mutex::new(Some(decision_tx)),
|
||||
}),
|
||||
},
|
||||
ServerEventV2::FileUpload {
|
||||
session_id,
|
||||
file_id,
|
||||
file,
|
||||
target_tx,
|
||||
} => {
|
||||
let file_size = file.size;
|
||||
RsHttpServerEvent::FileUpload {
|
||||
session_id,
|
||||
file_id,
|
||||
file,
|
||||
request: Some(RsHttpServerFileUploadRequest {
|
||||
file_size,
|
||||
target_tx: Mutex::new(Some(target_tx)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
ServerEventV2::SessionEnd { session_id, reason } => RsHttpServerEvent::SessionEnd {
|
||||
session_id,
|
||||
reason: match reason {
|
||||
SessionEndReasonV2::Finished => RsHttpServerSessionEndReason::Finished,
|
||||
SessionEndReasonV2::Cancelled => RsHttpServerSessionEndReason::Cancelled,
|
||||
},
|
||||
},
|
||||
};
|
||||
if event_tx.send(event).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn spawn_web_send_event_forwarder(
|
||||
mut rx: mpsc::Receiver<WebSendEvent>,
|
||||
event_tx: mpsc::Sender<RsHttpServerEvent>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
let event = match event {
|
||||
WebSendEvent::PrepareDownload {
|
||||
ip,
|
||||
session_id,
|
||||
user_agent,
|
||||
decision_tx,
|
||||
} => RsHttpServerEvent::PrepareDownload {
|
||||
ip: ip.to_string(),
|
||||
session_id,
|
||||
user_agent,
|
||||
request: Some(RsHttpServerPrepareDownloadRequest {
|
||||
decision_tx: Mutex::new(Some(decision_tx)),
|
||||
}),
|
||||
},
|
||||
WebSendEvent::FileDownload {
|
||||
session_id,
|
||||
file_id,
|
||||
file,
|
||||
content_tx,
|
||||
} => RsHttpServerEvent::FileDownload {
|
||||
session_id,
|
||||
file_id,
|
||||
file,
|
||||
request: Some(RsHttpServerFileDownloadRequest {
|
||||
content_tx: Mutex::new(Some(content_tx)),
|
||||
}),
|
||||
},
|
||||
};
|
||||
if event_tx.send(event).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn register_dto_from_v2(info: RegisterDtoV2) -> RegisterDto {
|
||||
RegisterDto {
|
||||
alias: info.alias,
|
||||
version: info.version,
|
||||
device_model: info.device_model,
|
||||
device_type: info.device_type,
|
||||
token: info.fingerprint,
|
||||
port: info.port,
|
||||
protocol: match info.protocol {
|
||||
ProtocolTypeV2::Http => ProtocolType::Http,
|
||||
ProtocolTypeV2::Https => ProtocolType::Https,
|
||||
},
|
||||
has_web_interface: info.download,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn starts_and_stops_server() {
|
||||
let listener = std::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)).unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
drop(listener);
|
||||
|
||||
let server = create_http_server();
|
||||
server
|
||||
.start(
|
||||
port,
|
||||
None,
|
||||
RsHttpServerInfo {
|
||||
alias: "test".to_string(),
|
||||
version: "2.1".to_string(),
|
||||
device_model: None,
|
||||
device_type: None,
|
||||
token: "test-token".to_string(),
|
||||
},
|
||||
None,
|
||||
Some(RsHttpServerV2Config { pin: None }),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod crypto;
|
||||
pub mod http;
|
||||
pub mod http_server;
|
||||
pub mod logging;
|
||||
pub mod model;
|
||||
pub mod stream;
|
||||
|
||||
+2874
-136
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user