use std::fs;
use std::path::{Path, PathBuf};
use crate::tensor::{Result, TensorError};
pub fn data_cache_dir() -> PathBuf {
if let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home).join(".flodl").join("data");
}
let tmp = std::env::temp_dir().join("flodl-data");
eprintln!(
"flodl data: HOME unset, caching datasets under {} — on a tmpfs /tmp \
this spends RAM, not disk. Set HOME, or pre-provision the source root.",
tmp.display(),
);
tmp
}
pub fn resolve_cached(
source_root: &Path,
subdir: &str,
file_name: &str,
valid: impl Fn(&Path) -> bool,
fetch: impl FnOnce(&Path) -> Result<()>,
) -> Result<PathBuf> {
let from_source = source_root.join(subdir).join(file_name);
if valid(&from_source) {
return Ok(from_source);
}
let dir = data_cache_dir().join(subdir);
let cached = dir.join(file_name);
if valid(&cached) {
return Ok(cached);
}
ensure_dir(&dir)?;
fetch(&cached)?;
Ok(cached)
}
pub fn publish_atomically(
dest: &Path,
write: impl FnOnce(&mut fs::File) -> Result<()>,
) -> Result<()> {
let name = dest
.file_name()
.and_then(std::ffi::OsStr::to_str)
.ok_or_else(|| {
TensorError::new(&format!(
"flodl data: publish target has no file name: {}",
dest.display()
))
})?;
let tmp = dest.with_file_name(format!("{name}.{}.part", std::process::id()));
let staged = (|| {
let mut f = fs::File::create(&tmp).map_err(|e| write_error(&tmp, &e))?;
write(&mut f)?;
f.sync_all()
.map_err(|e| TensorError::new(&format!("sync {}: {e}", tmp.display())))
})();
if let Err(e) = staged {
let _ = fs::remove_file(&tmp);
return Err(e);
}
fs::rename(&tmp, dest).map_err(|e| {
let _ = fs::remove_file(&tmp);
write_error(dest, &e)
})
}
pub fn write_error(path: &Path, e: &std::io::Error) -> TensorError {
TensorError::new(&format!(
"write {}: {e}\n \
this dataset path cannot be written. Either provision it from a host \
that can write it (and leave this one reading), or point the dataset \
source at a writable path with room for the data.",
path.display(),
))
}
fn ensure_dir(dir: &Path) -> Result<()> {
if dir.is_dir() {
return Ok(());
}
fs::create_dir_all(dir).map_err(|e| write_error(dir, &e))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_valid_source_file_is_used_where_it_is() {
let root = std::env::temp_dir().join(format!("flodl-hc-src-{}", std::process::id()));
let dir = root.join("sub");
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("f.bin"), b"abc").unwrap();
let got = resolve_cached(
&root,
"sub",
"f.bin",
|p| p.exists(),
|_| panic!("must not fetch when the source root already has it"),
)
.unwrap();
assert_eq!(got, dir.join("f.bin"));
fs::remove_dir_all(&root).unwrap();
}
#[test]
fn an_invalid_source_file_falls_through_to_the_fetch() {
let root = std::env::temp_dir().join(format!("flodl-hc-inv-{}", std::process::id()));
let dir = root.join("sub");
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("f.bin"), b"short").unwrap();
let sub = format!("flodl-hc-test-{}", std::process::id());
let cache = data_cache_dir().join(&sub);
let _ = fs::remove_dir_all(&cache);
let right_size = |p: &Path| fs::metadata(p).map(|m| m.len() == 3).unwrap_or(false);
let got = resolve_cached(&root, &sub, "f.bin", right_size, |dst| {
publish_atomically(dst, |f| {
use std::io::Write;
f.write_all(b"abc").map_err(|e| write_error(dst, &e))
})
})
.unwrap();
assert!(
got.starts_with(data_cache_dir()),
"fetched copy must land in the cache: {got:?}"
);
assert_eq!(fs::read(&got).unwrap(), b"abc");
fs::remove_dir_all(&root).unwrap();
let _ = fs::remove_dir_all(&cache);
}
#[test]
fn acquisition_never_writes_under_the_source_root() {
let root = std::env::temp_dir().join(format!("flodl-hc-ro-{}", std::process::id()));
fs::create_dir_all(&root).unwrap();
let sub = format!("flodl-hc-ro-{}", std::process::id());
let cache = data_cache_dir().join(&sub);
let _ = fs::remove_dir_all(&cache);
let got = resolve_cached(
&root,
&sub,
"f.bin",
|p| p.exists(),
|dst| {
publish_atomically(dst, |f| {
use std::io::Write;
f.write_all(b"fetched").map_err(|e| write_error(dst, &e))
})
},
)
.unwrap();
assert_eq!(fs::read(&got).unwrap(), b"fetched");
let under_source: Vec<_> = fs::read_dir(&root)
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
assert!(
under_source.is_empty(),
"the source root must be untouched, found {under_source:?}"
);
fs::remove_dir_all(&root).unwrap();
let _ = fs::remove_dir_all(&cache);
}
#[test]
fn a_failed_publish_leaves_neither_destination_nor_temp() {
let dir = std::env::temp_dir().join(format!("flodl-hc-fail-{}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
let dest = dir.join("f.bin");
let err = publish_atomically(&dest, |f| {
use std::io::Write;
f.write_all(b"partial").unwrap();
Err(TensorError::new("simulated mid-stream failure"))
})
.unwrap_err();
assert!(err.to_string().contains("simulated"), "got: {err}");
assert!(
!dest.exists(),
"destination must not exist after a failed publish"
);
let leftovers: Vec<_> = fs::read_dir(&dir)
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
assert!(
leftovers.is_empty(),
"temp must be cleaned up, found {leftovers:?}"
);
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_completed_publish_is_byte_exact() {
let dir = std::env::temp_dir().join(format!("flodl-hc-ok-{}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
let dest = dir.join("f.bin");
publish_atomically(&dest, |f| {
use std::io::Write;
f.write_all(&[7u8; 4096])
.map_err(|e| write_error(&dest, &e))
})
.unwrap();
assert_eq!(fs::read(&dest).unwrap(), vec![7u8; 4096]);
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn write_error_names_both_fixes() {
let e = std::io::Error::other("read-only file system");
let msg = write_error(Path::new("/flodl/data/x"), &e).to_string();
assert!(msg.contains("provision it from a host"), "got: {msg}");
assert!(msg.contains("writable path"), "got: {msg}");
}
}