char-coal 0.2.3

A command line dictionary
Documentation
use std::{
    collections::hash_map::DefaultHasher,
    fs::{self, File, OpenOptions},
    hash::{Hash, Hasher},
    io,
    path::{Path, PathBuf},
};

#[derive(Clone)]
pub struct Cache {
    cache_dir: PathBuf,
    vault_dir: PathBuf,
    tmp_dir: PathBuf,
}

enum CacheFile {
    Normal(u8, String),
    Absurd(u64),
}

impl CacheFile {
    fn str_hash(s: impl AsRef<str>) -> u64 {
        let mut hasher = DefaultHasher::new();
        s.as_ref().hash(&mut hasher);
        hasher.finish()
    }
    fn generate(s: String) -> Self {
        let hash_num = CacheFile::str_hash(&s);
        if s.contains(" ") || !s.is_ascii() {
            CacheFile::Absurd(hash_num)
        } else {
            CacheFile::Normal((hash_num % 256) as u8, s)
        }
    }
    fn consume(self, cache: &Cache, suffix: &'static str) -> io::Result<PathBuf> {
        match self {
            CacheFile::Normal(dir, file) => {
                let mut path = cache.cache_dir.clone();
                path.push(format!("{:02x}", dir));
                fs::create_dir_all(&path)?;
                path.push(format!("{}.{}", file, suffix));
                Ok(path)
            }
            CacheFile::Absurd(file) => {
                let mut path = cache.vault_dir.clone();
                path.push(format!("{:x}.{}", file, suffix));
                Ok(path)
            }
        }
    }
}

impl Cache {
    pub fn new(cache_dir: PathBuf, vault_dir: PathBuf, tmp_dir: PathBuf) -> Self {
        Self {
            cache_dir,
            vault_dir,
            tmp_dir,
        }
    }

    fn get_file_path(&self, word: impl AsRef<str>, suffix: &'static str) -> io::Result<PathBuf> {
        CacheFile::generate(word.as_ref().to_owned()).consume(&self, suffix)
    }

    pub fn query(&self, word: impl AsRef<str>, suffix: &'static str) -> io::Result<File> {
        let path = self.get_file_path(&word, suffix)?;
        let file = OpenOptions::new().read(true).open(path)?;
        Ok(file)
    }

    pub fn store(&self, word: impl AsRef<str>, suffix: &'static str) -> io::Result<File> {
        let path = self.get_file_path(&word, suffix)?;
        let file = OpenOptions::new().create(true).write(true).open(path)?;
        Ok(file)
    }

    pub fn show(&self) -> &PathBuf {
        &self.cache_dir
    }

    pub fn clean(&self) -> io::Result<()> {
        fs::remove_dir_all(&self.cache_dir)?;
        fs::remove_dir_all(&self.vault_dir)?;

        Ok(())
    }

    fn tilde_expand(dir: impl AsRef<Path>) -> io::Result<PathBuf> {
        let mut path = (dir.as_ref().into_iter().take(1))
            .map(|s| -> io::Result<_> {
                if s == "~" {
                    Ok(directories_next::UserDirs::new()
                        .ok_or(io::Error::from(io::ErrorKind::Unsupported))?
                        .home_dir()
                        .to_path_buf())
                } else {
                    Ok(PathBuf::from(s))
                }
            })
            .collect::<io::Result<PathBuf>>()?;
        for s in dir.as_ref().into_iter().skip(1) {
            path.push(s)
        }
        Ok(path)
    }

    fn ensure_dir(dir: &PathBuf) -> io::Result<()> {
        if (dir.parent())
            .map(|p| if p.exists() { Some(()) } else { None })
            .flatten()
            .is_none()
        {
            println!("Parent dir of target not exist.");
            Err(io::Error::from(io::ErrorKind::NotFound))?
        }
        Ok(())
    }

    pub fn import(&self, dir: PathBuf) -> io::Result<()> {
        fs::remove_dir_all(&self.tmp_dir)?;
        fs::create_dir_all(&self.tmp_dir)?;

        let dir = Self::tilde_expand(dir)?;
        Self::ensure_dir(&dir)?;

        let i_file = File::open(dir)?;
        let mut archive = tar::Archive::new(i_file);
        archive.unpack(&self.tmp_dir)?;

        for direntry in fs::read_dir(&self.tmp_dir)? {
            let direntry = direntry?;
            let src = direntry.path();

            let (src_name, src_suffix) = {
                fn split_file_at_dot(file: String) -> Result<(String, String), io::Error> {
                    if let Some((a, b)) = file.rsplit_once('.') {
                        Ok((a.to_owned(), b.to_owned()))
                    } else {
                        Err(io::Error::from(io::ErrorKind::InvalidInput))?
                    }
                }
                src.file_name()
                    .map(|s| s.to_str().unwrap().to_owned())
                    .map(split_file_at_dot)
                    .ok_or(io::Error::from(io::ErrorKind::InvalidInput))??
            };
            let src_suffix = match src_suffix.as_str() {
                "bin" => "bin",
                "mp3" => "mp3",
                _ => Err(io::Error::from(io::ErrorKind::InvalidInput))?,
            };
            let mut src = OpenOptions::new().read(true).open(src)?;
            let mut dest = self.store(src_name, src_suffix)?;
            io::copy(&mut src, &mut dest)?;
        }
        fs::remove_dir_all(&self.tmp_dir)?;
        Ok(())
    }

    pub fn export(&self, dir: PathBuf) -> io::Result<()> {
        let dir = Self::tilde_expand(dir)?;
        Self::ensure_dir(&dir)?;
        if dir.exists() {
            println!("Target exists.")
        }
        let o_file = File::create(dir)?;
        let mut builder = tar::Builder::new(o_file);
        (self.cache_dir.read_dir()?)
            .flat_map(|sub| -> io::Result<_> {
                let iter = sub?.path().read_dir()?;
                Ok(iter)
            })
            .flatten()
            .flat_map(|file| -> io::Result<_> {
                let p = file?.path();
                Ok(p)
            })
            .try_for_each(|path| -> io::Result<_> {
                builder.append_path_with_name(&path, path.file_name().unwrap())?;
                Ok(())
            })?;
        builder.finish()?;
        directories_next::UserDirs::new().unwrap().home_dir();
        Ok(())
    }
}