mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
feat: show checksum progress by byte
This commit is contained in:
@@ -3,9 +3,11 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:localsend_app/config/theme.dart';
|
||||
import 'package:localsend_app/gen/strings.g.dart';
|
||||
import 'package:localsend_app/model/state/send/send_session_state.dart';
|
||||
import 'package:localsend_app/provider/device_info_provider.dart';
|
||||
import 'package:localsend_app/provider/favorites_provider.dart';
|
||||
import 'package:localsend_app/provider/network/send_provider.dart';
|
||||
import 'package:localsend_app/provider/progress_provider.dart';
|
||||
import 'package:localsend_app/util/favorites.dart';
|
||||
import 'package:localsend_app/util/native/taskbar_helper.dart';
|
||||
import 'package:localsend_app/widget/animations/initial_fade_transition.dart';
|
||||
@@ -33,6 +35,19 @@ class SendPage extends StatefulWidget {
|
||||
State<SendPage> createState() => _SendPageState();
|
||||
}
|
||||
|
||||
double _hashProgress(SendSessionState sendState, ProgressNotifier progressNotifier) {
|
||||
final files = sendState.files.values;
|
||||
final totalBytes = files.fold<int>(0, (prev, curr) => prev + curr.file.size);
|
||||
if (totalBytes == 0) {
|
||||
return sendState.files.isEmpty ? 0 : sendState.hashedFileCount / sendState.files.length;
|
||||
}
|
||||
final hashedBytes = files.fold<double>(
|
||||
0,
|
||||
(prev, curr) => prev + progressNotifier.getProgress(sessionId: sendState.sessionId, fileId: curr.file.id) * curr.file.size,
|
||||
);
|
||||
return (hashedBytes / totalBytes).clamp(0, 1);
|
||||
}
|
||||
|
||||
class _SendPageState extends State<SendPage> with Refena {
|
||||
Device? _myDevice;
|
||||
Device? _targetDevice;
|
||||
@@ -145,7 +160,7 @@ class _SendPageState extends State<SendPage> with Refena {
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: LinearProgressIndicator(
|
||||
value: sendState.hashedFileCount / sendState.files.length,
|
||||
value: _hashProgress(sendState, ref.watch(progressProvider)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -140,7 +140,24 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
try {
|
||||
for (final (:id, :file) in selectedFiles) {
|
||||
try {
|
||||
hashes[id] = await calculateFileHash(path: file.path, bytes: file.bytes, cancelToken: hashCancelToken);
|
||||
hashes[id] = await calculateFileHash(
|
||||
path: file.path,
|
||||
bytes: file.bytes,
|
||||
cancelToken: hashCancelToken,
|
||||
onProgress: (bytes) {
|
||||
if (state[sessionId] == null) {
|
||||
// session has been canceled while calculating the checksums
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.notifier(progressProvider)
|
||||
.setProgress(
|
||||
sessionId: sessionId,
|
||||
fileId: id,
|
||||
progress: file.size == 0 ? 1 : (bytes / file.size).clamp(0, 1),
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
if (state[sessionId] != null) {
|
||||
// Sending the checksum is optional, so a file that cannot be read
|
||||
@@ -155,6 +172,9 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Also set for files whose hashing failed, so the progress bar stays
|
||||
// consistent with the files that are left.
|
||||
ref.notifier(progressProvider).setProgress(sessionId: sessionId, fileId: id, progress: 1);
|
||||
state = state.updateSession(
|
||||
sessionId: sessionId,
|
||||
state: (s) => s?.copyWith(hashedFileCount: s.hashedFileCount + 1),
|
||||
@@ -349,6 +369,10 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
file.file.id: fileMap.containsKey(file.file.id) ? file.copyWith(token: fileMap[file.file.id]) : file.copyWith(status: FileStatus.skipped),
|
||||
};
|
||||
|
||||
// The hash progress is no longer needed and must not be mistaken for
|
||||
// upload progress, which starts at zero for every file.
|
||||
ref.notifier(progressProvider).removeSession(sessionId);
|
||||
|
||||
if (state[sessionId]?.background == false) {
|
||||
final background = ref.read(settingsProvider).sendMode == SendMode.multiple;
|
||||
|
||||
|
||||
@@ -26,27 +26,38 @@ pub fn sha256_hex(data: &[u8]) -> String {
|
||||
}
|
||||
|
||||
/// Computes the SHA-256 checksum of a file's content, encoded as lowercase hex.
|
||||
///
|
||||
/// `progress` is invoked with the cumulative number of bytes hashed as each
|
||||
/// chunk is consumed, mirroring the upload progress callback.
|
||||
pub async fn sha256_file_content(
|
||||
content: FileContent,
|
||||
cancel_token: &CancellationToken,
|
||||
progress: impl Fn(u64),
|
||||
) -> 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::Stream(mut receiver) => {
|
||||
let mut hashed = 0_u64;
|
||||
loop {
|
||||
let chunk = tokio::select! {
|
||||
biased;
|
||||
_ = cancel_token.cancelled() => return Err(HashError::Cancelled),
|
||||
chunk = receiver.recv() => chunk,
|
||||
};
|
||||
match chunk {
|
||||
Some(chunk) => {
|
||||
hasher.update(&chunk);
|
||||
hashed += chunk.len() as u64;
|
||||
progress(hashed);
|
||||
}
|
||||
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?;
|
||||
read_and_hash_from_file(&mut hasher, file, cancel_token, progress).await?;
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
FileContent::Fd(fd) => {
|
||||
@@ -57,7 +68,7 @@ pub async fn sha256_file_content(
|
||||
// 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?;
|
||||
read_and_hash_from_file(&mut hasher, file, cancel_token, progress).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +80,12 @@ async fn read_and_hash_from_file(
|
||||
hasher: &mut Sha256,
|
||||
mut file: tokio::fs::File,
|
||||
cancel_token: &CancellationToken,
|
||||
progress: impl Fn(u64),
|
||||
) -> Result<(), HashError> {
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
let mut buffer = vec![0u8; HASH_BUFFER_SIZE];
|
||||
let mut hashed = 0_u64;
|
||||
loop {
|
||||
let read = tokio::select! {
|
||||
biased;
|
||||
@@ -83,6 +96,8 @@ async fn read_and_hash_from_file(
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..read]);
|
||||
hashed += read as u64;
|
||||
progress(hashed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+32
-13
@@ -12,26 +12,40 @@ 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();
|
||||
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.
|
||||
/// A file larger than the internal buffer must be hashed across multiple reads,
|
||||
/// reporting the cumulative progress after each of them.
|
||||
#[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();
|
||||
let progress = std::sync::Mutex::new(Vec::new());
|
||||
let hash = sha256_file_content(
|
||||
FileContent::Path(path.clone()),
|
||||
&CancellationToken::new(),
|
||||
|hashed| progress.lock().unwrap().push(hashed),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(hash, sha256_hex(&content));
|
||||
let progress = progress.into_inner().unwrap();
|
||||
assert!(progress.len() > 1);
|
||||
assert!(progress.windows(2).all(|pair| pair[0] < pair[1]));
|
||||
assert_eq!(*progress.last().unwrap(), content.len() as u64);
|
||||
tokio::fs::remove_file(&path).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -43,7 +57,7 @@ async fn hash_cancelled_while_reading() {
|
||||
|
||||
let handle = tokio::spawn({
|
||||
let token = cancel_token.clone();
|
||||
async move { sha256_file_content(FileContent::Stream(rx), &token).await }
|
||||
async move { sha256_file_content(FileContent::Stream(rx), &token, |_| {}).await }
|
||||
});
|
||||
|
||||
// The sender stays alive, so hashing only ends because of the cancellation.
|
||||
@@ -63,7 +77,7 @@ async fn hash_cancelled_before_start() {
|
||||
let cancel_token = CancellationToken::new();
|
||||
cancel_token.cancel();
|
||||
|
||||
let result = sha256_file_content(FileContent::Path(path.clone()), &cancel_token).await;
|
||||
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();
|
||||
@@ -73,7 +87,8 @@ async fn hash_cancelled_before_start() {
|
||||
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;
|
||||
let result =
|
||||
sha256_file_content(FileContent::Path(path), &CancellationToken::new(), |_| {}).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
@@ -86,9 +101,13 @@ async fn hash_stream() {
|
||||
tx.send(bytes::Bytes::from_static(b"world")).await.unwrap();
|
||||
});
|
||||
|
||||
let hash = sha256_file_content(FileContent::Stream(rx), &CancellationToken::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let hash = sha256_file_content(
|
||||
FileContent::Stream(rx),
|
||||
&CancellationToken::new(),
|
||||
|_| {},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(hash, HELLO_WORLD_HASH);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
||||
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
|
||||
import 'package:localsend_isolates/rust/api/cancel.dart';
|
||||
import 'package:localsend_isolates/rust/frb_generated.dart';
|
||||
|
||||
part 'crypto.freezed.dart';
|
||||
|
||||
Future<void> verifyCert({required String cert, required String publicKey}) =>
|
||||
RustLib.instance.api.crateApiCryptoVerifyCert(cert: cert, publicKey: publicKey);
|
||||
|
||||
@@ -16,7 +19,8 @@ Future<KeyPair> generateKeyPair() => RustLib.instance.api.crateApiCryptoGenerate
|
||||
/// certificate whose SHA-256 fingerprint identifies the device.
|
||||
Future<SecurityContext> generateSecurityContext() => RustLib.instance.api.crateApiCryptoGenerateSecurityContext();
|
||||
|
||||
/// Computes the SHA-256 checksum of a file, encoded as lowercase hex.
|
||||
/// Computes the SHA-256 checksum of a file, reported as the final
|
||||
/// [RsHashFileEvent::Done] event of the returned stream.
|
||||
///
|
||||
/// 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
|
||||
@@ -25,7 +29,7 @@ Future<SecurityContext> generateSecurityContext() => RustLib.instance.api.crateA
|
||||
/// 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}) =>
|
||||
Stream<RsHashFileEvent> hashFile({String? path, int? fileDescriptor, Uint8List? bytes, required RsCancellationToken cancelToken}) =>
|
||||
RustLib.instance.api.crateApiCryptoHashFile(path: path, fileDescriptor: fileDescriptor, bytes: bytes, cancelToken: cancelToken);
|
||||
|
||||
class KeyPair {
|
||||
@@ -46,6 +50,23 @@ class KeyPair {
|
||||
other is KeyPair && runtimeType == other.runtimeType && privateKey == other.privateKey && publicKey == other.publicKey;
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class RsHashFileEvent with _$RsHashFileEvent {
|
||||
const RsHashFileEvent._();
|
||||
|
||||
/// Cumulative number of bytes hashed so far.
|
||||
/// Throttled, so not every hashed chunk is reported.
|
||||
const factory RsHashFileEvent.progress({
|
||||
required BigInt bytes,
|
||||
}) = RsHashFileEvent_Progress;
|
||||
|
||||
/// Hashing has finished; [hash] is the checksum, encoded as lowercase hex.
|
||||
/// Always the last event of the stream.
|
||||
const factory RsHashFileEvent.done({
|
||||
required String hash,
|
||||
}) = RsHashFileEvent_Done;
|
||||
}
|
||||
|
||||
class SecurityContext {
|
||||
final String privateKey;
|
||||
final String publicKey;
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'crypto.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$RsHashFileEvent {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is RsHashFileEvent);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RsHashFileEvent()';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class $RsHashFileEventCopyWith<$Res> {
|
||||
$RsHashFileEventCopyWith(RsHashFileEvent _, $Res Function(RsHashFileEvent) __);
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [RsHashFileEvent].
|
||||
extension RsHashFileEventPatterns on RsHashFileEvent {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( RsHashFileEvent_Progress value)? progress,TResult Function( RsHashFileEvent_Done value)? done,required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case RsHashFileEvent_Progress() when progress != null:
|
||||
return progress(_that);case RsHashFileEvent_Done() when done != null:
|
||||
return done(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( RsHashFileEvent_Progress value) progress,required TResult Function( RsHashFileEvent_Done value) done,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case RsHashFileEvent_Progress():
|
||||
return progress(_that);case RsHashFileEvent_Done():
|
||||
return done(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( RsHashFileEvent_Progress value)? progress,TResult? Function( RsHashFileEvent_Done value)? done,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case RsHashFileEvent_Progress() when progress != null:
|
||||
return progress(_that);case RsHashFileEvent_Done() when done != null:
|
||||
return done(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( BigInt bytes)? progress,TResult Function( String hash)? done,required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case RsHashFileEvent_Progress() when progress != null:
|
||||
return progress(_that.bytes);case RsHashFileEvent_Done() when done != null:
|
||||
return done(_that.hash);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( BigInt bytes) progress,required TResult Function( String hash) done,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case RsHashFileEvent_Progress():
|
||||
return progress(_that.bytes);case RsHashFileEvent_Done():
|
||||
return done(_that.hash);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( BigInt bytes)? progress,TResult? Function( String hash)? done,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case RsHashFileEvent_Progress() when progress != null:
|
||||
return progress(_that.bytes);case RsHashFileEvent_Done() when done != null:
|
||||
return done(_that.hash);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class RsHashFileEvent_Progress extends RsHashFileEvent {
|
||||
const RsHashFileEvent_Progress({required this.bytes}): super._();
|
||||
|
||||
|
||||
final BigInt bytes;
|
||||
|
||||
/// Create a copy of RsHashFileEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$RsHashFileEvent_ProgressCopyWith<RsHashFileEvent_Progress> get copyWith => _$RsHashFileEvent_ProgressCopyWithImpl<RsHashFileEvent_Progress>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is RsHashFileEvent_Progress&&(identical(other.bytes, bytes) || other.bytes == bytes));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,bytes);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RsHashFileEvent.progress(bytes: $bytes)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $RsHashFileEvent_ProgressCopyWith<$Res> implements $RsHashFileEventCopyWith<$Res> {
|
||||
factory $RsHashFileEvent_ProgressCopyWith(RsHashFileEvent_Progress value, $Res Function(RsHashFileEvent_Progress) _then) = _$RsHashFileEvent_ProgressCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
BigInt bytes
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$RsHashFileEvent_ProgressCopyWithImpl<$Res>
|
||||
implements $RsHashFileEvent_ProgressCopyWith<$Res> {
|
||||
_$RsHashFileEvent_ProgressCopyWithImpl(this._self, this._then);
|
||||
|
||||
final RsHashFileEvent_Progress _self;
|
||||
final $Res Function(RsHashFileEvent_Progress) _then;
|
||||
|
||||
/// Create a copy of RsHashFileEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? bytes = null,}) {
|
||||
return _then(RsHashFileEvent_Progress(
|
||||
bytes: null == bytes ? _self.bytes : bytes // ignore: cast_nullable_to_non_nullable
|
||||
as BigInt,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class RsHashFileEvent_Done extends RsHashFileEvent {
|
||||
const RsHashFileEvent_Done({required this.hash}): super._();
|
||||
|
||||
|
||||
final String hash;
|
||||
|
||||
/// Create a copy of RsHashFileEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$RsHashFileEvent_DoneCopyWith<RsHashFileEvent_Done> get copyWith => _$RsHashFileEvent_DoneCopyWithImpl<RsHashFileEvent_Done>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is RsHashFileEvent_Done&&(identical(other.hash, hash) || other.hash == hash));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,hash);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RsHashFileEvent.done(hash: $hash)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $RsHashFileEvent_DoneCopyWith<$Res> implements $RsHashFileEventCopyWith<$Res> {
|
||||
factory $RsHashFileEvent_DoneCopyWith(RsHashFileEvent_Done value, $Res Function(RsHashFileEvent_Done) _then) = _$RsHashFileEvent_DoneCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String hash
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$RsHashFileEvent_DoneCopyWithImpl<$Res>
|
||||
implements $RsHashFileEvent_DoneCopyWith<$Res> {
|
||||
_$RsHashFileEvent_DoneCopyWithImpl(this._self, this._then);
|
||||
|
||||
final RsHashFileEvent_Done _self;
|
||||
final $Res Function(RsHashFileEvent_Done) _then;
|
||||
|
||||
/// Create a copy of RsHashFileEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? hash = null,}) {
|
||||
return _then(RsHashFileEvent_Done(
|
||||
hash: null == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -249,7 +249,7 @@ abstract class RustLibApi extends BaseApi {
|
||||
|
||||
Future<SecurityContext> crateApiCryptoGenerateSecurityContext();
|
||||
|
||||
Future<String> crateApiCryptoHashFile({String? path, int? fileDescriptor, Uint8List? bytes, required RsCancellationToken cancelToken});
|
||||
Stream<RsHashFileEvent> crateApiCryptoHashFile({String? path, int? fileDescriptor, Uint8List? bytes, required RsCancellationToken cancelToken});
|
||||
|
||||
Future<RsMulticast> crateApiMulticastStartMulticast({
|
||||
required String group,
|
||||
@@ -1697,31 +1697,36 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
);
|
||||
|
||||
@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: 46, port: port_);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_String,
|
||||
decodeErrorData: sse_decode_AnyhowException,
|
||||
Stream<RsHashFileEvent> crateApiCryptoHashFile({String? path, int? fileDescriptor, Uint8List? bytes, required RsCancellationToken cancelToken}) {
|
||||
final sink = RustStreamSink<RsHashFileEvent>();
|
||||
unawaited(
|
||||
handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_StreamSink_rs_hash_file_event_Sse(sink, serializer);
|
||||
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: 46, port: port_);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
decodeErrorData: sse_decode_AnyhowException,
|
||||
),
|
||||
constMeta: kCrateApiCryptoHashFileConstMeta,
|
||||
argValues: [sink, path, fileDescriptor, bytes, cancelToken],
|
||||
apiImpl: this,
|
||||
),
|
||||
constMeta: kCrateApiCryptoHashFileConstMeta,
|
||||
argValues: [path, fileDescriptor, bytes, cancelToken],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
return sink.stream;
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiCryptoHashFileConstMeta => const TaskConstMeta(
|
||||
debugName: 'hash_file',
|
||||
argNames: ['path', 'fileDescriptor', 'bytes', 'cancelToken'],
|
||||
argNames: ['sink', 'path', 'fileDescriptor', 'bytes', 'cancelToken'],
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -2207,6 +2212,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@protected
|
||||
RustStreamSink<RsHashFileEvent> dco_decode_StreamSink_rs_hash_file_event_Sse(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@protected
|
||||
RustStreamSink<RsMulticastDiscovered> dco_decode_StreamSink_rs_multicast_discovered_Sse(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
@@ -2790,6 +2801,23 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
RsHashFileEvent dco_decode_rs_hash_file_event(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
switch (raw[0]) {
|
||||
case 0:
|
||||
return RsHashFileEvent_Progress(
|
||||
bytes: dco_decode_u_64(raw[1]),
|
||||
);
|
||||
case 1:
|
||||
return RsHashFileEvent_Done(
|
||||
hash: dco_decode_String(raw[1]),
|
||||
);
|
||||
default:
|
||||
throw Exception('unreachable');
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
RsHttpClientError dco_decode_rs_http_client_error(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
@@ -3363,6 +3391,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
throw UnimplementedError('Unreachable ()');
|
||||
}
|
||||
|
||||
@protected
|
||||
RustStreamSink<RsHashFileEvent> sse_decode_StreamSink_rs_hash_file_event_Sse(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
throw UnimplementedError('Unreachable ()');
|
||||
}
|
||||
|
||||
@protected
|
||||
RustStreamSink<RsMulticastDiscovered> sse_decode_StreamSink_rs_multicast_discovered_Sse(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
@@ -4028,6 +4062,23 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return ResultWithPublicKeyRegisterResponseDto(publicKey: var_publicKey, body: var_body);
|
||||
}
|
||||
|
||||
@protected
|
||||
RsHashFileEvent sse_decode_rs_hash_file_event(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
||||
var tag_ = sse_decode_i_32(deserializer);
|
||||
switch (tag_) {
|
||||
case 0:
|
||||
var var_bytes = sse_decode_u_64(deserializer);
|
||||
return RsHashFileEvent_Progress(bytes: var_bytes);
|
||||
case 1:
|
||||
var var_hash = sse_decode_String(deserializer);
|
||||
return RsHashFileEvent_Done(hash: var_hash);
|
||||
default:
|
||||
throw UnimplementedError('');
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
RsHttpClientError sse_decode_rs_http_client_error(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
@@ -4650,6 +4701,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_StreamSink_rs_hash_file_event_Sse(RustStreamSink<RsHashFileEvent> self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_String(
|
||||
self.setupAndSerialize(
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_rs_hash_file_event,
|
||||
decodeErrorData: sse_decode_AnyhowException,
|
||||
),
|
||||
),
|
||||
serializer,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_StreamSink_rs_multicast_discovered_Sse(RustStreamSink<RsMulticastDiscovered> self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
@@ -5262,6 +5327,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
sse_encode_register_response_dto(self.body, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_rs_hash_file_event(RsHashFileEvent self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
switch (self) {
|
||||
case RsHashFileEvent_Progress(bytes: final bytes):
|
||||
sse_encode_i_32(0, serializer);
|
||||
sse_encode_u_64(bytes, serializer);
|
||||
case RsHashFileEvent_Done(hash: final hash):
|
||||
sse_encode_i_32(1, serializer);
|
||||
sse_encode_String(hash, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_rs_http_client_error(RsHttpClientError self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
||||
@@ -192,6 +192,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
RustStreamSink<Uint8List> dco_decode_StreamSink_list_prim_u_8_strict_Sse(dynamic raw);
|
||||
|
||||
@protected
|
||||
RustStreamSink<RsHashFileEvent> dco_decode_StreamSink_rs_hash_file_event_Sse(dynamic raw);
|
||||
|
||||
@protected
|
||||
RustStreamSink<RsMulticastDiscovered> dco_decode_StreamSink_rs_multicast_discovered_Sse(dynamic raw);
|
||||
|
||||
@@ -417,6 +420,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
ResultWithPublicKeyRegisterResponseDto dco_decode_result_with_public_key_register_response_dto(dynamic raw);
|
||||
|
||||
@protected
|
||||
RsHashFileEvent dco_decode_rs_hash_file_event(dynamic raw);
|
||||
|
||||
@protected
|
||||
RsHttpClientError dco_decode_rs_http_client_error(dynamic raw);
|
||||
|
||||
@@ -623,6 +629,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
RustStreamSink<Uint8List> sse_decode_StreamSink_list_prim_u_8_strict_Sse(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
RustStreamSink<RsHashFileEvent> sse_decode_StreamSink_rs_hash_file_event_Sse(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
RustStreamSink<RsMulticastDiscovered> sse_decode_StreamSink_rs_multicast_discovered_Sse(SseDeserializer deserializer);
|
||||
|
||||
@@ -850,6 +859,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
ResultWithPublicKeyRegisterResponseDto sse_decode_result_with_public_key_register_response_dto(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
RsHashFileEvent sse_decode_rs_hash_file_event(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
RsHttpClientError sse_decode_rs_http_client_error(SseDeserializer deserializer);
|
||||
|
||||
@@ -1100,6 +1112,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
void sse_encode_StreamSink_list_prim_u_8_strict_Sse(RustStreamSink<Uint8List> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_StreamSink_rs_hash_file_event_Sse(RustStreamSink<RsHashFileEvent> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_StreamSink_rs_multicast_discovered_Sse(RustStreamSink<RsMulticastDiscovered> self, SseSerializer serializer);
|
||||
|
||||
@@ -1329,6 +1344,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
void sse_encode_result_with_public_key_register_response_dto(ResultWithPublicKeyRegisterResponseDto self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_rs_hash_file_event(RsHashFileEvent self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_rs_http_client_error(RsHttpClientError self, SseSerializer serializer);
|
||||
|
||||
|
||||
@@ -194,6 +194,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
RustStreamSink<Uint8List> dco_decode_StreamSink_list_prim_u_8_strict_Sse(dynamic raw);
|
||||
|
||||
@protected
|
||||
RustStreamSink<RsHashFileEvent> dco_decode_StreamSink_rs_hash_file_event_Sse(dynamic raw);
|
||||
|
||||
@protected
|
||||
RustStreamSink<RsMulticastDiscovered> dco_decode_StreamSink_rs_multicast_discovered_Sse(dynamic raw);
|
||||
|
||||
@@ -419,6 +422,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
ResultWithPublicKeyRegisterResponseDto dco_decode_result_with_public_key_register_response_dto(dynamic raw);
|
||||
|
||||
@protected
|
||||
RsHashFileEvent dco_decode_rs_hash_file_event(dynamic raw);
|
||||
|
||||
@protected
|
||||
RsHttpClientError dco_decode_rs_http_client_error(dynamic raw);
|
||||
|
||||
@@ -625,6 +631,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
RustStreamSink<Uint8List> sse_decode_StreamSink_list_prim_u_8_strict_Sse(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
RustStreamSink<RsHashFileEvent> sse_decode_StreamSink_rs_hash_file_event_Sse(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
RustStreamSink<RsMulticastDiscovered> sse_decode_StreamSink_rs_multicast_discovered_Sse(SseDeserializer deserializer);
|
||||
|
||||
@@ -852,6 +861,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
ResultWithPublicKeyRegisterResponseDto sse_decode_result_with_public_key_register_response_dto(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
RsHashFileEvent sse_decode_rs_hash_file_event(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
RsHttpClientError sse_decode_rs_http_client_error(SseDeserializer deserializer);
|
||||
|
||||
@@ -1102,6 +1114,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
void sse_encode_StreamSink_list_prim_u_8_strict_Sse(RustStreamSink<Uint8List> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_StreamSink_rs_hash_file_event_Sse(RustStreamSink<RsHashFileEvent> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_StreamSink_rs_multicast_discovered_Sse(RustStreamSink<RsMulticastDiscovered> self, SseSerializer serializer);
|
||||
|
||||
@@ -1331,6 +1346,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
void sse_encode_result_with_public_key_register_response_dto(ResultWithPublicKeyRegisterResponseDto self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_rs_hash_file_event(RsHashFileEvent self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_rs_http_client_error(RsHttpClientError self, SseSerializer serializer);
|
||||
|
||||
|
||||
@@ -8,28 +8,44 @@ import 'package:localsend_isolates/util/android_channel.dart';
|
||||
///
|
||||
/// The file is read and hashed in Rust, so this only occupies the Dart isolate
|
||||
/// while waiting for the result.
|
||||
/// [onProgress] is called with the cumulative number of bytes hashed so far;
|
||||
/// the events are already throttled on the Rust side.
|
||||
/// Cancelling [cancelToken] aborts the calculation and throws.
|
||||
Future<String> calculateFileHash({
|
||||
required String? path,
|
||||
required List<int>? bytes,
|
||||
required RsCancellationToken cancelToken,
|
||||
void Function(int bytes)? onProgress,
|
||||
}) async {
|
||||
final Stream<rust_crypto.RsHashFileEvent> events;
|
||||
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);
|
||||
events = rust_crypto.hashFile(fileDescriptor: fileDescriptor, cancelToken: cancelToken);
|
||||
} else {
|
||||
events = rust_crypto.hashFile(path: path, cancelToken: cancelToken);
|
||||
}
|
||||
return await rust_crypto.hashFile(path: path, cancelToken: cancelToken);
|
||||
}
|
||||
|
||||
if (bytes != null) {
|
||||
return await rust_crypto.hashFile(
|
||||
} else if (bytes != null) {
|
||||
events = rust_crypto.hashFile(
|
||||
bytes: bytes is Uint8List ? bytes : Uint8List.fromList(bytes),
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
} else {
|
||||
throw ArgumentError('Either path or bytes must be provided');
|
||||
}
|
||||
|
||||
throw ArgumentError('Either path or bytes must be provided');
|
||||
await for (final event in events) {
|
||||
switch (event) {
|
||||
case rust_crypto.RsHashFileEvent_Progress():
|
||||
onProgress?.call(event.bytes.toInt());
|
||||
case rust_crypto.RsHashFileEvent_Done():
|
||||
return event.hash;
|
||||
}
|
||||
}
|
||||
|
||||
// The Done event is always the last event, so this is unreachable unless
|
||||
// the Rust side failed, in which case the loop above has already thrown.
|
||||
throw StateError('Hashing ended without a result');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::api::cancel::RsCancellationToken;
|
||||
use crate::frb_generated::StreamSink;
|
||||
|
||||
pub fn verify_cert(cert: String, public_key: String) -> anyhow::Result<()> {
|
||||
localsend::crypto::cert::verify_cert_from_pem(cert, Some(&public_key))
|
||||
@@ -40,7 +41,20 @@ pub struct SecurityContext {
|
||||
pub certificate_hash: String,
|
||||
}
|
||||
|
||||
/// Computes the SHA-256 checksum of a file, encoded as lowercase hex.
|
||||
/// An event emitted while a file is being hashed by [hash_file].
|
||||
#[derive(Clone)]
|
||||
pub enum RsHashFileEvent {
|
||||
/// Cumulative number of bytes hashed so far.
|
||||
/// Throttled, so not every hashed chunk is reported.
|
||||
Progress { bytes: u64 },
|
||||
|
||||
/// Hashing has finished; [hash] is the checksum, encoded as lowercase hex.
|
||||
/// Always the last event of the stream.
|
||||
Done { hash: String },
|
||||
}
|
||||
|
||||
/// Computes the SHA-256 checksum of a file, reported as the final
|
||||
/// [RsHashFileEvent::Done] event of the returned stream.
|
||||
///
|
||||
/// 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
|
||||
@@ -50,11 +64,12 @@ pub struct SecurityContext {
|
||||
/// 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(
|
||||
sink: StreamSink<RsHashFileEvent>,
|
||||
path: Option<String>,
|
||||
file_descriptor: Option<i32>,
|
||||
bytes: Option<Vec<u8>>,
|
||||
cancel_token: &RsCancellationToken,
|
||||
) -> anyhow::Result<String> {
|
||||
) -> anyhow::Result<()> {
|
||||
let content = match (path, file_descriptor, bytes) {
|
||||
(Some(path), None, None) => localsend::model::transfer::FileContent::Path(path.into()),
|
||||
(None, Some(file_descriptor), None) => {
|
||||
@@ -68,9 +83,33 @@ pub async fn hash_file(
|
||||
anyhow::bail!("File descriptors are only supported on Android");
|
||||
}
|
||||
}
|
||||
(None, None, Some(bytes)) => return Ok(localsend::crypto::hash::sha256_hex(&bytes)),
|
||||
(None, None, Some(bytes)) => {
|
||||
let hash = localsend::crypto::hash::sha256_hex(&bytes);
|
||||
let _ = sink.add(RsHashFileEvent::Done { hash });
|
||||
return Ok(());
|
||||
}
|
||||
_ => anyhow::bail!("Exactly one content source must be provided"),
|
||||
};
|
||||
|
||||
Ok(localsend::crypto::hash::sha256_file_content(content, &cancel_token.inner).await?)
|
||||
// Progress events with throttling
|
||||
let last_emit = std::cell::Cell::new(None::<std::time::Instant>);
|
||||
let progress = {
|
||||
let sink = sink.clone();
|
||||
move |hashed| {
|
||||
let now = std::time::Instant::now();
|
||||
if let Some(last) = last_emit.get() {
|
||||
if now.duration_since(last) < std::time::Duration::from_millis(20) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
last_emit.set(Some(now));
|
||||
let _ = sink.add(RsHashFileEvent::Progress { bytes: hashed });
|
||||
}
|
||||
};
|
||||
|
||||
let hash =
|
||||
localsend::crypto::hash::sha256_file_content(content, &cancel_token.inner, progress)
|
||||
.await?;
|
||||
let _ = sink.add(RsHashFileEvent::Done { hash });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2694,6 +2694,10 @@ fn wire__crate__api__crypto__hash_file_impl(
|
||||
};
|
||||
let mut deserializer =
|
||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||
let api_sink = <StreamSink<
|
||||
crate::api::crypto::RsHashFileEvent,
|
||||
flutter_rust_bridge::for_generated::SseCodec,
|
||||
>>::sse_decode(&mut deserializer);
|
||||
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);
|
||||
@@ -2724,6 +2728,7 @@ fn wire__crate__api__crypto__hash_file_impl(
|
||||
}
|
||||
let api_cancel_token_guard = api_cancel_token_guard.unwrap();
|
||||
let output_ok = crate::api::crypto::hash_file(
|
||||
api_sink,
|
||||
api_path,
|
||||
api_file_descriptor,
|
||||
api_bytes,
|
||||
@@ -3422,6 +3427,19 @@ impl SseDecode for StreamSink<Vec<u8>, flutter_rust_bridge::for_generated::SseCo
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode
|
||||
for StreamSink<
|
||||
crate::api::crypto::RsHashFileEvent,
|
||||
flutter_rust_bridge::for_generated::SseCodec,
|
||||
>
|
||||
{
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut inner = <String>::sse_decode(deserializer);
|
||||
return StreamSink::deserialize(inner);
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode
|
||||
for StreamSink<
|
||||
crate::api::multicast::RsMulticastDiscovered,
|
||||
@@ -4091,6 +4109,26 @@ impl SseDecode for crate::api::http::ResultWithPublicKeyRegisterResponseDto {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::api::crypto::RsHashFileEvent {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut tag_ = <i32>::sse_decode(deserializer);
|
||||
match tag_ {
|
||||
0 => {
|
||||
let mut var_bytes = <u64>::sse_decode(deserializer);
|
||||
return crate::api::crypto::RsHashFileEvent::Progress { bytes: var_bytes };
|
||||
}
|
||||
1 => {
|
||||
let mut var_hash = <String>::sse_decode(deserializer);
|
||||
return crate::api::crypto::RsHashFileEvent::Done { hash: var_hash };
|
||||
}
|
||||
_ => {
|
||||
unimplemented!("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::api::http::RsHttpClientError {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@@ -5345,6 +5383,33 @@ impl flutter_rust_bridge::IntoIntoDart<crate::api::http::ResultWithPublicKeyRegi
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::api::crypto::RsHashFileEvent {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
match self {
|
||||
crate::api::crypto::RsHashFileEvent::Progress { bytes } => {
|
||||
[0.into_dart(), bytes.into_into_dart().into_dart()].into_dart()
|
||||
}
|
||||
crate::api::crypto::RsHashFileEvent::Done { hash } => {
|
||||
[1.into_dart(), hash.into_into_dart().into_dart()].into_dart()
|
||||
}
|
||||
_ => {
|
||||
unimplemented!("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
|
||||
for crate::api::crypto::RsHashFileEvent
|
||||
{
|
||||
}
|
||||
impl flutter_rust_bridge::IntoIntoDart<crate::api::crypto::RsHashFileEvent>
|
||||
for crate::api::crypto::RsHashFileEvent
|
||||
{
|
||||
fn into_into_dart(self) -> crate::api::crypto::RsHashFileEvent {
|
||||
self
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::api::http::RsHttpClientError {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
match self {
|
||||
@@ -6029,6 +6094,18 @@ impl SseEncode for StreamSink<Vec<u8>, flutter_rust_bridge::for_generated::SseCo
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode
|
||||
for StreamSink<
|
||||
crate::api::crypto::RsHashFileEvent,
|
||||
flutter_rust_bridge::for_generated::SseCodec,
|
||||
>
|
||||
{
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
unimplemented!("")
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode
|
||||
for StreamSink<
|
||||
crate::api::multicast::RsMulticastDiscovered,
|
||||
@@ -6575,6 +6652,25 @@ impl SseEncode for crate::api::http::ResultWithPublicKeyRegisterResponseDto {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::api::crypto::RsHashFileEvent {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
match self {
|
||||
crate::api::crypto::RsHashFileEvent::Progress { bytes } => {
|
||||
<i32>::sse_encode(0, serializer);
|
||||
<u64>::sse_encode(bytes, serializer);
|
||||
}
|
||||
crate::api::crypto::RsHashFileEvent::Done { hash } => {
|
||||
<i32>::sse_encode(1, serializer);
|
||||
<String>::sse_encode(hash, serializer);
|
||||
}
|
||||
_ => {
|
||||
unimplemented!("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::api::http::RsHttpClientError {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ anyhow = "1.0.95"
|
||||
axum = { version = "0.8.1", features = ["ws"] }
|
||||
base64 = "0.22.1"
|
||||
futures-util = "0.3.31"
|
||||
localsend = { path = "../core" }
|
||||
localsend = { path = "../packages/core" }
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde_json = "1.0.135"
|
||||
tokio = { version = "1.43.0", features = ["full"] }
|
||||
|
||||
Reference in New Issue
Block a user