use std::path::{Path, PathBuf};
use crate::paths::ShelfPaths;
use crate::{Error, Result};
pub fn adopt(paths: &ShelfPaths, src: &Path, sha256_hex: &str) -> Result<PathBuf> {
let blob = paths.blob_file(sha256_hex);
if blob.is_file() {
return Ok(blob);
}
let blobs_dir = paths.blobs_dir();
std::fs::create_dir_all(&blobs_dir).map_err(|e| Error::io(&blobs_dir, e))?;
if std::fs::hard_link(src, &blob).is_ok() {
tracing::debug!(src = %src.display(), blob = %blob.display(), "adopted via hardlink");
return Ok(blob);
}
let tmp = blobs_dir.join(format!("sha256-{sha256_hex}.tmp-{}", std::process::id()));
let result = (|| -> Result<()> {
std::fs::copy(src, &tmp).map_err(|e| Error::io(&tmp, e))?;
std::fs::rename(&tmp, &blob).map_err(|e| Error::io(&blob, e))?;
Ok(())
})();
if result.is_err() {
let _ = std::fs::remove_file(&tmp);
}
result?;
tracing::debug!(src = %src.display(), blob = %blob.display(), "adopted via copy");
Ok(blob)
}
pub fn promote_download(paths: &ShelfPaths, part_file: &Path, sha256_hex: &str) -> Result<PathBuf> {
let blob = paths.blob_file(sha256_hex);
if blob.is_file() {
let _ = std::fs::remove_file(part_file);
return Ok(blob);
}
let blobs_dir = paths.blobs_dir();
std::fs::create_dir_all(&blobs_dir).map_err(|e| Error::io(&blobs_dir, e))?;
std::fs::rename(part_file, &blob).map_err(|e| Error::io(&blob, e))?;
Ok(blob)
}
pub fn list_blobs(paths: &ShelfPaths) -> Result<Vec<(String, PathBuf)>> {
let dir = paths.blobs_dir();
let mut out = Vec::new();
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(out),
Err(e) => return Err(Error::io(&dir, e)),
};
for entry in entries.filter_map(|e| e.ok()) {
let name = entry.file_name().to_string_lossy().into_owned();
if let Some(hex) = name.strip_prefix("sha256-") {
if crate::scan::is_sha256_hex(hex) {
out.push((hex.to_owned(), entry.path()));
}
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adopt_hardlinks_on_same_filesystem() {
let tmp = tempfile::tempdir().unwrap();
let paths = ShelfPaths::at(tmp.path().join("shelf"));
paths.ensure_layout().unwrap();
let src = tmp.path().join("model.gguf");
std::fs::write(&src, b"CONTENT").unwrap();
let blob = adopt(&paths, &src, "ab".repeat(32).as_str()).unwrap();
assert!(blob.is_file());
#[cfg(unix)]
assert!(crate::identity::same_inode(&src, &blob));
assert_eq!(adopt(&paths, &src, "ab".repeat(32).as_str()).unwrap(), blob);
}
#[test]
fn promote_moves_part_into_store() {
let tmp = tempfile::tempdir().unwrap();
let paths = ShelfPaths::at(tmp.path().join("shelf"));
paths.ensure_layout().unwrap();
let part = paths.downloads_dir().join("x.part");
std::fs::write(&part, b"DL").unwrap();
let blob = promote_download(&paths, &part, "cd".repeat(32).as_str()).unwrap();
assert!(blob.is_file());
assert!(!part.exists());
assert_eq!(std::fs::read(&blob).unwrap(), b"DL");
}
}