use std::{ env, fs, io, path::{Path, PathBuf}, process, time::SystemTime, }; use anyhow::{ Context, bail, }; #[tokio::main] async fn main () -> anyhow::Result <()> { use clap::{Arg, App, SubCommand}; let matches = App::new ("ReactorScram/Lookup") .author ("ReactorScram (Trisha)") .about ("Looks up information about a file, based on its hash") .subcommand (SubCommand::with_name ("browse") .about ("Opens a read-only Lookup file, from Trisha's repo, in a web browser, using `xdg-open`") .arg ( Arg::with_name ("INPUT") .help ("Sets the input file to use") .required (true) ) ) .subcommand (SubCommand::with_name ("edit") .about ("Opens a local Lookup file with $EDITOR") .arg ( Arg::with_name ("INPUT") .help ("Sets the input file to use") .required (true) ) ) .arg ( Arg::with_name ("INPUT") .help ("Sets the input file to use") ) .get_matches (); if let Some (matches) = matches.subcommand_matches ("browse") { let public_repo = "https://six-five-six-four.com/git/reactor/lookup/src/branch/main/lookup_repo"; let input_path = Path::new (matches.value_of ("INPUT").unwrap ()); let b3_hash = hash (input_path)?; let url = format! ("{}/{:?}", public_repo, repo_path (&b3_hash)); process::Command::new ("xdg-open") .arg (url) .status ().context ("while spawning browser")?; return Ok (()); } if let Some (matches) = matches.subcommand_matches ("edit") { let input_path = Path::new (matches.value_of ("INPUT").unwrap ()); let b3_hash = hash (input_path)?; let local_path = PathBuf::from ("lookup_repo").join (repo_path (&b3_hash)); let local_dir = local_path.parent ().unwrap (); let editor = env::var ("EDITOR").unwrap_or_else (|_| String::from ("nano")); fs::create_dir_all (local_dir)?; process::Command::new ("kate") .arg ("-b") .arg (local_path) .status ().context ("while spawning editor")?; return Ok (()); } let input_path = Path::new (matches.value_of ("INPUT").unwrap ()); let b3_hash = hash (input_path)?; let local_path = PathBuf::from ("lookup_repo").join (repo_path (&b3_hash)); println! ("Local path: {:?}", local_path); Ok (()) } fn hash (input_path: &Path) -> anyhow::Result { let mut hasher = blake3::Hasher::new (); { let mod_guard = FileModificationGuard::try_new (input_path)?; let mut input_file = fs::File::open (input_path)?; io::copy (&mut input_file, &mut hasher)?; mod_guard.check ().context ("while hashing input file")?; } Ok (hasher.finalize ()) } fn repo_path (b3_hash: &blake3::Hash) -> PathBuf { let b3_hex = b3_hash.to_hex (); PathBuf::from ("blake3") .join (&b3_hex [0..2]) .join (&b3_hex [2..]) } struct FileModificationGuard <'a> { path: &'a Path, metadata_start: CommonFileMetadata, } #[derive (PartialEq)] struct CommonFileMetadata { len: u64, modified: SystemTime } impl <'a> FileModificationGuard <'a> { fn try_new (path: &'a Path) -> anyhow::Result { Ok (Self { path, metadata_start: CommonFileMetadata::try_new (path)?, }) } fn check (&self) -> anyhow::Result <()> { let md = CommonFileMetadata::try_new (self.path)?; if md != self.metadata_start { bail! ("File metadata changed"); } Ok (()) } } impl CommonFileMetadata { fn try_new (path: &Path) -> anyhow::Result { let md = fs::metadata (path)?; Ok (Self { len: md.len (), modified: md.modified ()?, }) } }