use std::{
collections::{HashMap, HashSet},
fs::{self, hard_link},
hash::{DefaultHasher, Hash, Hasher},
io,
ops::Deref,
os::unix::fs::symlink,
path::{Path, PathBuf},
sync::Arc,
};
use tracing::{info, trace};
use walkdir::WalkDir;
use crate::CacheKey;
use super::SnapStorage;
type Hashed = String;
#[derive(Debug, Clone)]
pub struct SnapFs {
pub blob_path: Arc<PathBuf>,
}
impl SnapStorage for SnapFs {
fn save_on_gen(
&self,
key: &str,
prev_generation: Option<&str>,
item_hash: &str,
) -> crate::Hashed {
let item_path = full_blob_path(&self.blob_path, item_hash);
let top_map = match prev_generation {
Some(prev_gen) => FsDir::load(self.blob_path.clone(), prev_gen.to_owned()).unwrap(),
None => {
let empty = Dir::new();
empty.persist(self.blob_path.clone())
}
};
let all: ItemPath = top_map.all_dirs(&key);
let x = all.save_item(key.to_owned(), item_path);
let top_hash = x.first().unwrap().hash.clone();
info!("new generation after item insert: {top_hash}");
top_hash
}
fn all_paths(&self, gen_hashh: &str) -> std::collections::HashSet<Content> {
let path = self.the_full_blob_path(gen_hashh);
let mut output = HashSet::new();
for entry in WalkDir::new(&path).follow_links(true) {
let entry = entry.unwrap();
let path = entry.path();
let c = Content::new(path.to_path_buf());
output.insert(c);
}
output
}
fn get_all(&self, hash: &str) -> HashMap<String, Vec<u8>> {
let path = self.the_full_blob_path(hash);
let mut file_map = HashMap::new();
for entry in WalkDir::new(&path).follow_links(true) {
let entry = entry.unwrap();
let path = entry.path();
if path.is_file() {
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
let contents = fs::read(path).unwrap();
file_map.insert(file_name.to_string(), contents);
}
}
}
file_map
}
fn get_cache(&self, gen_hash: &str, cache_key: &CacheKey) -> Vec<String> {
info!("get cache on hash {gen_hash} for item {cache_key:?}");
let path = self.full_path_leaf(gen_hash, &cache_key.to_string());
dbg!(&path);
let mut out = vec![];
let Ok(entries) = fs::read_dir(&path) else {
dbg!("no cache dir found: {path}");
return vec![];
};
for entry in entries {
out.push(entry.unwrap().file_name().into_string().unwrap());
}
out
}
fn insert_cache(&self, gen_hash: &str, cache_key: &CacheKey, item: &str) -> Hashed {
trace!("inserting cache on gen {gen_hash} item {item}");
let item_blob_path = self.full_path(gen_hash, item);
debug_assert!(item_blob_path.is_file());
let path = FsDir::load(self.blob_path.clone(), gen_hash.to_owned()).unwrap();
let itempath = path.all_dirs(&cache_key.to_string());
itempath
.save_item(item.to_string(), item_blob_path)
.first()
.unwrap()
.hash
.clone()
}
fn remove_cache(&self, gen_hash: &str, cache_key: &CacheKey, item: &str) -> Hashed {
trace!("removing cache on gen {gen_hash} item {item}");
let topdir = FsDir::load(self.blob_path.clone(), gen_hash.to_string()).unwrap();
let all = topdir.all_dirs(&cache_key.to_string());
all.remove_item(item.to_owned()).first().unwrap().get_hash()
}
fn get(&self, hash: &str, key: &str) -> Option<Vec<u8>> {
trace!("try get item: {key} on hash: {hash}");
let path = self.full_path(hash, key);
fs::read(&path).ok()
}
fn save_item(&self, item_hash: &str, item: Vec<u8>) {
let path = self.the_full_blob_path(item_hash);
fs::File::create(&path).unwrap();
fs::write(&path, &item).unwrap();
}
fn remove(&self, gen_hash: &str, key: &str) -> crate::Hashed {
let topdir = FsDir::load(self.blob_path.clone(), gen_hash.to_string()).unwrap();
let all = topdir.all_dirs(key);
all.remove_item(key.to_owned()).first().unwrap().get_hash()
}
}
impl SnapFs {
pub fn new(root: PathBuf) -> Self {
let blob_path = Arc::new(root.join("blobs"));
fs::create_dir_all(&*blob_path).unwrap();
let selv = Self { blob_path };
selv
}
pub fn the_full_blob_path(&self, hash: &str) -> PathBuf {
full_blob_path(&self.blob_path, hash)
}
fn full_path_leaf(&self, _gen: &str, key: &str) -> PathBuf {
info!("get full leaf path of key: {key} on gen : {_gen}");
let mut path = self.the_full_blob_path(_gen);
for cmp in get_key_components(key) {
path = path.join(format!("{cmp}"));
}
path
}
fn full_path(&self, _gen: &str, key: &str) -> PathBuf {
info!("get full path of key: {key} on gen : {_gen}");
let mut path = self.full_path_leaf(_gen, key);
path = path.join(key);
path
}
}
fn get_hash<T: Hash>(item: &T) -> Hashed {
let mut hasher = DefaultHasher::new();
item.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
#[derive(Debug)]
struct FsDir {
blob_path: Arc<PathBuf>,
hash: Hashed,
dir: Dir,
}
impl Deref for FsDir {
type Target = Dir;
fn deref(&self) -> &Self::Target {
&self.dir
}
}
impl FsDir {
fn load(dir_path: Arc<PathBuf>, hash: Hashed) -> Option<Self> {
info!("loading dir: {dir_path:?}");
let path = full_blob_path(&dir_path, &hash);
if !path.exists() {
dbg!(&path);
return None;
}
let mut contents: HashMap<String, Content> = Default::default();
for entry in fs::read_dir(path).unwrap() {
let entry = entry.unwrap();
let name = entry.file_name().into_string().unwrap();
let path = entry.path();
let sympath = match fs::read_link(&path) {
Ok(p) => p,
Err(_e) => path,
};
contents.insert(name, Content::new(sympath));
}
let dir = Dir { contents };
Some(Self {
blob_path: dir_path,
hash,
dir,
})
}
fn into_inner(self) -> Dir {
self.dir
}
fn path(&self) -> PathBuf {
full_blob_path(&self.blob_path, &self.hash)
}
fn all_dirs(self, key: &str) -> ItemPath {
trace!("retrieving full itempath of key: {key}");
let dir_path = self.blob_path.clone();
let mut path = self.path();
let mut out = ItemPath {
blob_path: self.blob_path.clone(),
top_dir: self.into_inner(),
dirs: vec![],
};
for num in get_key_components(key) {
path = path.join(format!("{num}"));
if path.exists() {
let sym = match fs::read_link(&path) {
Ok(sym) => sym,
Err(e) => {
let s = format!("{path:?} {e} {key}");
panic!("{}", s);
}
};
let hash = sym.file_name().unwrap().to_str().unwrap().to_string();
let dir = Self::load(dir_path.clone(), hash.to_owned())
.unwrap()
.into_inner();
out.dirs.push((dir, num));
} else {
let dir = Dir::new();
out.dirs.push((dir, num));
}
}
out
}
}
fn get_key_components(key: &str) -> Vec<String> {
let hash = get_hash(&key.to_string());
let mut out = vec![];
let mut chars = hash.chars();
for _ in 0..3 {
out.push(chars.next().unwrap().to_string());
}
info!("key: {key}, components: {out:?}");
out
}
#[derive(Debug)]
struct ItemPath {
top_dir: Dir,
dirs: Vec<(Dir, String)>,
blob_path: Arc<PathBuf>,
}
impl ItemPath {
fn modify<F: FnOnce(&mut Dir)>(mut self, dir_modifier: F) -> Vec<FsDir> {
let mut out = vec![];
let (item_dir, mut parent_component) = self.dirs.pop().unwrap();
let mut dir = item_dir;
dir_modifier(&mut dir);
let fs_item_dir = dir.persist(self.blob_path.clone());
let mut path = fs_item_dir.path();
out.push(fs_item_dir);
while let Some((dir, cmp)) = self.dirs.pop() {
let mut dir = dir;
dir.insert_dir(format!("{parent_component}"), path.clone());
let fsdir = dir.persist(self.blob_path.clone());
path = fsdir.path();
parent_component = cmp;
out.insert(0, fsdir);
}
let mut top_dir = self.top_dir;
top_dir.insert_dir(format!("{parent_component}"), path);
let fs_top_dir = top_dir.persist(self.blob_path.clone());
out.insert(0, fs_top_dir);
out
}
fn save_item(self, key: String, item_path: PathBuf) -> Vec<FsDir> {
tracing::trace!("inserting item: key: {key}, path: {item_path:?}");
let f: Box<dyn FnOnce(&mut Dir)> =
Box::new(|dir: &mut Dir| match dir.insert_file(key, item_path) {
Some(old) => {
tracing::debug!("previous item: {old:?}")
}
None => {
tracing::trace!("item inserted for first time");
}
});
self.modify(f)
}
fn remove_item(self, key: String) -> Vec<FsDir> {
tracing::trace!("removing item: {key}");
let f: Box<dyn FnOnce(&mut Dir)> =
Box::new(|dir: &mut Dir| match dir.contents.remove(&key) {
Some(old) => {
tracing::debug!("item removed: {old:?}");
}
None => {
tracing::warn!("tried to remove {key}, but it was not present");
}
});
self.modify(f)
}
}
#[derive(Debug, Hash, Eq, PartialEq)]
pub enum Content {
File(PathBuf),
Dir(PathBuf),
}
impl Deref for Content {
type Target = Path;
fn deref(&self) -> &Self::Target {
match self {
Content::File(p) => &p,
Content::Dir(p) => &p,
}
}
}
impl Content {
pub fn new(path: PathBuf) -> Self {
let path = path.canonicalize().unwrap();
if path.is_file() {
Self::File(path)
} else {
Self::Dir(path)
}
}
pub fn delete(self) -> io::Result<()> {
match self {
Content::File(p) => fs::remove_file(&p),
Content::Dir(p) => fs::remove_dir_all(&p),
}
}
fn create_file_reference(&self, link: PathBuf) {
match self {
Self::File(original) => match hard_link(&original, &link) {
Ok(()) => {}
Err(e) => {
dbg!(original, link, e);
}
},
Self::Dir(original) => match symlink(original, link) {
Ok(()) => {}
Err(e) => {
dbg!(e);
}
},
}
}
}
#[derive(Debug)]
struct Dir {
contents: HashMap<String, Content>,
}
impl Deref for Dir {
type Target = HashMap<String, Content>;
fn deref(&self) -> &Self::Target {
&self.contents
}
}
impl Dir {
fn get_hash(&self) -> Hashed {
if self.contents.is_empty() {
get_hash(&())
} else {
let mut hash = Hashed::default();
for (_, val) in self.contents.iter() {
let entry_hash = val.file_name().unwrap().to_str().unwrap();
hash.push_str(&entry_hash);
}
get_hash(&hash)
}
}
fn new() -> Self {
Self {
contents: Default::default(),
}
}
fn insert_dir(&mut self, key: String, path: PathBuf) {
self.contents.insert(key, Content::Dir(path));
}
fn insert_file(&mut self, key: String, path: PathBuf) -> Option<Content> {
self.contents.insert(key, Content::File(path))
}
fn persist(self, dir_path: Arc<PathBuf>) -> FsDir {
let hash = self.get_hash();
let path = full_blob_path(&dir_path, &hash);
fs::create_dir_all(&path).unwrap();
for (name, original) in self.contents.iter() {
let link = path.join(name);
original.create_file_reference(link);
}
FsDir::load(dir_path, hash).unwrap()
}
}
fn full_blob_path(blob_store: &Path, hash: &str) -> PathBuf {
let mut topdir = String::new();
let mut chars = hash.chars();
topdir.push(chars.next().unwrap());
topdir.push(chars.next().unwrap());
let dir = blob_store.join(topdir);
fs::create_dir_all(&dir).ok();
dir.join(hash)
}