diff --git a/src/hash.rs b/src/hash.rs new file mode 100644 index 0000000..53032ff --- /dev/null +++ b/src/hash.rs @@ -0,0 +1,217 @@ +/* +* Copyright (C) 2021 Aravinth Manivannan +* +* Use of this source code is governed by the Apache 2.0 and/or the MIT +* License. +*/ + +use std::collections::HashMap; +use std::io::Error; +use std::path::Path; +use std::{fs, path::PathBuf}; + +use derive_builder::Builder; +use walkdir::WalkDir; + +#[derive(Debug, Clone, Builder)] +pub struct Buster { + // source directory + #[builder(setter(into))] + source: String, + // mime_types for hashing + mime_types: Vec, + // directory for writing results + #[builder(setter(into))] + result: String, + // copy other non-hashed files from source dire to result dir? + copy: bool, + follow_links: bool, +} + +impl Buster { + pub fn init(&self) -> Result<(), Error> { + let res = Path::new(&self.result); + if res.exists() { + fs::remove_dir_all(&self.result).unwrap(); + } + + fs::create_dir(&self.result).unwrap(); + self.create_dir_structure(Path::new(&self.source))?; + Ok(()) + } + + fn hasher(payload: &str) -> String { + use data_encoding::HEXUPPER; + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(payload); + HEXUPPER.encode(&hasher.finalize()) + } + + // if mime types are common, then you shoud be fine using this + // use [hash] when when they aren't + // + // doesn't process files for which mime is not resolved + pub fn try_hash(&self) -> Result, Error> { + let mut file_map: HashMap = HashMap::default(); + for entry in WalkDir::new(&self.source) + .follow_links(self.follow_links) + .into_iter() + { + let entry = entry?; + let path = entry.path(); + let path = Path::new(&path); + + for mime_type in self.mime_types.iter() { + if let Some(file_mime) = mime_guess::from_path(path).first() { + if &file_mime == mime_type { + let contents = Self::read_to_string(&path).unwrap(); + let hash = Self::hasher(&contents); + let new_name = format!( + "{}.{}.{}", + path.file_stem().unwrap().to_str().unwrap(), + hash, + path.extension().unwrap().to_str().unwrap() + ); + self.copy(path, &new_name); + + let (source, destination) = self.gen_map(path, &&new_name); + file_map.insert( + source.to_str().unwrap().into(), + destination.to_str().unwrap().into(), + ); + } + } + } + } + + Ok(file_map) + } + + // panics when mimetypes are detected. This way you'll know which files are ignored + // from processing + pub fn hash(&self) -> Result, Error> { + let mut file_map: HashMap = HashMap::default(); + + for entry in WalkDir::new(&self.source) + .follow_links(self.follow_links) + .into_iter() + { + let entry = entry?; + + let path = entry.path(); + if !path.is_dir() { + let path = Path::new(&path); + + for mime_type in self.mime_types.iter() { + let file_mime = mime_guess::from_path(path) + .first() + .expect(&format!("couldn't resolve MIME for file: {:?}", &path)); + if &file_mime == mime_type { + let contents = Self::read_to_string(&path).unwrap(); + let hash = Self::hasher(&contents); + let new_name = format!( + "{}.{}.{}", + path.file_stem().unwrap().to_str().unwrap(), + hash, + path.extension().unwrap().to_str().unwrap() + ); + self.copy(path, &new_name); + let (source, destination) = self.gen_map(path, &&new_name); + file_map.insert( + source.to_str().unwrap().into(), + destination.to_str().unwrap().into(), + ); + } + } + } + } + + Ok(file_map) + } + + fn read_to_string(path: &Path) -> Result { + use std::fs::File; + use std::io::{BufRead, BufReader}; + + let input = File::open(path)?; + let buffered = BufReader::new(input); + + let mut res = String::new(); + for line in buffered.lines() { + res.push_str(&line?) + } + + Ok(res) + } + + fn gen_map<'a>(&self, source: &'a Path, name: &str) -> (&'a Path, PathBuf) { + let rel_location = source.strip_prefix(&self.source).unwrap().parent().unwrap(); + let destination = Path::new(&self.result).join(rel_location).join(name); + (source, destination) + } + + fn copy(&self, source: &Path, name: &str) { + let rel_location = source.strip_prefix(&self.source).unwrap().parent().unwrap(); + let destination = Path::new(&self.result).join(rel_location).join(name); + fs::copy(source, &destination).unwrap(); + } + + fn create_dir_structure(&self, path: &Path) -> Result<(), Error> { + for entry in WalkDir::new(&path) + .follow_links(self.follow_links) + .into_iter() + { + let entry = entry?; + let entry_path = entry.path(); + let entry_path = Path::new(&entry_path); + + if entry_path.is_dir() && path != entry_path { + Self::create_dir_structure(&self, entry_path)?; + } else { + if entry_path.is_dir() { + let rel_location = entry_path.strip_prefix(&self.source).unwrap(); + let destination = Path::new(&self.result).join(rel_location); + if !destination.exists() { + fs::create_dir(destination)? + } + } + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hasher_works() { + let types = vec![ + mime::IMAGE_PNG, + mime::IMAGE_SVG, + mime::IMAGE_JPEG, + mime::IMAGE_GIF, + ]; + + let config = BusterBuilder::default() + .source("./dist") + .result("./prod") + .mime_types(types) + .copy(true) + .follow_links(true) + .build() + .unwrap(); + + config.init().unwrap(); + let mut map = config.hash().unwrap(); + + for (k, v) in map.drain() { + let src = Path::new(&k); + let dest = Path::new(&v); + + assert_eq!(src.exists(), dest.exists()); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index c9a48b1..8a8ee45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,160 +32,5 @@ //! } //! ``` -use std::fs; -use std::io::Error; -use std::path::Path; - -use derive_builder::Builder; -use walkdir::WalkDir; - -#[derive(Debug, Clone, Builder)] -pub struct Buster { - // source directory - #[builder(setter(into))] - source: String, - // mime_types for hashing - mime_types: Vec, - // directory for writing results - #[builder(setter(into))] - result: String, - // copy other non-hashed files from source dire to result dir? - copy: bool, - follow_links: bool, -} - -impl Buster { - pub fn init(&self) -> Result<(), Error> { - let res = Path::new(&self.result); - if res.exists() { - fs::remove_dir_all(&self.result).unwrap(); - } - - fs::create_dir(&self.result).unwrap(); - self.create_dir_structure(Path::new(&self.source))?; - Ok(()) - } - - fn hasher(payload: &str) -> String { - use data_encoding::HEXUPPER; - use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); - hasher.update(payload); - HEXUPPER.encode(&hasher.finalize()) - } - - // if mime types are common, then you shoud be fine using this - // use [hash] when when they aren't - // - // doesn't process files for which mime is not resolved - pub fn try_hash(&self) -> Result<(), Error> { - for entry in WalkDir::new(&self.source) - .follow_links(self.follow_links) - .into_iter() - { - let entry = entry?; - let path = entry.path(); - let path = Path::new(&path); - - for mime_type in self.mime_types.iter() { - if let Some(file_mime) = mime_guess::from_path(path).first() { - if &file_mime == mime_type { - let contents = Self::read_to_string(&path).unwrap(); - let hash = Self::hasher(&contents); - let new_name = format!( - "{}.{}.{}", - path.file_stem().unwrap().to_str().unwrap(), - hash, - path.extension().unwrap().to_str().unwrap() - ); - // println!("{}", &new_name); - self.copy(path, &new_name); - // let data = fs::copy(path, new_name); - } - } - } - } - - Ok(()) - } - - // panics when mimetypes are detected. This way you'll know which files are ignored - // from processing - pub fn hash(&self) -> Result<(), Error> { - for entry in WalkDir::new(&self.source) - .follow_links(self.follow_links) - .into_iter() - { - let entry = entry?; - - let path = entry.path(); - if !path.is_dir() { - let path = Path::new(&path); - - for mime_type in self.mime_types.iter() { - let file_mime = mime_guess::from_path(path) - .first() - .expect(&format!("couldn't resolve MIME for file: {:?}", &path)); - if &file_mime == mime_type { - let contents = Self::read_to_string(&path).unwrap(); - let hash = Self::hasher(&contents); - let new_name = format!( - "{}.{}.{}", - path.file_stem().unwrap().to_str().unwrap(), - hash, - path.extension().unwrap().to_str().unwrap() - ); - self.copy(path, &new_name); - } - } - } - } - - Ok(()) - } - - fn read_to_string(path: &Path) -> Result { - use std::fs::File; - use std::io::{BufRead, BufReader}; - - let input = File::open(path)?; - let buffered = BufReader::new(input); - - let mut res = String::new(); - for line in buffered.lines() { - res.push_str(&line?) - } - - Ok(res) - } - - fn copy(&self, source: &Path, name: &str) { - let rel_location = source.strip_prefix(&self.source).unwrap().parent().unwrap(); - let destination = Path::new(&self.result).join(rel_location).join(name); - fs::copy(source, &destination).unwrap(); - } - - fn create_dir_structure(&self, path: &Path) -> Result<(), Error> { - for entry in WalkDir::new(&path) - .follow_links(self.follow_links) - .into_iter() - { - let entry = entry?; - let entry_path = entry.path(); - let entry_path = Path::new(&entry_path); - - if entry_path.is_dir() && path != entry_path { - Self::create_dir_structure(&self, entry_path)?; - } else { - if entry_path.is_dir() { - let rel_location = entry_path.strip_prefix(&self.source).unwrap(); - let destination = Path::new(&self.result).join(rel_location); - if !destination.exists() { - fs::create_dir(destination)? - } - } - } - } - Ok(()) - } -} +pub mod hash; +pub use hash::BusterBuilder;