use crate::error::Result;
use rayon::prelude::*;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::PathBuf;
pub fn calculate_s3_etag(file_path: PathBuf, part_size: u64) -> Result<String> {
let file = File::open(&file_path)?;
let file_size = file.metadata()?.len();
if file_size == 0 {
return Ok(format!("{:x}", md5::compute(b"")));
}
if file_size <= part_size {
let mut reader = File::open(&file_path)?;
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
let digest = md5::compute(&buffer);
Ok(format!("{:x}", digest))
} else {
let num_parts = (file_size + part_size - 1) / part_size;
let part_hashes: Result<Vec<Vec<u8>>> = (0..num_parts)
.into_par_iter()
.map(|part_num| -> Result<Vec<u8>> {
let mut file = File::open(&file_path)?;
let offset = part_num * part_size;
let bytes_to_read = std::cmp::min(part_size, file_size - offset);
let mut buffer = vec![0; bytes_to_read as usize];
file.seek(SeekFrom::Start(offset))?;
file.read_exact(&mut buffer)?;
let digest = md5::compute(&buffer);
Ok(digest.0.to_vec())
})
.collect();
let combined_hashes: Vec<u8> = part_hashes?.into_iter().flatten().collect();
let final_digest = md5::compute(&combined_hashes);
Ok(format!("{:x}-{}", final_digest, num_parts))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
const MB: u64 = 1024 * 1024;
#[test]
fn test_etag_empty_file() {
let file = NamedTempFile::new().unwrap();
let etag = calculate_s3_etag(file.path().to_path_buf(), 5 * MB).unwrap();
assert_eq!(etag, "d41d8cd98f00b204e9800998ecf8427e");
}
#[test]
fn test_etag_single_part() {
let mut file = NamedTempFile::new().unwrap();
let content = b"hello world";
file.write_all(content).unwrap();
let etag = calculate_s3_etag(file.path().to_path_buf(), 5 * MB).unwrap();
let expected_etag = format!("{:x}", md5::compute(content));
assert_eq!(etag, expected_etag);
}
#[test]
fn test_etag_multi_part() {
let part_size = 1 * MB;
let file_size = (2.5 * MB as f64) as usize;
let mut file = NamedTempFile::new().unwrap();
let content = vec![b'a'; file_size];
file.write_all(&content).unwrap();
let part1_md5 = md5::compute(&content[0..(1 * MB as usize)]);
let part2_md5 = md5::compute(&content[(1 * MB as usize)..(2 * MB as usize)]);
let part3_md5 = md5::compute(&content[(2 * MB as usize)..]);
let mut combined_md5s = Vec::new();
combined_md5s.extend_from_slice(&part1_md5.0);
combined_md5s.extend_from_slice(&part2_md5.0);
combined_md5s.extend_from_slice(&part3_md5.0);
let final_md5 = md5::compute(&combined_md5s);
let expected_etag = format!("{:x}-3", final_md5);
let etag = calculate_s3_etag(file.path().to_path_buf(), part_size).unwrap();
assert_eq!(etag, expected_etag);
}
}