headless_lms_utils/
folder_checksum.rs#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{fs::Permissions, path::Path};
use crate::prelude::*;
use blake3::Hash;
use futures::StreamExt;
use tokio::{fs::File, io::BufReader};
use tokio_util::io::ReaderStream;
use walkdir::WalkDir;
pub async fn hash_folder(root_path: &Path) -> UtilResult<Hash> {
let mut hasher = blake3::Hasher::new();
let walker = WalkDir::new(root_path)
.follow_links(false)
.max_open(10)
.contents_first(false)
.sort_by_file_name();
for entry in walker {
let entry = entry?;
let metadata = entry.metadata()?;
let file_type = metadata.file_type();
let permissions = metadata.permissions();
let full_path = entry.path();
let directory = file_type.is_dir();
let file = file_type.is_file();
let symlink = file_type.is_symlink();
let permissions_mode = determine_permissions_mode_for_hashing(&permissions);
let relative_path = full_path.strip_prefix(root_path)?;
let serialized_metadata = format!(
"-{}{}{}{}{:?}-",
directory as u8, file as u8, symlink as u8, &permissions_mode, &relative_path
);
hasher.update(serialized_metadata.as_bytes());
if file {
let file = File::open(full_path).await?;
let reader = BufReader::new(file);
let mut stream = ReaderStream::new(reader);
while let Some(chunk) = stream.next().await {
hasher.update(&chunk?);
}
}
if symlink {
let res = tokio::fs::read_link(full_path).await?;
hasher.update(res.display().to_string().as_bytes());
}
}
let hash = hasher.finalize();
Ok(hash)
}
fn determine_permissions_mode_for_hashing(permissions: &Permissions) -> u32 {
if cfg!(unix) {
return permissions.mode();
}
if permissions.readonly() {
0o444
} else {
0o644
}
}
#[cfg(test)]
mod tests {
use tempdir::TempDir;
use tokio::{
fs::{self, create_dir, remove_dir, symlink},
io::AsyncWriteExt,
};
use super::*;
async fn do_the_test() {
let dir = TempDir::new("test-folder-checksum").expect("Failed to create a temp dir");
File::open(dir.path())
.await
.unwrap()
.set_permissions(Permissions::from_mode(0o755))
.await
.unwrap();
let first_hash = hash_folder(dir.path()).await.unwrap();
assert_eq!(
first_hash.to_hex().to_string(),
"01444ae9678097d0214e449568b68eb351c4743b2697bfc3d517b5c601535823"
);
let mut file = File::create(dir.path().join("test-file")).await.unwrap();
file.set_permissions(Permissions::from_mode(0o644))
.await
.unwrap();
file.write_all(b"Test file").await.unwrap();
let second_hash = hash_folder(dir.path()).await.unwrap();
assert_eq!(
second_hash.to_hex().to_string(),
"c2f4caaaafeb41dfd5e5381ea9c1583ccaa7d09378745def8c979b1e1f0e5c2a"
);
fs::set_permissions(dir.path().join("test-file"), Permissions::from_mode(0o444))
.await
.unwrap();
let third_hash = hash_folder(dir.path()).await.unwrap();
assert_eq!(
third_hash.to_hex().to_string(),
"1b1820abcb400974e0eb751c103303864f7b0ae7ad387c5135521d9968dbb4de"
);
let inner_dir_path = dir.path().join("directory");
create_dir(&inner_dir_path).await.unwrap();
File::open(inner_dir_path)
.await
.unwrap()
.set_permissions(Permissions::from_mode(0o755))
.await
.unwrap();
let fourth_hash = hash_folder(dir.path()).await.unwrap();
assert_eq!(
fourth_hash.to_hex().to_string(),
"f1113337a98c5fe5d7ed0f2a9fc17490993b1149ea44784a027e53d1a1884c9e"
);
remove_dir(&dir.path().join("directory")).await.unwrap();
let fifth_hash = hash_folder(dir.path()).await.unwrap();
assert_eq!(
fifth_hash.to_hex().to_string(),
"1b1820abcb400974e0eb751c103303864f7b0ae7ad387c5135521d9968dbb4de"
);
let file = File::create(&dir.path().join("directory")).await.unwrap();
file.set_permissions(Permissions::from_mode(0o755))
.await
.unwrap();
let sixth_hash = hash_folder(dir.path()).await.unwrap();
assert_ne!(
fifth_hash.to_hex().to_string(),
sixth_hash.to_hex().to_string()
);
assert_eq!(
sixth_hash.to_hex().to_string(),
"4b9255096a4b233be4a24b0fb74fa5e955a0261a422c8e9cfbe7ac11f1256030"
);
let symlink_path = &dir.path().join("symlink");
symlink(Path::new("directory"), &symlink_path)
.await
.unwrap();
File::open(symlink_path)
.await
.unwrap()
.set_permissions(Permissions::from_mode(0o644))
.await
.unwrap();
let seventh_hash = hash_folder(dir.path()).await.unwrap();
assert_eq!(
seventh_hash.to_hex().to_string(),
"5144015ff90807ec6448a0b6bfcc470de495182441e0af019c6483da8edaa05c"
);
}
#[cfg(not(target_os = "windows"))]
#[tokio::test]
async fn it_works() {
let res = std::panic::catch_unwind(|| {
futures::executor::block_on(do_the_test());
});
if res.is_ok() {
return;
}
warn!("First attempt at the folder checksum test failed. Retrying in case there was a file corruption issue on this machine.");
do_the_test().await;
}
}