use std::io;
use std::path::{Component, Path, PathBuf, Prefix};
use std::{fs, path};
const PROJECT_SCRATCH_DIR: &str = ".webui-press-cache";
pub(crate) fn scratch_base(build_dir: &Path, project_dir: &Path) -> io::Result<PathBuf> {
resolve_scratch_base(build_dir, project_dir, &std::env::temp_dir())
}
fn resolve_scratch_base(
build_dir: &Path,
project_dir: &Path,
system_temp: &Path,
) -> io::Result<PathBuf> {
let build = path::absolute(build_dir)?;
let system = path::absolute(system_temp)?;
if same_volume(&system, &build) {
return Ok(system);
}
let local = path::absolute(project_dir)?.join(PROJECT_SCRATCH_DIR);
prepare_project_scratch(&local)?;
Ok(local)
}
fn prepare_project_scratch(directory: &Path) -> io::Result<()> {
fs::create_dir_all(directory)?;
let ignore = directory.join(".gitignore");
if ignore.exists() {
return Ok(());
}
fs::write(ignore, b"*\n")
}
fn same_volume(left: &Path, right: &Path) -> bool {
volume_key(left) == volume_key(right)
}
fn volume_key(path: &Path) -> Option<String> {
let Some(Component::Prefix(prefix)) = path.components().next() else {
return None;
};
Some(match prefix.kind() {
Prefix::Disk(drive) | Prefix::VerbatimDisk(drive) => {
char::from(drive).to_ascii_lowercase().to_string()
}
Prefix::UNC(server, share) | Prefix::VerbatimUNC(server, share) => format!(
r"\\{}\{}",
server.to_string_lossy().to_ascii_lowercase(),
share.to_string_lossy().to_ascii_lowercase()
),
Prefix::DeviceNS(name) | Prefix::Verbatim(name) => {
name.to_string_lossy().to_ascii_lowercase()
}
})
}
#[cfg(test)]
mod tests {
use super::*;
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn unique_dir(name: &str) -> PathBuf {
std::env::temp_dir().join(format!("webui-press-scratch-{name}-{}", std::process::id()))
}
#[test]
fn same_volume_matches_identical_roots() {
if cfg!(windows) {
assert!(same_volume(
Path::new(r"C:\Users\me\AppData\Local\Temp"),
Path::new(r"C:\repos\site")
));
assert!(same_volume(Path::new(r"c:\lower"), Path::new(r"C:\UPPER")));
assert!(same_volume(
Path::new(r"\\?\E:\repos\site"),
Path::new(r"E:\repos\site")
));
} else {
assert!(same_volume(Path::new("/tmp"), Path::new("/home/me/site")));
}
}
#[cfg(windows)]
#[test]
fn same_volume_separates_distinct_windows_volumes() {
assert!(!same_volume(
Path::new(r"C:\Users\me\AppData\Local\Temp"),
Path::new(r"E:\repos\site")
));
assert!(!same_volume(
Path::new(r"\\server\share\site"),
Path::new(r"C:\repos\site")
));
assert!(same_volume(
Path::new(r"\\server\share\site"),
Path::new(r"\\SERVER\SHARE\other")
));
}
#[test]
fn same_volume_keeps_system_temp_when_volumes_match() -> TestResult {
let build = unique_dir("same-volume");
fs::create_dir_all(&build)?;
let system = std::env::temp_dir();
let outcome = resolve_scratch_base(&build, &build, &system);
let _ = fs::remove_dir_all(&build);
assert_eq!(outcome?, path::absolute(&system)?);
Ok(())
}
#[cfg(windows)]
#[test]
fn cross_volume_system_temp_falls_back_to_project_scratch() -> TestResult {
let project = unique_dir("cross-volume");
fs::create_dir_all(&project)?;
let foreign_temp = Path::new(r"\\webui-press-test\temp");
let outcome = resolve_scratch_base(&project, &project, foreign_temp);
let resolved = match outcome {
Ok(resolved) => resolved,
Err(error) => {
let _ = fs::remove_dir_all(&project);
return Err(error.into());
}
};
let expected = path::absolute(&project)?.join(PROJECT_SCRATCH_DIR);
let ignored = fs::read_to_string(resolved.join(".gitignore"));
let _ = fs::remove_dir_all(&project);
assert_eq!(resolved, expected);
assert_eq!(ignored?, "*\n");
Ok(())
}
#[cfg(windows)]
#[test]
fn output_volume_decides_the_base_not_the_config_directory() -> TestResult {
let system = std::env::temp_dir();
let build = system.join("webui-press-output-volume");
let project = Path::new(r"\\webui-press-test\project");
assert_eq!(
resolve_scratch_base(&build, project, &system)?,
path::absolute(&system)?
);
Ok(())
}
#[test]
fn project_scratch_preparation_is_idempotent() -> TestResult {
let directory = unique_dir("idempotent");
let _ = fs::remove_dir_all(&directory);
let outcome: TestResult = (|| {
prepare_project_scratch(&directory)?;
fs::write(directory.join(".gitignore"), b"custom\n")?;
prepare_project_scratch(&directory)?;
assert_eq!(
fs::read_to_string(directory.join(".gitignore"))?,
"custom\n"
);
Ok(())
})();
let _ = fs::remove_dir_all(&directory);
outcome
}
}