libcachebust/src/hash.rs

255 lines
7.7 KiB
Rust
Raw Normal View History

2021-04-08 20:05:11 +05:30
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* Use of this source code is governed by the Apache 2.0 and/or the MIT
* License.
*/
use std::io::Error;
use std::path::Path;
use std::{fs, path::PathBuf};
use derive_builder::Builder;
use walkdir::WalkDir;
2021-04-08 20:45:38 +05:30
use crate::map::Files;
2021-04-08 20:05:11 +05:30
#[derive(Debug, Clone, Builder)]
pub struct Buster {
// source directory
#[builder(setter(into))]
source: String,
// mime_types for hashing
mime_types: Vec<mime::Mime>,
// 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
2021-04-08 20:45:38 +05:30
pub fn try_hash(&self) -> Result<Files, Error> {
let mut file_map: Files = Files::default();
2021-04-08 20:05:11 +05:30
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);
2021-04-08 22:00:41 +05:30
let _ = file_map.add(
source.to_str().unwrap().into(),
destination.to_str().unwrap().into(),
);
2021-04-08 20:05:11 +05:30
}
}
}
}
Ok(file_map)
}
// panics when mimetypes are detected. This way you'll know which files are ignored
// from processing
2021-04-08 20:45:38 +05:30
pub fn hash(&self) -> Result<Files, Error> {
let mut file_map: Files = Files::default();
2021-04-08 20:05:11 +05:30
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);
2021-04-08 22:00:41 +05:30
let _ = file_map.add(
source.to_str().unwrap().into(),
destination.to_str().unwrap().into(),
);
2021-04-08 20:05:11 +05:30
}
}
}
}
Ok(file_map)
}
fn read_to_string(path: &Path) -> Result<String, Error> {
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)]
2021-04-08 22:00:41 +05:30
pub mod tests {
2021-04-08 20:05:11 +05:30
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();
2021-04-08 20:45:38 +05:30
let mut files = config.hash().unwrap();
2021-04-08 20:05:11 +05:30
2021-04-08 20:45:38 +05:30
for (k, v) in files.map.drain() {
2021-04-08 20:05:11 +05:30
let src = Path::new(&k);
let dest = Path::new(&v);
assert_eq!(src.exists(), dest.exists());
}
2021-04-08 22:00:41 +05:30
cleanup(&config);
2021-04-08 20:05:11 +05:30
}
2021-04-08 20:14:53 +05:30
#[test]
fn try_hash_works() {
let types = vec![
mime::IMAGE_PNG,
mime::IMAGE_SVG,
mime::IMAGE_JPEG,
mime::IMAGE_GIF,
];
let config = BusterBuilder::default()
.source("./dist")
.result("/tmp/prod")
.mime_types(types)
.copy(true)
.follow_links(true)
.build()
.unwrap();
config.init().unwrap();
2021-04-08 22:00:41 +05:30
let mut files = config.hash().unwrap();
2021-04-08 20:14:53 +05:30
2021-04-08 20:45:38 +05:30
for (k, v) in files.map.drain() {
2021-04-08 20:14:53 +05:30
let src = Path::new(&k);
let dest = Path::new(&v);
assert_eq!(src.exists(), dest.exists());
}
2021-04-08 22:00:41 +05:30
cleanup(&config);
}
pub fn cleanup(config: &Buster) {
let _ = fs::remove_dir_all(&config.result);
2021-04-08 20:14:53 +05:30
}
2021-04-08 20:05:11 +05:30
}