use std::fs::{File, OpenOptions};
use std::os::windows::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
const FILE_SHARE_READ: u32 = 0x0000_0001;
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
pub struct Locks {
held: Vec<File>,
pub files: usize,
pub directories: usize,
}
pub fn acquire(pinned: &[PathBuf], protected: &[PathBuf]) -> Result<Locks> {
let mut locks = Locks {
held: Vec::new(),
files: 0,
directories: 0,
};
for path in pinned {
if let Ok(handle) = directory(path) {
locks.held.push(handle);
locks.directories += 1;
}
}
for path in protected {
let metadata = std::fs::symlink_metadata(path)
.with_context(|| format!("failed to read {}", path.display()))?;
if metadata.is_dir() {
locks.held.push(
directory(path).with_context(|| format!("failed to lock {}", path.display()))?,
);
locks.directories += 1;
for entry in walk(path) {
if let Ok(handle) = file(&entry) {
locks.held.push(handle);
locks.files += 1;
}
}
} else {
locks.held.push(file(path).with_context(|| {
format!(
"failed to lock {} — something already has it open for writing",
path.display()
)
})?);
locks.files += 1;
}
}
Ok(locks)
}
fn file(path: &Path) -> Result<File> {
Ok(OpenOptions::new()
.read(true)
.share_mode(FILE_SHARE_READ)
.open(path)?)
}
fn directory(path: &Path) -> Result<File> {
Ok(OpenOptions::new()
.read(true)
.share_mode(FILE_SHARE_READ)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(path)?)
}
fn walk(root: &Path) -> Vec<PathBuf> {
let mut found = Vec::new();
let mut pending = vec![root.to_path_buf()];
while let Some(directory) = pending.pop() {
let Ok(entries) = std::fs::read_dir(&directory) else {
continue;
};
for entry in entries.flatten() {
let Ok(kind) = entry.file_type() else {
continue;
};
if kind.is_dir() {
pending.push(entry.path());
} else if kind.is_file() {
found.push(entry.path());
}
}
}
found
}