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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user