//! `cachedir` is a tiny utility for tagging, finding, and untagging //! directories as cache directories, according to the //! specification. //! //! ~~~sh //! $ cachedir find $HOME //! $ cachedir tag $HOME/.cache //! $ cachedir find $HOME //! /home/liw/.cache //! $ cachedir untag $HOME/.cache //! $ cachedir find $HOME //! $ //! ~~~ use clap::{Parser, Subcommand}; use std::fs::{read, remove_file, write}; use std::path::{Path, PathBuf}; use walkdir::{DirEntry, WalkDir}; /// Name of tag file. const CACHEDIR_TAG: &str = "CACHEDIR.TAG"; /// Prefix of contents of tag file. const TAG: &str = "Signature: 8a477f597d28d172789f06886806bc55"; /// The main program. fn main() { if let Err(e) = real_main() { eprintln!("ERROR: {}", e); std::process::exit(1); } } /// Fallible main program. fn real_main() -> anyhow::Result<()> { let args = Args::parse(); match args.cmd { Command::Find { dirs } => walk(&dirs, find)?, Command::Tag { dirs } => walk(&dirs, tag)?, Command::Untag { dirs } => walk(&dirs, untag)?, Command::IsCache { dir } => is_cache(dir)?, } Ok(()) } /// Command line arguments. #[derive(Debug, Parser)] struct Args { #[clap(subcommand)] cmd: Command, } /// Subcommands. #[derive(Debug, Subcommand)] enum Command { /// Find cache directories starting at given directories. Find { dirs: Vec }, /// Add cache directory tag to given directories. Tag { dirs: Vec }, /// Remove cache directory tag from given directories. Untag { dirs: Vec }, /// Is the single given directory tagged as a cache directory? IsCache { dir: PathBuf }, } /// Is the given directory a cache? fn is_cache(dir: PathBuf) -> Result<(), std::io::Error> { let filename = dir.join(CACHEDIR_TAG); if !is_cachedir_tag(&filename)? { std::process::exit(1); } Ok(()) } /// Is the given directory a cache? fn find(entry: &DirEntry) -> Result<(), std::io::Error> { let filename = tag_filename(entry); if is_cachedir_tag(&filename)? { println!("{}", entry.path().display()); } Ok(()) } /// Add a tag file to the given directory. fn tag(entry: &DirEntry) -> Result<(), std::io::Error> { let filename = tag_filename(entry); if !is_cachedir_tag(&filename)? { write(filename, TAG)?; } Ok(()) } /// Remove a tag file from the given directory. fn untag(entry: &DirEntry) -> Result<(), std::io::Error> { let filename = tag_filename(entry); if is_cachedir_tag(&filename)? { remove_file(filename)?; } Ok(()) } /// Iterate over all directories, call given function for each. fn walk(dirs: &[PathBuf], mut func: F) -> Result<(), std::io::Error> where F: FnMut(&DirEntry) -> Result<(), std::io::Error>, { for dir in dirs { for entry in WalkDir::new(dir) { let entry = entry?; if entry.metadata()?.is_dir() { func(&entry)?; } } } Ok(()) } /// Is the given file a cache directory tag? fn is_cachedir_tag(filename: &Path) -> Result { if filename.exists() { let data = read(filename)?; if data.len() >= TAG.len() { Ok(&data[..TAG.len()] == TAG.as_bytes()) } else { Ok(false) } } else { Ok(false) } } /// Return path to tag file in a directory. fn tag_filename(entry: &DirEntry) -> PathBuf { entry.path().join(CACHEDIR_TAG) }