use std::collections::HashMap;
use anyhow::Result;
use camino::{Utf8Path, Utf8PathBuf};
use cap_std_ext::cap_std::fs::Dir;
use indexmap::IndexMap;
use super::{ComponentId, ComponentInfo, ComponentsRepo, FileMap, FileType};
const MIN_SIZE: u64 = 1024 * 1024;
const REPO_NAME: &str = "bigfiles";
pub struct BigfilesRepo {
components: IndexMap<String, u64>,
path_to_component: HashMap<Utf8PathBuf, ComponentId>,
mtime_clamp: u64,
}
impl BigfilesRepo {
pub fn load(files: &FileMap, mtime_clamp: u64) -> Option<Self> {
let mut components: IndexMap<String, u64> = IndexMap::new();
let mut path_to_component: HashMap<Utf8PathBuf, ComponentId> = HashMap::new();
let mut inode_to_paths: HashMap<u64, Vec<&Utf8PathBuf>> = HashMap::new();
for (path, file_info) in files {
if file_info.file_type == FileType::File
&& file_info.nlink > 1
&& file_info.size >= MIN_SIZE
{
inode_to_paths.entry(file_info.ino).or_default().push(path);
}
}
for (path, file_info) in files {
if file_info.file_type != FileType::File || file_info.size < MIN_SIZE {
continue;
}
if file_info.nlink > 1 && !inode_to_paths.contains_key(&file_info.ino) {
continue;
}
let filename = path
.file_name()
.map(|s| s.to_string())
.expect("filename has no basename");
let component_name = if components.contains_key(&filename) {
path.strip_prefix("/")
.expect("non-absolute file path in FileMap")
.as_str()
.to_string()
} else {
filename
};
let (idx, _) = components.insert_full(component_name, file_info.mtime);
tracing::trace!(path = %path, component = %components[idx], id = idx, "bigfiles component created");
let component_id = ComponentId(idx);
path_to_component.insert(path.clone(), component_id);
if let Some(linked_paths) = inode_to_paths.remove(&file_info.ino) {
for linked_path in linked_paths {
path_to_component.insert(linked_path.clone(), component_id);
}
}
}
if components.is_empty() {
return None;
}
tracing::debug!(
components = components.len(),
paths = path_to_component.len(),
"loaded bigfiles components"
);
Some(Self {
components,
path_to_component,
mtime_clamp,
})
}
}
impl ComponentsRepo for BigfilesRepo {
fn name(&self) -> &'static str {
REPO_NAME
}
fn default_priority(&self) -> usize {
80
}
fn weak_claims_for_path(
&self,
_rootfs: &Dir,
path: &Utf8Path,
_file_info: &super::FileInfo,
) -> Result<Vec<ComponentId>> {
Ok(self
.path_to_component
.get(path)
.map(|id| vec![*id])
.unwrap_or_default())
}
fn component_info(&self, id: ComponentId) -> ComponentInfo<'_> {
let (name, &mtime) = self
.components
.get_index(id.0)
.expect("invalid ComponentId");
let mtime_clamp = mtime.min(self.mtime_clamp);
ComponentInfo {
name,
mtime_clamp,
stability: 0.0,
}
}
}
#[cfg(test)]
mod tests {
use cap_std_ext::cap_std::ambient_authority;
use cap_std_ext::cap_std::fs::Dir;
use super::*;
fn setup_rootfs<F>(setup: F) -> (tempfile::TempDir, Dir, FileMap)
where
F: FnOnce(&Dir),
{
let tmp = tempfile::tempdir().unwrap();
let rootfs = Dir::open_ambient_dir(tmp.path(), ambient_authority()).unwrap();
setup(&rootfs);
let files = crate::scan::Scanner::new(&rootfs).scan().unwrap();
(tmp, rootfs, files)
}
fn create_sparse_file(rootfs: &Dir, path: &str, size: u64) {
let file = rootfs.create(path).unwrap();
file.set_len(size).unwrap();
}
fn fi(file_type: FileType) -> crate::components::FileInfo {
crate::components::FileInfo::dummy(file_type)
}
fn assert_component(repo: &BigfilesRepo, rootfs: &Dir, path: &str, expected: &str) {
let claims = repo
.weak_claims_for_path(rootfs, Utf8Path::new(path), &fi(FileType::File))
.unwrap();
assert_eq!(claims.len(), 1, "{path} should have exactly one claim");
assert_eq!(
repo.component_info(claims[0]).name,
expected,
"{path} should be claimed by {expected}"
);
}
#[test]
fn test_bigfiles_claims_large_files() {
let tmp = tempfile::tempdir().unwrap();
let rootfs = Dir::open_ambient_dir(tmp.path(), ambient_authority()).unwrap();
rootfs.create_dir_all("usr/bin").unwrap();
rootfs.write("usr/bin/small", "x").unwrap();
rootfs.create_dir_all("usr/lib/modules").unwrap();
create_sparse_file(&rootfs, "usr/lib/modules/initramfs.img", 118 * 1024 * 1024);
rootfs
.create_dir_all("usr/lib/sysimage/rpm-ostree-base-db")
.unwrap();
rootfs.create_dir_all("usr/share/rpm").unwrap();
create_sparse_file(
&rootfs,
"usr/lib/sysimage/rpm-ostree-base-db/rpmdb.sqlite",
26 * 1024 * 1024,
);
std::fs::hard_link(
tmp.path()
.join("usr/lib/sysimage/rpm-ostree-base-db/rpmdb.sqlite"),
tmp.path().join("usr/share/rpm/rpmdb.sqlite"),
)
.unwrap();
let rootfs = Dir::open_ambient_dir(tmp.path(), ambient_authority()).unwrap();
let files = crate::scan::Scanner::new(&rootfs).scan().unwrap();
let repo = BigfilesRepo::load(&files, 0).unwrap();
let claims = repo
.weak_claims_for_path(
&rootfs,
Utf8Path::new("/usr/bin/small"),
&fi(FileType::File),
)
.unwrap();
assert!(claims.is_empty());
assert_component(
&repo,
&rootfs,
"/usr/lib/modules/initramfs.img",
"initramfs.img",
);
assert_component(
&repo,
&rootfs,
"/usr/lib/sysimage/rpm-ostree-base-db/rpmdb.sqlite",
"rpmdb.sqlite",
);
assert_component(
&repo,
&rootfs,
"/usr/share/rpm/rpmdb.sqlite",
"rpmdb.sqlite",
);
assert_eq!(repo.components.len(), 2);
}
#[test]
fn test_bigfiles_duplicate_filenames() {
let (_tmp, rootfs, files) = setup_rootfs(|rootfs| {
rootfs.create_dir("a").unwrap();
rootfs.create_dir("b").unwrap();
create_sparse_file(rootfs, "a/foobar", 4 * 1024 * 1024);
create_sparse_file(rootfs, "b/foobar", 4 * 1024 * 1024);
});
let repo = BigfilesRepo::load(&files, 0).unwrap();
assert_component(&repo, &rootfs, "/a/foobar", "foobar");
assert_component(&repo, &rootfs, "/b/foobar", "b/foobar");
}
}