mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
feat: send sha256
This commit is contained in:
@@ -220,6 +220,7 @@
|
|||||||
"saveToGalleryOff": "Turned off automatically because there are folders."
|
"saveToGalleryOff": "Turned off automatically because there are folders."
|
||||||
},
|
},
|
||||||
"sendPage": {
|
"sendPage": {
|
||||||
|
"calculatingChecksum": "Calculating checksum ({curr} / {n})",
|
||||||
"waiting": "Waiting for response…",
|
"waiting": "Waiting for response…",
|
||||||
"rejected": "The recipient has rejected the request.",
|
"rejected": "The recipient has rejected the request.",
|
||||||
"tooManyAttempts": "@:web.tooManyAttempts",
|
"tooManyAttempts": "@:web.tooManyAttempts",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 55
|
/// Locales: 55
|
||||||
/// Strings: 18424 (334 per locale)
|
/// Strings: 18425 (335 per locale)
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
@@ -132,7 +132,8 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
|
|||||||
srCyrl(languageCode: 'sr', scriptCode: 'Cyrl'),
|
srCyrl(languageCode: 'sr', scriptCode: 'Cyrl'),
|
||||||
zhCn(languageCode: 'zh', countryCode: 'CN'),
|
zhCn(languageCode: 'zh', countryCode: 'CN'),
|
||||||
zhHk(languageCode: 'zh', countryCode: 'HK'),
|
zhHk(languageCode: 'zh', countryCode: 'HK'),
|
||||||
zhTw(languageCode: 'zh', countryCode: 'TW');
|
zhTw(languageCode: 'zh', countryCode: 'TW')
|
||||||
|
;
|
||||||
|
|
||||||
const AppLocale({
|
const AppLocale({
|
||||||
required this.languageCode,
|
required this.languageCode,
|
||||||
|
|||||||
@@ -440,6 +440,9 @@ class Translations$sendPage$en {
|
|||||||
|
|
||||||
// Translations
|
// Translations
|
||||||
|
|
||||||
|
/// en: 'Calculating checksum ({curr} / {n})'
|
||||||
|
String calculatingChecksum({required Object curr, required Object n}) => 'Calculating checksum (${curr} / ${n})';
|
||||||
|
|
||||||
/// en: 'Waiting for response…'
|
/// en: 'Waiting for response…'
|
||||||
String get waiting => 'Waiting for response…';
|
String get waiting => 'Waiting for response…';
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ class SendSessionState with SendSessionStateMappable implements SessionState {
|
|||||||
final Device target;
|
final Device target;
|
||||||
final Map<String, SendingFile> files; // file id as key
|
final Map<String, SendingFile> files; // file id as key
|
||||||
|
|
||||||
|
/// Amount of files whose checksum has been calculated.
|
||||||
|
/// The checksums are calculated before the request is sent to the receiver,
|
||||||
|
/// so this is less than the file count while the session is being prepared.
|
||||||
|
final int hashedFileCount;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final int? startTime;
|
final int? startTime;
|
||||||
|
|
||||||
@@ -34,6 +39,7 @@ class SendSessionState with SendSessionStateMappable implements SessionState {
|
|||||||
required this.status,
|
required this.status,
|
||||||
required this.target,
|
required this.target,
|
||||||
required this.files,
|
required this.files,
|
||||||
|
required this.hashedFileCount,
|
||||||
required this.startTime,
|
required this.startTime,
|
||||||
required this.endTime,
|
required this.endTime,
|
||||||
required this.sendingTasks,
|
required this.sendingTasks,
|
||||||
@@ -45,7 +51,7 @@ class SendSessionState with SendSessionStateMappable implements SessionState {
|
|||||||
/// SendingFile.
|
/// SendingFile.
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SendSessionState(sessionId: $sessionId, remoteSessionId: $remoteSessionId, background: $background, status: $status, target: $target, files: $files, startTime: $startTime, endTime: $endTime, sendingTasks: $sendingTasks, errorMessage: $errorMessage)';
|
return 'SendSessionState(sessionId: $sessionId, remoteSessionId: $remoteSessionId, background: $background, status: $status, target: $target, files: $files, hashedFileCount: $hashedFileCount, startTime: $startTime, endTime: $endTime, sendingTasks: $sendingTasks, errorMessage: $errorMessage)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ class SendSessionStateMapper extends ClassMapperBase<SendSessionState> {
|
|||||||
static Map<String, SendingFile> _$files(SendSessionState v) => v.files;
|
static Map<String, SendingFile> _$files(SendSessionState v) => v.files;
|
||||||
static const Field<SendSessionState, Map<String, SendingFile>> _f$files =
|
static const Field<SendSessionState, Map<String, SendingFile>> _f$files =
|
||||||
Field('files', _$files);
|
Field('files', _$files);
|
||||||
|
static int _$hashedFileCount(SendSessionState v) => v.hashedFileCount;
|
||||||
|
static const Field<SendSessionState, int> _f$hashedFileCount = Field(
|
||||||
|
'hashedFileCount',
|
||||||
|
_$hashedFileCount,
|
||||||
|
);
|
||||||
static int? _$startTime(SendSessionState v) => v.startTime;
|
static int? _$startTime(SendSessionState v) => v.startTime;
|
||||||
static const Field<SendSessionState, int> _f$startTime = Field(
|
static const Field<SendSessionState, int> _f$startTime = Field(
|
||||||
'startTime',
|
'startTime',
|
||||||
@@ -80,6 +85,7 @@ class SendSessionStateMapper extends ClassMapperBase<SendSessionState> {
|
|||||||
#status: _f$status,
|
#status: _f$status,
|
||||||
#target: _f$target,
|
#target: _f$target,
|
||||||
#files: _f$files,
|
#files: _f$files,
|
||||||
|
#hashedFileCount: _f$hashedFileCount,
|
||||||
#startTime: _f$startTime,
|
#startTime: _f$startTime,
|
||||||
#endTime: _f$endTime,
|
#endTime: _f$endTime,
|
||||||
#sendingTasks: _f$sendingTasks,
|
#sendingTasks: _f$sendingTasks,
|
||||||
@@ -94,6 +100,7 @@ class SendSessionStateMapper extends ClassMapperBase<SendSessionState> {
|
|||||||
status: data.dec(_f$status),
|
status: data.dec(_f$status),
|
||||||
target: data.dec(_f$target),
|
target: data.dec(_f$target),
|
||||||
files: data.dec(_f$files),
|
files: data.dec(_f$files),
|
||||||
|
hashedFileCount: data.dec(_f$hashedFileCount),
|
||||||
startTime: data.dec(_f$startTime),
|
startTime: data.dec(_f$startTime),
|
||||||
endTime: data.dec(_f$endTime),
|
endTime: data.dec(_f$endTime),
|
||||||
sendingTasks: data.dec(_f$sendingTasks),
|
sendingTasks: data.dec(_f$sendingTasks),
|
||||||
@@ -180,6 +187,7 @@ abstract class SendSessionStateCopyWith<$R, $In extends SendSessionState, $Out>
|
|||||||
SessionStatus? status,
|
SessionStatus? status,
|
||||||
Device? target,
|
Device? target,
|
||||||
Map<String, SendingFile>? files,
|
Map<String, SendingFile>? files,
|
||||||
|
int? hashedFileCount,
|
||||||
int? startTime,
|
int? startTime,
|
||||||
int? endTime,
|
int? endTime,
|
||||||
List<SendingTask>? sendingTasks,
|
List<SendingTask>? sendingTasks,
|
||||||
@@ -230,6 +238,7 @@ class _SendSessionStateCopyWithImpl<$R, $Out>
|
|||||||
SessionStatus? status,
|
SessionStatus? status,
|
||||||
Device? target,
|
Device? target,
|
||||||
Map<String, SendingFile>? files,
|
Map<String, SendingFile>? files,
|
||||||
|
int? hashedFileCount,
|
||||||
Object? startTime = $none,
|
Object? startTime = $none,
|
||||||
Object? endTime = $none,
|
Object? endTime = $none,
|
||||||
Object? sendingTasks = $none,
|
Object? sendingTasks = $none,
|
||||||
@@ -242,6 +251,7 @@ class _SendSessionStateCopyWithImpl<$R, $Out>
|
|||||||
if (status != null) #status: status,
|
if (status != null) #status: status,
|
||||||
if (target != null) #target: target,
|
if (target != null) #target: target,
|
||||||
if (files != null) #files: files,
|
if (files != null) #files: files,
|
||||||
|
if (hashedFileCount != null) #hashedFileCount: hashedFileCount,
|
||||||
if (startTime != $none) #startTime: startTime,
|
if (startTime != $none) #startTime: startTime,
|
||||||
if (endTime != $none) #endTime: endTime,
|
if (endTime != $none) #endTime: endTime,
|
||||||
if (sendingTasks != $none) #sendingTasks: sendingTasks,
|
if (sendingTasks != $none) #sendingTasks: sendingTasks,
|
||||||
@@ -256,6 +266,7 @@ class _SendSessionStateCopyWithImpl<$R, $Out>
|
|||||||
status: data.get(#status, or: $value.status),
|
status: data.get(#status, or: $value.status),
|
||||||
target: data.get(#target, or: $value.target),
|
target: data.get(#target, or: $value.target),
|
||||||
files: data.get(#files, or: $value.files),
|
files: data.get(#files, or: $value.files),
|
||||||
|
hashedFileCount: data.get(#hashedFileCount, or: $value.hashedFileCount),
|
||||||
startTime: data.get(#startTime, or: $value.startTime),
|
startTime: data.get(#startTime, or: $value.startTime),
|
||||||
endTime: data.get(#endTime, or: $value.endTime),
|
endTime: data.get(#endTime, or: $value.endTime),
|
||||||
sendingTasks: data.get(#sendingTasks, or: $value.sendingTasks),
|
sendingTasks: data.get(#sendingTasks, or: $value.sendingTasks),
|
||||||
|
|||||||
@@ -134,7 +134,23 @@ class _SendPageState extends State<SendPage> with Refena {
|
|||||||
switch (sendState.status) {
|
switch (sendState.status) {
|
||||||
SessionStatus.waiting => Padding(
|
SessionStatus.waiting => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 20),
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
child: Text(t.sendPage.waiting, textAlign: TextAlign.center),
|
child: sendState.hashedFileCount < sendState.files.length
|
||||||
|
? Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
t.sendPage.calculatingChecksum(curr: sendState.hashedFileCount, n: sendState.files.length),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: sendState.hashedFileCount / sendState.files.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Text(t.sendPage.waiting, textAlign: TextAlign.center),
|
||||||
),
|
),
|
||||||
SessionStatus.declined => Padding(
|
SessionStatus.declined => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 20),
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
|
|||||||
@@ -529,6 +529,7 @@ class _MultiSendDeviceListTile extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ref = context.ref;
|
final ref = context.ref;
|
||||||
final session = ref.watch(sendProvider).values.firstWhereOrNull((s) => s.target.ip == device.ip);
|
final session = ref.watch(sendProvider).values.firstWhereOrNull((s) => s.target.ip == device.ip);
|
||||||
|
final String? info;
|
||||||
final double? progress;
|
final double? progress;
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
final files = session.files.values.where((f) => f.token != null);
|
final files = session.files.values.where((f) => f.token != null);
|
||||||
@@ -539,12 +540,16 @@ class _MultiSendDeviceListTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
final totalBytes = files.fold<int>(0, (prev, curr) => prev + curr.file.size);
|
final totalBytes = files.fold<int>(0, (prev, curr) => prev + curr.file.size);
|
||||||
progress = totalBytes == 0 ? 0 : currBytes / totalBytes;
|
progress = totalBytes == 0 ? 0 : currBytes / totalBytes;
|
||||||
|
info = session.hashedFileCount < session.files.length
|
||||||
|
? t.sendPage.calculatingChecksum(curr: session.hashedFileCount, n: session.files.length)
|
||||||
|
: session.status.humanString;
|
||||||
} else {
|
} else {
|
||||||
progress = null;
|
progress = null;
|
||||||
|
info = null;
|
||||||
}
|
}
|
||||||
return DeviceListTile(
|
return DeviceListTile(
|
||||||
device: device,
|
device: device,
|
||||||
info: session?.status.humanString,
|
info: info,
|
||||||
progress: progress,
|
progress: progress,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
nameOverride: nameOverride,
|
nameOverride: nameOverride,
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ import 'package:localsend_isolates/model/dto/file_dto.dart';
|
|||||||
import 'package:localsend_isolates/model/file_status.dart';
|
import 'package:localsend_isolates/model/file_status.dart';
|
||||||
import 'package:localsend_isolates/model/file_type.dart';
|
import 'package:localsend_isolates/model/file_type.dart';
|
||||||
import 'package:localsend_isolates/model/session_status.dart';
|
import 'package:localsend_isolates/model/session_status.dart';
|
||||||
|
import 'package:localsend_isolates/rust/api/cancel.dart' as rust_cancel;
|
||||||
import 'package:localsend_isolates/rust/api/http.dart' as rust_http;
|
import 'package:localsend_isolates/rust/api/http.dart' as rust_http;
|
||||||
import 'package:localsend_isolates/rust/api/model.dart' as rust_model;
|
import 'package:localsend_isolates/rust/api/model.dart' as rust_model;
|
||||||
|
import 'package:localsend_isolates/util/file_hash.dart';
|
||||||
import 'package:localsend_isolates/util/rust.dart';
|
import 'package:localsend_isolates/util/rust.dart';
|
||||||
import 'package:localsend_isolates/util/sleep.dart';
|
import 'package:localsend_isolates/util/sleep.dart';
|
||||||
import 'package:localsend_isolates/util/transfer_notification.dart';
|
import 'package:localsend_isolates/util/transfer_notification.dart';
|
||||||
@@ -45,6 +47,10 @@ final sendProvider = NotifierProvider<SendNotifier, Map<String, SendSessionState
|
|||||||
class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||||
SendNotifier();
|
SendNotifier();
|
||||||
|
|
||||||
|
/// Cancel tokens of the running checksum calculations.
|
||||||
|
/// Session ID -> Cancel token
|
||||||
|
final _hashCancelTokens = <String, rust_cancel.RsCancellationToken>{};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, SendSessionState> init() {
|
Map<String, SendSessionState> init() {
|
||||||
return {};
|
return {};
|
||||||
@@ -61,51 +67,109 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
|||||||
final client = ref.read(httpProvider).v2;
|
final client = ref.read(httpProvider).v2;
|
||||||
final sessionId = _uuid.v4();
|
final sessionId = _uuid.v4();
|
||||||
|
|
||||||
final requestState = SendSessionState(
|
// The ids are assigned upfront, so the checksums calculated below
|
||||||
|
// can be mapped back to the corresponding file.
|
||||||
|
final selectedFiles = files.map((file) => (id: _uuid.v4(), file: file)).toList();
|
||||||
|
|
||||||
|
state = state.updateSession(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
remoteSessionId: null,
|
state: (_) => SendSessionState(
|
||||||
background: background,
|
sessionId: sessionId,
|
||||||
status: SessionStatus.waiting,
|
remoteSessionId: null,
|
||||||
target: target,
|
background: background,
|
||||||
files: Map.fromEntries(
|
status: SessionStatus.waiting,
|
||||||
await Future.wait(
|
target: target,
|
||||||
files.map((file) async {
|
files: {
|
||||||
final id = _uuid.v4();
|
for (final (:id, :file) in selectedFiles)
|
||||||
return MapEntry(
|
id: SendingFile(
|
||||||
id,
|
file: FileDto(
|
||||||
SendingFile(
|
id: id,
|
||||||
file: FileDto(
|
fileName: file.name,
|
||||||
id: id,
|
size: file.size,
|
||||||
fileName: file.name,
|
fileType: file.fileType,
|
||||||
size: file.size,
|
hash: null,
|
||||||
fileType: file.fileType,
|
// calculated below
|
||||||
hash: null,
|
preview: files.length == 1 && files.first.fileType == FileType.text && files.first.bytes != null
|
||||||
preview: files.length == 1 && files.first.fileType == FileType.text && files.first.bytes != null
|
? utf8.decode(files.first.bytes!) // send simple message by embedding it into the preview
|
||||||
? utf8.decode(files.first.bytes!) // send simple message by embedding it into the preview
|
: null,
|
||||||
: null,
|
metadata: file.lastModified != null || file.lastAccessed != null
|
||||||
metadata: file.lastModified != null || file.lastAccessed != null
|
? FileMetadata(
|
||||||
? FileMetadata(
|
lastModified: file.lastModified,
|
||||||
lastModified: file.lastModified,
|
lastAccessed: file.lastAccessed,
|
||||||
lastAccessed: file.lastAccessed,
|
)
|
||||||
)
|
: null,
|
||||||
: null,
|
|
||||||
),
|
|
||||||
status: FileStatus.queue,
|
|
||||||
token: null,
|
|
||||||
thumbnail: file.thumbnail,
|
|
||||||
asset: file.asset,
|
|
||||||
path: file.path,
|
|
||||||
bytes: file.bytes,
|
|
||||||
errorMessage: null,
|
|
||||||
),
|
),
|
||||||
);
|
status: FileStatus.queue,
|
||||||
}),
|
token: null,
|
||||||
),
|
thumbnail: file.thumbnail,
|
||||||
|
asset: file.asset,
|
||||||
|
path: file.path,
|
||||||
|
bytes: file.bytes,
|
||||||
|
errorMessage: null,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
hashedFileCount: 0,
|
||||||
|
startTime: null,
|
||||||
|
endTime: null,
|
||||||
|
sendingTasks: [],
|
||||||
|
errorMessage: null,
|
||||||
),
|
),
|
||||||
startTime: null,
|
);
|
||||||
endTime: null,
|
|
||||||
sendingTasks: [],
|
if (!background) {
|
||||||
errorMessage: null,
|
// ignore: use_build_context_synchronously, unawaited_futures
|
||||||
|
Routerino.context.push(
|
||||||
|
() => SendPage(showAppBar: false, closeSessionOnClose: true, sessionId: sessionId),
|
||||||
|
transition: RouterinoTransition.fade(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the checksums which are part of the request.
|
||||||
|
// The files are read and hashed in Rust, one file after another.
|
||||||
|
final hashCancelToken = rust_cancel.createCancellationToken();
|
||||||
|
_hashCancelTokens[sessionId] = hashCancelToken;
|
||||||
|
final hashes = <String, String>{};
|
||||||
|
try {
|
||||||
|
for (final (:id, :file) in selectedFiles) {
|
||||||
|
try {
|
||||||
|
hashes[id] = await calculateFileHash(path: file.path, bytes: file.bytes, cancelToken: hashCancelToken);
|
||||||
|
} catch (e) {
|
||||||
|
if (state[sessionId] != null) {
|
||||||
|
// Sending the checksum is optional, so a file that cannot be read
|
||||||
|
// here still gets a chance to be sent.
|
||||||
|
// Errors caused by the cancellation are not logged.
|
||||||
|
_logger.warning('Could not calculate the checksum of ${file.name}', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state[sessionId] == null) {
|
||||||
|
// session has been canceled while calculating the checksums
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.updateSession(
|
||||||
|
sessionId: sessionId,
|
||||||
|
state: (s) => s?.copyWith(hashedFileCount: s.hashedFileCount + 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_hashCancelTokens.remove(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
final hashedState = state[sessionId];
|
||||||
|
if (hashedState == null) {
|
||||||
|
// session has been canceled while calculating the checksums
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final requestState = hashedState.copyWith(
|
||||||
|
files: hashedState.files.map(
|
||||||
|
(id, sendingFile) => MapEntry(id, sendingFile.copyWith(file: sendingFile.file.withHash(hashes[id]))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
state = state.updateSession(
|
||||||
|
sessionId: sessionId,
|
||||||
|
state: (_) => requestState,
|
||||||
);
|
);
|
||||||
|
|
||||||
final originDevice = ref.read(deviceFullInfoProvider);
|
final originDevice = ref.read(deviceFullInfoProvider);
|
||||||
@@ -125,19 +189,6 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
state = state.updateSession(
|
|
||||||
sessionId: sessionId,
|
|
||||||
state: (_) => requestState,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!background) {
|
|
||||||
// ignore: use_build_context_synchronously, unawaited_futures
|
|
||||||
Routerino.context.push(
|
|
||||||
() => SendPage(showAppBar: false, closeSessionOnClose: true, sessionId: sessionId),
|
|
||||||
transition: RouterinoTransition.fade(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
rust_http.PrepareUploadResult? response;
|
rust_http.PrepareUploadResult? response;
|
||||||
bool invalidPin;
|
bool invalidPin;
|
||||||
bool pinFirstAttempt = true;
|
bool pinFirstAttempt = true;
|
||||||
@@ -617,6 +668,8 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _cancelRunningRequests(SendSessionState state) {
|
void _cancelRunningRequests(SendSessionState state) {
|
||||||
|
_hashCancelTokens.remove(state.sessionId)?.cancel();
|
||||||
|
|
||||||
for (final task in state.sendingTasks ?? <SendingTask>[]) {
|
for (final task in state.sendingTasks ?? <SendingTask>[]) {
|
||||||
ref
|
ref
|
||||||
.redux(parentIsolateProvider)
|
.redux(parentIsolateProvider)
|
||||||
@@ -635,6 +688,7 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
TransferNotification.stop(sessionId);
|
TransferNotification.stop(sessionId);
|
||||||
|
_hashCancelTokens.remove(sessionId)?.cancel();
|
||||||
state = state.removeSession(ref, sessionId);
|
state = state.removeSession(ref, sessionId);
|
||||||
if (sessionState.status == SessionStatus.finished && ref.read(settingsProvider).sendMode == SendMode.single) {
|
if (sessionState.status == SessionStatus.finished && ref.read(settingsProvider).sendMode == SendMode.single) {
|
||||||
// clear selected files
|
// clear selected files
|
||||||
@@ -646,6 +700,10 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
|||||||
for (final sessionId in state.keys) {
|
for (final sessionId in state.keys) {
|
||||||
TransferNotification.stop(sessionId);
|
TransferNotification.stop(sessionId);
|
||||||
}
|
}
|
||||||
|
for (final cancelToken in _hashCancelTokens.values) {
|
||||||
|
cancelToken.cancel();
|
||||||
|
}
|
||||||
|
_hashCancelTokens.clear();
|
||||||
state = {};
|
state = {};
|
||||||
ref.notifier(progressProvider).removeAllSessions();
|
ref.notifier(progressProvider).removeAllSessions();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ x509-parser = { version = "0.18.0", features = ["verify"], optional = true }
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
crypto = ["ed25519-dalek", "rsa", "sha2"]
|
crypto = ["ed25519-dalek", "rsa", "sha2", "tokio-util"]
|
||||||
http = ["crypto", "form_urlencoded", "http-body-util", "hyper", "hyper-util", "pem", "percent-encoding", "reqwest", "rustls", "socket2", "tokio-rustls", "tokio-util", "x509-parser"]
|
http = ["crypto", "form_urlencoded", "http-body-util", "hyper", "hyper-util", "pem", "percent-encoding", "reqwest", "rustls", "socket2", "tokio-rustls", "tokio-util", "x509-parser"]
|
||||||
webrtc-signaling = ["tokio-tungstenite"]
|
webrtc-signaling = ["tokio-tungstenite"]
|
||||||
webrtc = ["crypto", "flate2", "dep:webrtc", "webrtc-signaling", "x509-parser"]
|
webrtc = ["crypto", "flate2", "dep:webrtc", "webrtc-signaling", "x509-parser"]
|
||||||
|
|||||||
@@ -1,7 +1,92 @@
|
|||||||
|
use crate::model::transfer::FileContent;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
/// Buffer size used when hashing a file chunk by chunk.
|
||||||
|
const HASH_BUFFER_SIZE: usize = 64 * 1024;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum HashError {
|
||||||
|
#[error("Failed to read the file: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("Hashing has been cancelled")]
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sha256(data: &[u8]) -> Vec<u8> {
|
pub fn sha256(data: &[u8]) -> Vec<u8> {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(data);
|
hasher.update(data);
|
||||||
hasher.finalize().to_vec()
|
hasher.finalize().to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Computes the SHA-256 checksum of `data`, encoded as lowercase hex.
|
||||||
|
pub fn sha256_hex(data: &[u8]) -> String {
|
||||||
|
to_hex(&sha256(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the SHA-256 checksum of a file's content, encoded as lowercase hex.
|
||||||
|
pub async fn sha256_file_content(
|
||||||
|
content: FileContent,
|
||||||
|
cancel_token: &CancellationToken,
|
||||||
|
) -> Result<String, HashError> {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
match content {
|
||||||
|
FileContent::Stream(mut receiver) => loop {
|
||||||
|
let chunk = tokio::select! {
|
||||||
|
biased;
|
||||||
|
_ = cancel_token.cancelled() => return Err(HashError::Cancelled),
|
||||||
|
chunk = receiver.recv() => chunk,
|
||||||
|
};
|
||||||
|
match chunk {
|
||||||
|
Some(chunk) => hasher.update(&chunk),
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FileContent::Path(path) => {
|
||||||
|
tracing::info!("Hashing file content from path: {}", path.display());
|
||||||
|
let file = tokio::fs::File::open(&path).await?;
|
||||||
|
read_and_hash_from_file(&mut hasher, file, cancel_token).await?;
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
FileContent::Fd(fd) => {
|
||||||
|
use std::os::fd::FromRawFd;
|
||||||
|
|
||||||
|
tracing::info!("Hashing file content from file descriptor: {fd}");
|
||||||
|
// SAFETY: the descriptor is owned by this call; wrapping it in a File
|
||||||
|
// transfers that ownership so it is closed once hashing finishes.
|
||||||
|
let std_file = unsafe { std::fs::File::from_raw_fd(fd) };
|
||||||
|
let file = tokio::fs::File::from_std(std_file);
|
||||||
|
read_and_hash_from_file(&mut hasher, file, cancel_token).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(to_hex(&hasher.finalize()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads `file` to EOF, feeding every chunk into `hasher`.
|
||||||
|
async fn read_and_hash_from_file(
|
||||||
|
hasher: &mut Sha256,
|
||||||
|
mut file: tokio::fs::File,
|
||||||
|
cancel_token: &CancellationToken,
|
||||||
|
) -> Result<(), HashError> {
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
let mut buffer = vec![0u8; HASH_BUFFER_SIZE];
|
||||||
|
loop {
|
||||||
|
let read = tokio::select! {
|
||||||
|
biased;
|
||||||
|
_ = cancel_token.cancelled() => return Err(HashError::Cancelled),
|
||||||
|
read = file.read(&mut buffer) => read?,
|
||||||
|
};
|
||||||
|
if read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
hasher.update(&buffer[..read]);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_hex(bytes: &[u8]) -> String {
|
||||||
|
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#![cfg(feature = "crypto")]
|
||||||
|
|
||||||
|
use localsend::crypto::hash::{sha256_file_content, sha256_hex, HashError};
|
||||||
|
use localsend::model::transfer::FileContent;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
/// SHA-256 of "hello world".
|
||||||
|
const HELLO_WORLD_HASH: &str = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn hash_file_from_path() {
|
||||||
|
let path = std::env::temp_dir().join(format!("localsend-hash-{}", uuid::Uuid::new_v4()));
|
||||||
|
tokio::fs::write(&path, b"hello world").await.unwrap();
|
||||||
|
|
||||||
|
let hash = sha256_file_content(FileContent::Path(path.clone()), &CancellationToken::new())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(hash, HELLO_WORLD_HASH);
|
||||||
|
tokio::fs::remove_file(&path).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A file larger than the internal buffer must be hashed across multiple reads.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn hash_large_file_from_path() {
|
||||||
|
let content: Vec<u8> = (0..500_000).map(|i| (i % 251) as u8).collect();
|
||||||
|
let path = std::env::temp_dir().join(format!("localsend-hash-{}", uuid::Uuid::new_v4()));
|
||||||
|
tokio::fs::write(&path, &content).await.unwrap();
|
||||||
|
|
||||||
|
let hash = sha256_file_content(FileContent::Path(path.clone()), &CancellationToken::new())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(hash, sha256_hex(&content));
|
||||||
|
tokio::fs::remove_file(&path).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hashing must stop when the token is cancelled while the file is being read.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn hash_cancelled_while_reading() {
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::channel(1);
|
||||||
|
let cancel_token = CancellationToken::new();
|
||||||
|
|
||||||
|
let handle = tokio::spawn({
|
||||||
|
let token = cancel_token.clone();
|
||||||
|
async move { sha256_file_content(FileContent::Stream(rx), &token).await }
|
||||||
|
});
|
||||||
|
|
||||||
|
// The sender stays alive, so hashing only ends because of the cancellation.
|
||||||
|
tx.send(bytes::Bytes::from_static(b"hello ")).await.unwrap();
|
||||||
|
cancel_token.cancel();
|
||||||
|
|
||||||
|
let result = handle.await.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(result, Err(HashError::Cancelled)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A token that is already cancelled must not start reading at all.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn hash_cancelled_before_start() {
|
||||||
|
let path = std::env::temp_dir().join(format!("localsend-hash-{}", uuid::Uuid::new_v4()));
|
||||||
|
tokio::fs::write(&path, b"hello world").await.unwrap();
|
||||||
|
let cancel_token = CancellationToken::new();
|
||||||
|
cancel_token.cancel();
|
||||||
|
|
||||||
|
let result = sha256_file_content(FileContent::Path(path.clone()), &cancel_token).await;
|
||||||
|
|
||||||
|
assert!(matches!(result, Err(HashError::Cancelled)));
|
||||||
|
tokio::fs::remove_file(&path).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn hash_missing_file_fails() {
|
||||||
|
let path = std::env::temp_dir().join(format!("localsend-hash-{}", uuid::Uuid::new_v4()));
|
||||||
|
|
||||||
|
let result = sha256_file_content(FileContent::Path(path), &CancellationToken::new()).await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn hash_stream() {
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::channel(4);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tx.send(bytes::Bytes::from_static(b"hello ")).await.unwrap();
|
||||||
|
tx.send(bytes::Bytes::from_static(b"world")).await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let hash = sha256_file_content(FileContent::Stream(rx), &CancellationToken::new())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(hash, HELLO_WORLD_HASH);
|
||||||
|
}
|
||||||
@@ -41,6 +41,19 @@ class FileDto {
|
|||||||
|
|
||||||
String lookupMime() => lookupMimeType(fileName) ?? 'application/octet-stream';
|
String lookupMime() => lookupMimeType(fileName) ?? 'application/octet-stream';
|
||||||
|
|
||||||
|
/// Returns a copy of this DTO with the given [hash].
|
||||||
|
FileDto withHash(String? hash) {
|
||||||
|
return FileDto(
|
||||||
|
id: id,
|
||||||
|
fileName: fileName,
|
||||||
|
size: size,
|
||||||
|
fileType: fileType,
|
||||||
|
hash: hash,
|
||||||
|
preview: preview,
|
||||||
|
metadata: metadata,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 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_isolates/rust/frb_generated.dart';
|
||||||
|
|
||||||
|
RsCancellationToken createCancellationToken() => RustLib.instance.api.crateApiCancelCreateCancellationToken();
|
||||||
|
|
||||||
|
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsCancellationToken>>
|
||||||
|
abstract class RsCancellationToken implements RustOpaqueInterface {
|
||||||
|
void cancel();
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
// 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:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
import 'package:localsend_isolates/rust/api/cancel.dart';
|
||||||
import 'package:localsend_isolates/rust/frb_generated.dart';
|
import 'package:localsend_isolates/rust/frb_generated.dart';
|
||||||
|
|
||||||
Future<void> verifyCert({required String cert, required String publicKey}) =>
|
Future<void> verifyCert({required String cert, required String publicKey}) =>
|
||||||
@@ -11,6 +12,18 @@ Future<void> verifyCert({required String cert, required String publicKey}) =>
|
|||||||
|
|
||||||
Future<KeyPair> generateKeyPair() => RustLib.instance.api.crateApiCryptoGenerateKeyPair();
|
Future<KeyPair> generateKeyPair() => RustLib.instance.api.crateApiCryptoGenerateKeyPair();
|
||||||
|
|
||||||
|
/// Computes the SHA-256 checksum of a file, encoded as lowercase hex.
|
||||||
|
///
|
||||||
|
/// The file is read chunk by chunk, so it is never fully loaded into memory.
|
||||||
|
/// Cancelling [cancel_token] aborts the read, so hashing a large file does not
|
||||||
|
/// have to be waited out.
|
||||||
|
///
|
||||||
|
/// Exactly one content source must be provided:
|
||||||
|
/// a [path] to a regular file, a [file_descriptor] (Android only), or [bytes]
|
||||||
|
/// for a file that only lives in memory.
|
||||||
|
Future<String> hashFile({String? path, int? fileDescriptor, Uint8List? bytes, required RsCancellationToken cancelToken}) =>
|
||||||
|
RustLib.instance.api.crateApiCryptoHashFile(path: path, fileDescriptor: fileDescriptor, bytes: bytes, cancelToken: cancelToken);
|
||||||
|
|
||||||
class KeyPair {
|
class KeyPair {
|
||||||
final String privateKey;
|
final String privateKey;
|
||||||
final String publicKey;
|
final String publicKey;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
|
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
|
||||||
|
import 'package:localsend_isolates/rust/api/cancel.dart';
|
||||||
import 'package:localsend_isolates/rust/api/model.dart';
|
import 'package:localsend_isolates/rust/api/model.dart';
|
||||||
import 'package:localsend_isolates/rust/api/stream.dart';
|
import 'package:localsend_isolates/rust/api/stream.dart';
|
||||||
import 'package:localsend_isolates/rust/frb_generated.dart';
|
import 'package:localsend_isolates/rust/frb_generated.dart';
|
||||||
@@ -17,13 +18,6 @@ part 'http.freezed.dart';
|
|||||||
RsHttpClient createClient({required String privateKey, required String cert, required LsHttpClientVersion version, int? 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);
|
RustLib.instance.api.crateApiHttpCreateClient(privateKey: privateKey, cert: cert, version: version, timeoutMs: timeoutMs);
|
||||||
|
|
||||||
RsCancellationToken createCancellationToken() => RustLib.instance.api.crateApiHttpCreateCancellationToken();
|
|
||||||
|
|
||||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsCancellationToken>>
|
|
||||||
abstract class RsCancellationToken implements RustOpaqueInterface {
|
|
||||||
void cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsHttpClient>>
|
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsHttpClient>>
|
||||||
abstract class RsHttpClient implements RustOpaqueInterface {
|
abstract class RsHttpClient implements RustOpaqueInterface {
|
||||||
Future<void> cancel({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});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
import 'package:localsend_isolates/rust/api/cancel.dart';
|
||||||
import 'package:localsend_isolates/rust/api/crypto.dart';
|
import 'package:localsend_isolates/rust/api/crypto.dart';
|
||||||
import 'package:localsend_isolates/rust/api/http.dart';
|
import 'package:localsend_isolates/rust/api/http.dart';
|
||||||
import 'package:localsend_isolates/rust/api/logging.dart';
|
import 'package:localsend_isolates/rust/api/logging.dart';
|
||||||
@@ -72,7 +73,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
|||||||
String get codegenVersion => '2.12.0';
|
String get codegenVersion => '2.12.0';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get rustContentHash => 1916764705;
|
int get rustContentHash => 1193619754;
|
||||||
|
|
||||||
static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig(
|
static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig(
|
||||||
stem: 'rust_lib_localsend_app',
|
stem: 'rust_lib_localsend_app',
|
||||||
@@ -108,7 +109,7 @@ abstract class RustLibApi extends BaseApi {
|
|||||||
|
|
||||||
Future<void> crateApiWebrtcLsSignalingConnectionUpdateInfo({required LsSignalingConnection that, required ClientInfoWithoutId info});
|
Future<void> crateApiWebrtcLsSignalingConnectionUpdateInfo({required LsSignalingConnection that, required ClientInfoWithoutId info});
|
||||||
|
|
||||||
void crateApiHttpRsCancellationTokenCancel({required RsCancellationToken that});
|
void crateApiCancelRsCancellationTokenCancel({required RsCancellationToken that});
|
||||||
|
|
||||||
Future<void> crateApiHttpRsHttpClientCancel({
|
Future<void> crateApiHttpRsHttpClientCancel({
|
||||||
required RsHttpClient that,
|
required RsHttpClient that,
|
||||||
@@ -222,7 +223,7 @@ abstract class RustLibApi extends BaseApi {
|
|||||||
required FutureOr<void> Function(LsSignalingConnection) onConnection,
|
required FutureOr<void> Function(LsSignalingConnection) onConnection,
|
||||||
});
|
});
|
||||||
|
|
||||||
RsCancellationToken crateApiHttpCreateCancellationToken();
|
RsCancellationToken crateApiCancelCreateCancellationToken();
|
||||||
|
|
||||||
RsHttpClient crateApiHttpCreateClient({required String privateKey, required String cert, required LsHttpClientVersion version, int? timeoutMs});
|
RsHttpClient crateApiHttpCreateClient({required String privateKey, required String cert, required LsHttpClientVersion version, int? timeoutMs});
|
||||||
|
|
||||||
@@ -232,6 +233,8 @@ abstract class RustLibApi extends BaseApi {
|
|||||||
|
|
||||||
Future<KeyPair> crateApiCryptoGenerateKeyPair();
|
Future<KeyPair> crateApiCryptoGenerateKeyPair();
|
||||||
|
|
||||||
|
Future<String> crateApiCryptoHashFile({String? path, int? fileDescriptor, Uint8List? bytes, required RsCancellationToken cancelToken});
|
||||||
|
|
||||||
Future<RsHttpServer> crateApiServerStartServer({
|
Future<RsHttpServer> crateApiServerStartServer({
|
||||||
required int port,
|
required int port,
|
||||||
TlsConfig? tls,
|
TlsConfig? tls,
|
||||||
@@ -470,7 +473,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void crateApiHttpRsCancellationTokenCancel({required RsCancellationToken that}) {
|
void crateApiCancelRsCancellationTokenCancel({required RsCancellationToken that}) {
|
||||||
return handler.executeSync(
|
return handler.executeSync(
|
||||||
SyncTask(
|
SyncTask(
|
||||||
callFfi: () {
|
callFfi: () {
|
||||||
@@ -482,14 +485,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
decodeSuccessData: sse_decode_unit,
|
decodeSuccessData: sse_decode_unit,
|
||||||
decodeErrorData: null,
|
decodeErrorData: null,
|
||||||
),
|
),
|
||||||
constMeta: kCrateApiHttpRsCancellationTokenCancelConstMeta,
|
constMeta: kCrateApiCancelRsCancellationTokenCancelConstMeta,
|
||||||
argValues: [that],
|
argValues: [that],
|
||||||
apiImpl: this,
|
apiImpl: this,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskConstMeta get kCrateApiHttpRsCancellationTokenCancelConstMeta => const TaskConstMeta(
|
TaskConstMeta get kCrateApiCancelRsCancellationTokenCancelConstMeta => const TaskConstMeta(
|
||||||
debugName: 'RsCancellationToken_cancel',
|
debugName: 'RsCancellationToken_cancel',
|
||||||
argNames: ['that'],
|
argNames: ['that'],
|
||||||
);
|
);
|
||||||
@@ -1420,7 +1423,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RsCancellationToken crateApiHttpCreateCancellationToken() {
|
RsCancellationToken crateApiCancelCreateCancellationToken() {
|
||||||
return handler.executeSync(
|
return handler.executeSync(
|
||||||
SyncTask(
|
SyncTask(
|
||||||
callFfi: () {
|
callFfi: () {
|
||||||
@@ -1431,14 +1434,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken,
|
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken,
|
||||||
decodeErrorData: null,
|
decodeErrorData: null,
|
||||||
),
|
),
|
||||||
constMeta: kCrateApiHttpCreateCancellationTokenConstMeta,
|
constMeta: kCrateApiCancelCreateCancellationTokenConstMeta,
|
||||||
argValues: [],
|
argValues: [],
|
||||||
apiImpl: this,
|
apiImpl: this,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskConstMeta get kCrateApiHttpCreateCancellationTokenConstMeta => const TaskConstMeta(
|
TaskConstMeta get kCrateApiCancelCreateCancellationTokenConstMeta => const TaskConstMeta(
|
||||||
debugName: 'create_cancellation_token',
|
debugName: 'create_cancellation_token',
|
||||||
argNames: [],
|
argNames: [],
|
||||||
);
|
);
|
||||||
@@ -1544,6 +1547,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
argNames: [],
|
argNames: [],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> crateApiCryptoHashFile({String? path, int? fileDescriptor, Uint8List? bytes, required RsCancellationToken cancelToken}) {
|
||||||
|
return handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_opt_String(path, serializer);
|
||||||
|
sse_encode_opt_box_autoadd_i_32(fileDescriptor, serializer);
|
||||||
|
sse_encode_opt_list_prim_u_8_strict(bytes, serializer);
|
||||||
|
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(cancelToken, serializer);
|
||||||
|
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 42, port: port_);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_String,
|
||||||
|
decodeErrorData: sse_decode_AnyhowException,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiCryptoHashFileConstMeta,
|
||||||
|
argValues: [path, fileDescriptor, bytes, cancelToken],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiCryptoHashFileConstMeta => const TaskConstMeta(
|
||||||
|
debugName: 'hash_file',
|
||||||
|
argNames: ['path', 'fileDescriptor', 'bytes', 'cancelToken'],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<RsHttpServer> crateApiServerStartServer({
|
Future<RsHttpServer> crateApiServerStartServer({
|
||||||
required int port,
|
required int port,
|
||||||
@@ -1571,7 +1602,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
sse_encode_opt_String(pin, serializer);
|
sse_encode_opt_String(pin, serializer);
|
||||||
sse_encode_opt_box_autoadd_web_send_params(webSend, serializer);
|
sse_encode_opt_box_autoadd_web_send_params(webSend, serializer);
|
||||||
sse_encode_opt_String(showToken, serializer);
|
sse_encode_opt_String(showToken, serializer);
|
||||||
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 42, port: port_);
|
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 43, port: port_);
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer,
|
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer,
|
||||||
@@ -1597,7 +1628,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
sse_encode_String(cert, serializer);
|
sse_encode_String(cert, serializer);
|
||||||
sse_encode_String(publicKey, serializer);
|
sse_encode_String(publicKey, serializer);
|
||||||
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 43, port: port_);
|
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 44, port: port_);
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
decodeSuccessData: sse_decode_unit,
|
decodeSuccessData: sse_decode_unit,
|
||||||
@@ -2327,6 +2358,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
return raw == null ? null : dco_decode_list_String(raw);
|
return raw == null ? null : dco_decode_list_String(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return raw == null ? null : dco_decode_list_prim_u_8_strict(raw);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PinConfig dco_decode_pin_config(dynamic raw) {
|
PinConfig dco_decode_pin_config(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
@@ -3498,6 +3535,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
||||||
|
if (sse_decode_bool(deserializer)) {
|
||||||
|
return (sse_decode_list_prim_u_8_strict(deserializer));
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PinConfig sse_decode_pin_config(SseDeserializer deserializer) {
|
PinConfig sse_decode_pin_config(SseDeserializer deserializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
@@ -4697,6 +4745,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_list_prim_u_8_strict(Uint8List? self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
||||||
|
sse_encode_bool(self != null, serializer);
|
||||||
|
if (self != null) {
|
||||||
|
sse_encode_list_prim_u_8_strict(self, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_pin_config(PinConfig self, SseSerializer serializer) {
|
void sse_encode_pin_config(PinConfig self, SseSerializer serializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
@@ -5142,7 +5200,7 @@ class RsCancellationTokenImpl extends RustOpaque implements RsCancellationToken
|
|||||||
rustArcDecrementStrongCountPtr: RustLib.instance.api.rust_arc_decrement_strong_count_RsCancellationTokenPtr,
|
rustArcDecrementStrongCountPtr: RustLib.instance.api.rust_arc_decrement_strong_count_RsCancellationTokenPtr,
|
||||||
);
|
);
|
||||||
|
|
||||||
void cancel() => RustLib.instance.api.crateApiHttpRsCancellationTokenCancel(
|
void cancel() => RustLib.instance.api.crateApiCancelRsCancellationTokenCancel(
|
||||||
that: this,
|
that: this,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'dart:convert';
|
|||||||
import 'dart:ffi' as ffi;
|
import 'dart:ffi' as ffi;
|
||||||
|
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
||||||
|
import 'package:localsend_isolates/rust/api/cancel.dart';
|
||||||
import 'package:localsend_isolates/rust/api/crypto.dart';
|
import 'package:localsend_isolates/rust/api/crypto.dart';
|
||||||
import 'package:localsend_isolates/rust/api/http.dart';
|
import 'package:localsend_isolates/rust/api/http.dart';
|
||||||
import 'package:localsend_isolates/rust/api/logging.dart';
|
import 'package:localsend_isolates/rust/api/logging.dart';
|
||||||
@@ -349,6 +350,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
List<String>? dco_decode_opt_list_String(dynamic raw);
|
List<String>? dco_decode_opt_list_String(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PinConfig dco_decode_pin_config(dynamic raw);
|
PinConfig dco_decode_pin_config(dynamic raw);
|
||||||
|
|
||||||
@@ -758,6 +762,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
List<String>? sse_decode_opt_list_String(SseDeserializer deserializer);
|
List<String>? sse_decode_opt_list_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PinConfig sse_decode_pin_config(SseDeserializer deserializer);
|
PinConfig sse_decode_pin_config(SseDeserializer deserializer);
|
||||||
|
|
||||||
@@ -1212,6 +1219,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_list_String(List<String>? self, SseSerializer serializer);
|
void sse_encode_opt_list_String(List<String>? self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_list_prim_u_8_strict(Uint8List? self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_pin_config(PinConfig self, SseSerializer serializer);
|
void sse_encode_pin_config(PinConfig self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
|
||||||
|
import 'package:localsend_isolates/rust/api/cancel.dart';
|
||||||
import 'package:localsend_isolates/rust/api/crypto.dart';
|
import 'package:localsend_isolates/rust/api/crypto.dart';
|
||||||
import 'package:localsend_isolates/rust/api/http.dart';
|
import 'package:localsend_isolates/rust/api/http.dart';
|
||||||
import 'package:localsend_isolates/rust/api/logging.dart';
|
import 'package:localsend_isolates/rust/api/logging.dart';
|
||||||
@@ -351,6 +352,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
List<String>? dco_decode_opt_list_String(dynamic raw);
|
List<String>? dco_decode_opt_list_String(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PinConfig dco_decode_pin_config(dynamic raw);
|
PinConfig dco_decode_pin_config(dynamic raw);
|
||||||
|
|
||||||
@@ -760,6 +764,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
List<String>? sse_decode_opt_list_String(SseDeserializer deserializer);
|
List<String>? sse_decode_opt_list_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PinConfig sse_decode_pin_config(SseDeserializer deserializer);
|
PinConfig sse_decode_pin_config(SseDeserializer deserializer);
|
||||||
|
|
||||||
@@ -1214,6 +1221,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_list_String(List<String>? self, SseSerializer serializer);
|
void sse_encode_opt_list_String(List<String>? self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_list_prim_u_8_strict(Uint8List? self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_pin_config(PinConfig self, SseSerializer serializer);
|
void sse_encode_pin_config(PinConfig self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:localsend_isolates/isolate.dart';
|
import 'package:localsend_isolates/isolate.dart';
|
||||||
import 'package:localsend_isolates/model/device.dart';
|
import 'package:localsend_isolates/model/device.dart';
|
||||||
import 'package:localsend_isolates/rust/api/http.dart';
|
import 'package:localsend_isolates/rust/api/cancel.dart';
|
||||||
import 'package:localsend_isolates/src/isolate/child/main.dart';
|
import 'package:localsend_isolates/src/isolate/child/main.dart';
|
||||||
import 'package:localsend_isolates/src/isolate/dto/send_to_isolate_data.dart';
|
import 'package:localsend_isolates/src/isolate/dto/send_to_isolate_data.dart';
|
||||||
import 'package:localsend_isolates/src/task/upload/http_upload.dart';
|
import 'package:localsend_isolates/src/task/upload/http_upload.dart';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:localsend_isolates/model/device.dart';
|
import 'package:localsend_isolates/model/device.dart';
|
||||||
|
import 'package:localsend_isolates/rust/api/cancel.dart';
|
||||||
import 'package:localsend_isolates/rust/api/http.dart';
|
import 'package:localsend_isolates/rust/api/http.dart';
|
||||||
import 'package:localsend_isolates/rust/api/stream.dart';
|
import 'package:localsend_isolates/rust/api/stream.dart';
|
||||||
import 'package:localsend_isolates/src/isolate/child/http_provider.dart';
|
import 'package:localsend_isolates/src/isolate/child/http_provider.dart';
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:localsend_isolates/rust/api/cancel.dart';
|
||||||
|
import 'package:localsend_isolates/rust/api/crypto.dart' as rust_crypto;
|
||||||
|
import 'package:localsend_isolates/util/android_channel.dart';
|
||||||
|
|
||||||
|
/// Calculates the SHA-256 checksum (lowercase hex) of a single file.
|
||||||
|
///
|
||||||
|
/// The file is read and hashed in Rust, so this only occupies the Dart isolate
|
||||||
|
/// while waiting for the result.
|
||||||
|
/// Cancelling [cancelToken] aborts the calculation and throws.
|
||||||
|
Future<String> calculateFileHash({
|
||||||
|
required String? path,
|
||||||
|
required List<int>? bytes,
|
||||||
|
required RsCancellationToken cancelToken,
|
||||||
|
}) async {
|
||||||
|
if (path != null) {
|
||||||
|
if (path.startsWith('content://')) {
|
||||||
|
// Android SAF files can only be read through a file descriptor.
|
||||||
|
// The descriptor is closed by Rust after the file has been read.
|
||||||
|
final fileDescriptor = await getFileDescriptorAndroid(uri: path);
|
||||||
|
return await rust_crypto.hashFile(fileDescriptor: fileDescriptor, cancelToken: cancelToken);
|
||||||
|
}
|
||||||
|
return await rust_crypto.hashFile(path: path, cancelToken: cancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes != null) {
|
||||||
|
return await rust_crypto.hashFile(
|
||||||
|
bytes: bytes is Uint8List ? bytes : Uint8List.fromList(bytes),
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ArgumentError('Either path or bytes must be provided');
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
use flutter_rust_bridge::frb;
|
||||||
|
|
||||||
|
pub struct RsCancellationToken {
|
||||||
|
pub(crate) inner: tokio_util::sync::CancellationToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[frb(sync)]
|
||||||
|
pub fn create_cancellation_token() -> RsCancellationToken {
|
||||||
|
RsCancellationToken {
|
||||||
|
inner: tokio_util::sync::CancellationToken::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RsCancellationToken {
|
||||||
|
#[frb(sync)]
|
||||||
|
pub fn cancel(&self) {
|
||||||
|
self.inner.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use crate::api::cancel::RsCancellationToken;
|
||||||
|
|
||||||
pub fn verify_cert(cert: String, public_key: String) -> anyhow::Result<()> {
|
pub fn verify_cert(cert: String, public_key: String) -> anyhow::Result<()> {
|
||||||
localsend::crypto::cert::verify_cert_from_pem(cert, Some(&public_key))
|
localsend::crypto::cert::verify_cert_from_pem(cert, Some(&public_key))
|
||||||
}
|
}
|
||||||
@@ -17,3 +19,38 @@ pub struct KeyPair {
|
|||||||
pub private_key: String,
|
pub private_key: String,
|
||||||
pub public_key: String,
|
pub public_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Computes the SHA-256 checksum of a file, encoded as lowercase hex.
|
||||||
|
///
|
||||||
|
/// The file is read chunk by chunk, so it is never fully loaded into memory.
|
||||||
|
/// Cancelling [cancel_token] aborts the read, so hashing a large file does not
|
||||||
|
/// have to be waited out.
|
||||||
|
///
|
||||||
|
/// Exactly one content source must be provided:
|
||||||
|
/// a [path] to a regular file, a [file_descriptor] (Android only), or [bytes]
|
||||||
|
/// for a file that only lives in memory.
|
||||||
|
pub async fn hash_file(
|
||||||
|
path: Option<String>,
|
||||||
|
file_descriptor: Option<i32>,
|
||||||
|
bytes: Option<Vec<u8>>,
|
||||||
|
cancel_token: &RsCancellationToken,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
let content = match (path, file_descriptor, bytes) {
|
||||||
|
(Some(path), None, None) => localsend::model::transfer::FileContent::Path(path.into()),
|
||||||
|
(None, Some(file_descriptor), None) => {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
localsend::model::transfer::FileContent::Fd(file_descriptor)
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
{
|
||||||
|
let _ = file_descriptor;
|
||||||
|
anyhow::bail!("File descriptors are only supported on Android");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, None, Some(bytes)) => return Ok(localsend::crypto::hash::sha256_hex(&bytes)),
|
||||||
|
_ => anyhow::bail!("Exactly one content source must be provided"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(localsend::crypto::hash::sha256_file_content(content, &cancel_token.inner).await?)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::api::cancel::RsCancellationToken;
|
||||||
use crate::api::stream;
|
use crate::api::stream;
|
||||||
use crate::frb_generated::StreamSink;
|
use crate::frb_generated::StreamSink;
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
@@ -29,24 +30,6 @@ pub fn create_client(
|
|||||||
Ok(RsHttpClient { inner })
|
Ok(RsHttpClient { inner })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RsCancellationToken {
|
|
||||||
inner: tokio_util::sync::CancellationToken,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[frb(sync)]
|
|
||||||
pub fn create_cancellation_token() -> RsCancellationToken {
|
|
||||||
RsCancellationToken {
|
|
||||||
inner: tokio_util::sync::CancellationToken::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RsCancellationToken {
|
|
||||||
#[frb(sync)]
|
|
||||||
pub fn cancel(&self) {
|
|
||||||
self.inner.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RsHttpClient {
|
impl RsHttpClient {
|
||||||
pub async fn register(
|
pub async fn register(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod cancel;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
// Section: imports
|
// Section: imports
|
||||||
|
|
||||||
|
use crate::api::cancel::*;
|
||||||
use crate::api::http::*;
|
use crate::api::http::*;
|
||||||
use crate::api::server::*;
|
use crate::api::server::*;
|
||||||
use crate::api::stream::*;
|
use crate::api::stream::*;
|
||||||
@@ -42,7 +43,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
|||||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||||
);
|
);
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1916764705;
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1193619754;
|
||||||
|
|
||||||
// Section: executor
|
// Section: executor
|
||||||
|
|
||||||
@@ -358,7 +359,7 @@ fn wire__crate__api__webrtc__LsSignalingConnection_update_info_impl(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn wire__crate__api__http__RsCancellationToken_cancel_impl(
|
fn wire__crate__api__cancel__RsCancellationToken_cancel_impl(
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
rust_vec_len_: i32,
|
rust_vec_len_: i32,
|
||||||
data_len_: i32,
|
data_len_: i32,
|
||||||
@@ -399,7 +400,7 @@ fn wire__crate__api__http__RsCancellationToken_cancel_impl(
|
|||||||
}
|
}
|
||||||
let api_that_guard = api_that_guard.unwrap();
|
let api_that_guard = api_that_guard.unwrap();
|
||||||
let output_ok = Result::<_, ()>::Ok({
|
let output_ok = Result::<_, ()>::Ok({
|
||||||
crate::api::http::RsCancellationToken::cancel(&*api_that_guard);
|
crate::api::cancel::RsCancellationToken::cancel(&*api_that_guard);
|
||||||
})?;
|
})?;
|
||||||
Ok(output_ok)
|
Ok(output_ok)
|
||||||
})())
|
})())
|
||||||
@@ -2272,7 +2273,7 @@ let api_on_connection = decode_DartFn_Inputs_Auto_Owned_RustOpaque_flutter_rust_
|
|||||||
})().await)
|
})().await)
|
||||||
} })
|
} })
|
||||||
}
|
}
|
||||||
fn wire__crate__api__http__create_cancellation_token_impl(
|
fn wire__crate__api__cancel__create_cancellation_token_impl(
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
rust_vec_len_: i32,
|
rust_vec_len_: i32,
|
||||||
data_len_: i32,
|
data_len_: i32,
|
||||||
@@ -2295,7 +2296,8 @@ fn wire__crate__api__http__create_cancellation_token_impl(
|
|||||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
deserializer.end();
|
deserializer.end();
|
||||||
transform_result_sse::<_, ()>((move || {
|
transform_result_sse::<_, ()>((move || {
|
||||||
let output_ok = Result::<_, ()>::Ok(crate::api::http::create_cancellation_token())?;
|
let output_ok =
|
||||||
|
Result::<_, ()>::Ok(crate::api::cancel::create_cancellation_token())?;
|
||||||
Ok(output_ok)
|
Ok(output_ok)
|
||||||
})())
|
})())
|
||||||
},
|
},
|
||||||
@@ -2440,6 +2442,72 @@ fn wire__crate__api__crypto__generate_key_pair_impl(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__api__crypto__hash_file_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "hash_file",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_path = <Option<String>>::sse_decode(&mut deserializer);
|
||||||
|
let api_file_descriptor = <Option<i32>>::sse_decode(&mut deserializer);
|
||||||
|
let api_bytes = <Option<Vec<u8>>>::sse_decode(&mut deserializer);
|
||||||
|
let api_cancel_token = <RustOpaqueMoi<
|
||||||
|
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsCancellationToken>,
|
||||||
|
>>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| async move {
|
||||||
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||||
|
(move || async move {
|
||||||
|
let mut api_cancel_token_guard = None;
|
||||||
|
let decode_indices_ =
|
||||||
|
flutter_rust_bridge::for_generated::lockable_compute_decode_order(
|
||||||
|
vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new(
|
||||||
|
&api_cancel_token,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
)],
|
||||||
|
);
|
||||||
|
for i in decode_indices_ {
|
||||||
|
match i {
|
||||||
|
0 => {
|
||||||
|
api_cancel_token_guard =
|
||||||
|
Some(api_cancel_token.lockable_decode_async_ref().await)
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let api_cancel_token_guard = api_cancel_token_guard.unwrap();
|
||||||
|
let output_ok = crate::api::crypto::hash_file(
|
||||||
|
api_path,
|
||||||
|
api_file_descriptor,
|
||||||
|
api_bytes,
|
||||||
|
&*api_cancel_token_guard,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__api__server__start_server_impl(
|
fn wire__crate__api__server__start_server_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
@@ -3452,6 +3520,17 @@ impl SseDecode for Option<Vec<String>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseDecode for Option<Vec<u8>> {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
if (<bool>::sse_decode(deserializer)) {
|
||||||
|
return Some(<Vec<u8>>::sse_decode(deserializer));
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseDecode for crate::api::webrtc::PinConfig {
|
impl SseDecode for crate::api::webrtc::PinConfig {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
@@ -4194,8 +4273,9 @@ fn pde_ffi_dispatcher_primary_impl(
|
|||||||
wire__crate__api__logging__enable_debug_logging_impl(port, ptr, rust_vec_len, data_len)
|
wire__crate__api__logging__enable_debug_logging_impl(port, ptr, rust_vec_len, data_len)
|
||||||
}
|
}
|
||||||
41 => wire__crate__api__crypto__generate_key_pair_impl(port, ptr, rust_vec_len, data_len),
|
41 => wire__crate__api__crypto__generate_key_pair_impl(port, ptr, rust_vec_len, data_len),
|
||||||
42 => wire__crate__api__server__start_server_impl(port, ptr, rust_vec_len, data_len),
|
42 => wire__crate__api__crypto__hash_file_impl(port, ptr, rust_vec_len, data_len),
|
||||||
43 => wire__crate__api__crypto__verify_cert_impl(port, ptr, rust_vec_len, data_len),
|
43 => wire__crate__api__server__start_server_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
44 => wire__crate__api__crypto__verify_cert_impl(port, ptr, rust_vec_len, data_len),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4209,8 +4289,8 @@ fn pde_ffi_dispatcher_sync_impl(
|
|||||||
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||||
match func_id {
|
match func_id {
|
||||||
2 => wire__crate__api__stream__Dart2RustStreamSink_close_impl(ptr, rust_vec_len, data_len),
|
2 => wire__crate__api__stream__Dart2RustStreamSink_close_impl(ptr, rust_vec_len, data_len),
|
||||||
6 => wire__crate__api__http__RsCancellationToken_cancel_impl(ptr, rust_vec_len, data_len),
|
6 => wire__crate__api__cancel__RsCancellationToken_cancel_impl(ptr, rust_vec_len, data_len),
|
||||||
37 => wire__crate__api__http__create_cancellation_token_impl(ptr, rust_vec_len, data_len),
|
37 => wire__crate__api__cancel__create_cancellation_token_impl(ptr, rust_vec_len, data_len),
|
||||||
38 => wire__crate__api__http__create_client_impl(ptr, rust_vec_len, data_len),
|
38 => wire__crate__api__http__create_client_impl(ptr, rust_vec_len, data_len),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
@@ -5798,6 +5878,16 @@ impl SseEncode for Option<Vec<String>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseEncode for Option<Vec<u8>> {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<bool>::sse_encode(self.is_some(), serializer);
|
||||||
|
if let Some(value) = self {
|
||||||
|
<Vec<u8>>::sse_encode(value, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseEncode for crate::api::webrtc::PinConfig {
|
impl SseEncode for crate::api::webrtc::PinConfig {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
@@ -6263,6 +6353,7 @@ mod io {
|
|||||||
// Section: imports
|
// Section: imports
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::api::cancel::*;
|
||||||
use crate::api::http::*;
|
use crate::api::http::*;
|
||||||
use crate::api::server::*;
|
use crate::api::server::*;
|
||||||
use crate::api::stream::*;
|
use crate::api::stream::*;
|
||||||
@@ -6429,6 +6520,7 @@ mod web {
|
|||||||
// Section: imports
|
// Section: imports
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::api::cancel::*;
|
||||||
use crate::api::http::*;
|
use crate::api::http::*;
|
||||||
use crate::api::server::*;
|
use crate::api::server::*;
|
||||||
use crate::api::stream::*;
|
use crate::api::stream::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user