use std::{collections::HashMap, fs, path::Path};
pub(crate) struct FileState {
pub key: String,
pub stamp: String,
}
pub(super) fn states(root: &Path, paths: &[&str]) -> Result<HashMap<String, FileState>, String> {
let workers = if cfg!(windows) {
std::thread::available_parallelism()
.map(usize::from)
.unwrap_or(1)
.min(8)
.min(paths.len() / 256)
.max(1)
} else {
1
};
let read = |chunk: &[&str]| {
chunk
.iter()
.map(|path| (String::from(*path), state(&root.join(path))))
.collect::<HashMap<_, _>>()
};
if workers == 1 {
return Ok(read(paths));
}
std::thread::scope(|scope| {
let mut jobs = Vec::with_capacity(workers);
let mut result = HashMap::with_capacity(paths.len());
for chunk in paths.chunks(paths.len().div_ceil(workers)) {
match std::thread::Builder::new()
.name("tano-file-state".into())
.spawn_scoped(scope, move || read(chunk))
{
Ok(job) => jobs.push(job),
Err(_) => result.extend(read(chunk)),
}
}
for job in jobs {
result.extend(
job.join()
.map_err(|_| "读取文件状态的后台任务失败".to_string())?,
);
}
Ok(result)
})
}
#[cfg(windows)]
pub(crate) fn state(path: &Path) -> FileState {
use std::os::windows::{fs::OpenOptionsExt, io::AsRawHandle};
use windows_sys::Win32::Storage::FileSystem::{
FileBasicInfo, GetFileInformationByHandle, GetFileInformationByHandleEx,
BY_HANDLE_FILE_INFORMATION, FILE_BASIC_INFO, FILE_FLAG_BACKUP_SEMANTICS,
FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_DELETE, FILE_SHARE_READ,
};
let empty = || FileState {
key: String::new(),
stamp: String::new(),
};
let Ok(file) = fs::OpenOptions::new()
.read(true)
.share_mode(FILE_SHARE_READ | FILE_SHARE_DELETE)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)
else {
return empty();
};
let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
if unsafe { GetFileInformationByHandle(file.as_raw_handle() as _, &mut info) } == 0 {
return empty();
}
let key = format!(
"{}:{}:{}",
info.dwVolumeSerialNumber, info.nFileIndexHigh, info.nFileIndexLow
);
let mut basic: FILE_BASIC_INFO = unsafe { std::mem::zeroed() };
let reliable = unsafe {
GetFileInformationByHandleEx(
file.as_raw_handle() as _,
FileBasicInfo,
&mut basic as *mut _ as _,
std::mem::size_of::<FILE_BASIC_INFO>() as u32,
)
} != 0
&& basic.ChangeTime > 0
&& (info.nFileIndexHigh != 0 || info.nFileIndexLow != 0);
let stamp = if reliable {
format!(
"win1:{key}:{}:{}:{}:{}:{}",
basic.CreationTime,
basic.LastWriteTime,
basic.ChangeTime,
info.nFileSizeHigh,
info.nFileSizeLow
)
} else {
String::new()
};
FileState { key, stamp }
}
#[cfg(unix)]
pub(crate) fn state(path: &Path) -> FileState {
use std::os::unix::fs::MetadataExt;
let Ok(meta) = fs::symlink_metadata(path) else {
return FileState {
key: String::new(),
stamp: String::new(),
};
};
let key = format!("{}:{}", meta.dev(), meta.ino());
let stamp = format!(
"unix1:{key}:{}:{}:{}:{}:{}",
meta.len(),
meta.mtime(),
meta.mtime_nsec(),
meta.ctime(),
meta.ctime_nsec()
);
FileState { key, stamp }
}
#[cfg(not(any(windows, unix)))]
pub(crate) fn state(_path: &Path) -> FileState {
FileState {
key: String::new(),
stamp: String::new(),
}
}
pub(super) fn unchanged(before: &FileState, after: &FileState) -> bool {
!before.stamp.is_empty() && before.stamp == after.stamp
}
#[cfg(test)]
thread_local! { static CONTENT_READS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) }; }
#[cfg(test)]
pub(super) fn content_read() {
CONTENT_READS.with(|count| count.set(count.get() + 1));
}
#[cfg(test)]
pub(super) fn take_content_reads() -> usize {
CONTENT_READS.with(|count| count.replace(0))
}