changelog and docs

This commit is contained in:
Aravinth Manivannan 2021-04-10 17:45:00 +05:30
parent edf8a3121b
commit 23bf39b328
Signed by: realaravinth
GPG key ID: AD9F0F08E855ED88
5 changed files with 109 additions and 32 deletions

5
CHANGELOG.md Normal file
View file

@ -0,0 +1,5 @@
## 0.1.0
## Added:
- `SHA-256`-based cache-buster
- runtime filemap loading

View file

@ -17,6 +17,5 @@ fn main() {
.build() .build()
.unwrap(); .unwrap();
config.init().unwrap(); config.process().unwrap().to_env();
config.hash().unwrap().to_env();
} }

View file

@ -4,6 +4,17 @@
* Use of this source code is governed by the Apache 2.0 and/or the MIT * Use of this source code is governed by the Apache 2.0 and/or the MIT
* License. * License.
*/ */
//! Module describing runtime compoenet for fetching modified filenames
//!
//! Add the following tou your program to load the filemap during compiletime:
//!
//! ```no_run
//! use cache_buster::Files;
//!
//! fn main(){
//! let files = Files::load();
//! }
//! ```
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::env;
@ -12,13 +23,18 @@ use serde::{Deserialize, Serialize};
const ENV_VAR_NAME: &str = "CACHE_BUSTER_FILE_MAP"; const ENV_VAR_NAME: &str = "CACHE_BUSTER_FILE_MAP";
/// Filemap struct
///
/// maps original names to generated names
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct Files { pub struct Files {
/// filemap<original-path, modified-path>
pub map: HashMap<String, String>, pub map: HashMap<String, String>,
base_dir: String, base_dir: String,
} }
impl Files { impl Files {
/// Initialize map
pub fn new(base_dir: &str) -> Self { pub fn new(base_dir: &str) -> Self {
Files { Files {
map: HashMap::default(), map: HashMap::default(),
@ -26,6 +42,10 @@ impl Files {
} }
} }
/// Get relative file path
///
/// If the modified filename path is './prod/test.randomhash.svg`, it will
/// output `/test.randomhash.svg`. For full path, see [get_full_path]
pub fn get<'a>(&'a self, path: &'a str) -> Option<&'a str> { pub fn get<'a>(&'a self, path: &'a str) -> Option<&'a str> {
if let Some(path) = self.map.get(path) { if let Some(path) = self.map.get(path) {
Some(&path[self.base_dir.len()..]) Some(&path[self.base_dir.len()..])
@ -34,10 +54,15 @@ impl Files {
} }
} }
/// Get file path
///
/// If the modified filename path is './prod/test.randomhash.svg`, it will
/// output `/prod/test.randomhash.svg`. For relative path, see [get]
pub fn get_full_path<'a>(&'a self, path: &'a str) -> Option<&'a String> { pub fn get_full_path<'a>(&'a self, path: &'a str) -> Option<&'a String> {
self.map.get(path) self.map.get(path)
} }
/// Create file map: map original path to modified paths
pub fn add(&mut self, k: String, v: String) -> Result<(), &'static str> { pub fn add(&mut self, k: String, v: String) -> Result<(), &'static str> {
if self.map.contains_key(&k) { if self.map.contains_key(&k) {
Err("key exists") Err("key exists")
@ -47,6 +72,8 @@ impl Files {
} }
} }
/// This crate uses compile-time environment variables to transfer
/// data to the main program. This funtction sets that variable
pub fn to_env(&self) { pub fn to_env(&self) {
println!( println!(
"cargo:rustc-env={}={}", "cargo:rustc-env={}={}",
@ -61,6 +88,7 @@ impl Files {
env::set_var(ENV_VAR_NAME, serde_json::to_string(&self).unwrap()); env::set_var(ENV_VAR_NAME, serde_json::to_string(&self).unwrap());
} }
/// Load filemap in main program. Should be called from main program
pub fn load() -> Self { pub fn load() -> Self {
let env = env::var(ENV_VAR_NAME) let env = env::var(ENV_VAR_NAME)
.expect("unable to read env var, might be a bug in lib. Please report on GitHub"); .expect("unable to read env var, might be a bug in lib. Please report on GitHub");
@ -95,8 +123,7 @@ mod tests {
.build() .build()
.unwrap(); .unwrap();
config.init().unwrap(); let files = config.process().unwrap();
let files = config.hash().unwrap();
assert!(get_full_path_runner("./dist/log-out.svg", &files)); assert!(get_full_path_runner("./dist/log-out.svg", &files));
assert!(get_full_path_runner( assert!(get_full_path_runner(
@ -138,8 +165,7 @@ mod tests {
.build() .build()
.unwrap(); .unwrap();
config.init().unwrap(); let files = config.process().unwrap();
let files = config.hash().unwrap();
files.to_env(); files.to_env();
@ -168,8 +194,7 @@ mod tests {
.build() .build()
.unwrap(); .unwrap();
config.init().unwrap(); let files = config.process().unwrap();
let files = config.hash().unwrap();
assert!(get_runner("./dist/log-out.svg", &files)); assert!(get_runner("./dist/log-out.svg", &files));
assert!(get_runner("./dist/a/b/c/d/s/d/svg/credit-card.svg", &files)); assert!(get_runner("./dist/a/b/c/d/s/d/svg/credit-card.svg", &files));

View file

@ -4,13 +4,13 @@
* Use of this source code is governed by the Apache 2.0 and/or the MIT * Use of this source code is governed by the Apache 2.0 and/or the MIT
* License. * License.
*/ */
#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)]
//! ```rust //! ```no_run
//! use cache_buster::BusterBuilder; //! use cache_buster::BusterBuilder;
//! //!
//! fn main() { //! fn main() {
//! // note: add error checking yourself. //! // note: add error checking yourself.
//! // println!("cargo:rustc-env=GIT_HASH={}", git_hash); //! // println!("cargo:rustc-env=GIT_process={}", git_process);
//! let types = vec![ //! let types = vec![
//! mime::IMAGE_PNG, //! mime::IMAGE_PNG,
//! mime::IMAGE_SVG, //! mime::IMAGE_SVG,
@ -27,8 +27,7 @@
//! .build() //! .build()
//! .unwrap(); //! .unwrap();
//! //!
//! config.init().unwrap(); //! config.process().unwrap();
//! config.hash().unwrap();
//! } //! }
//! ``` //! ```

View file

@ -5,6 +5,39 @@
* License. * License.
*/ */
//! Module describing file processor that changes filenames to setup cache-busting
//!
//! Run the following during build using `build.rs`:
//!
//! ```rust
//! use cache_buster::BusterBuilder;
//!
//! fn main() {
//! // note: add error checking yourself.
//! // println!("cargo:rustc-env=GIT_process={}", git_process);
//! 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.process().unwrap();
//! }
//! ```
//!
//! There's a runtime component to this library which will let you read modified
//! filenames from within your program. See [Files]
use std::io::Error; use std::io::Error;
use std::path::Path; use std::path::Path;
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
@ -14,23 +47,26 @@ use walkdir::WalkDir;
use crate::Files; use crate::Files;
/// Configuration for setting up cache-busting
#[derive(Debug, Clone, Builder)] #[derive(Debug, Clone, Builder)]
pub struct Buster { pub struct Buster {
// source directory /// source directory
#[builder(setter(into))] #[builder(setter(into))]
source: String, source: String,
// mime_types for hashing /// mime_types for hashing
mime_types: Vec<mime::Mime>, mime_types: Vec<mime::Mime>,
// directory for writing results /// directory for writing results
#[builder(setter(into))] #[builder(setter(into))]
result: String, result: String,
// copy other non-hashed files from source dire to result dir? /// copy other non-hashed files from source dire to result dir?
copy: bool, copy: bool,
/// follow symlinks?
follow_links: bool, follow_links: bool,
} }
impl Buster { impl Buster {
pub fn init(&self) -> Result<(), Error> { // creates base_dir to output files to
fn init(&self) -> Result<(), Error> {
let res = Path::new(&self.result); let res = Path::new(&self.result);
if res.exists() { if res.exists() {
fs::remove_dir_all(&self.result).unwrap(); fs::remove_dir_all(&self.result).unwrap();
@ -49,11 +85,16 @@ impl Buster {
HEXUPPER.encode(&hasher.finalize()) HEXUPPER.encode(&hasher.finalize())
} }
// if mime types are common, then you shoud be fine using this /// Processes files.
// use [hash] when when they aren't ///
// /// If MIME types are uncommon, then use this funtion
// doesn't process files for which mime is not resolved /// as it won't panic when a weird MIM is encountered.
pub fn try_hash(&self) -> Result<Files, Error> { ///
/// Otherwise, use [process]
///
/// Note: it omits processing uncommon MIME types
pub fn try_process(&self) -> Result<Files, Error> {
self.init()?;
let mut file_map: Files = Files::new(&self.result); let mut file_map: Files = Files::new(&self.result);
for entry in WalkDir::new(&self.source) for entry in WalkDir::new(&self.source)
.follow_links(self.follow_links) .follow_links(self.follow_links)
@ -89,9 +130,15 @@ impl Buster {
Ok(file_map) Ok(file_map)
} }
// panics when mimetypes are detected. This way you'll know which files are ignored /// Processes files.
// from processing ///
pub fn hash(&self) -> Result<Files, Error> { /// If MIME types are common, then use this funtion
/// as it will panic when a weird MIM is encountered.
pub fn process(&self) -> Result<Files, Error> {
// panics when mimetypes are detected. This way you'll know which files are ignored
// from processing
self.init()?;
let mut file_map: Files = Files::new(&self.result); let mut file_map: Files = Files::new(&self.result);
for entry in WalkDir::new(&self.source) for entry in WalkDir::new(&self.source)
@ -131,6 +178,7 @@ impl Buster {
Ok(file_map) Ok(file_map)
} }
// helper fn to read file to string
fn read_to_string(path: &Path) -> Result<Vec<u8>, Error> { fn read_to_string(path: &Path) -> Result<Vec<u8>, Error> {
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
@ -141,18 +189,21 @@ impl Buster {
Ok(file_content) Ok(file_content)
} }
// helper fn to generate filemap
fn gen_map<'a>(&self, source: &'a Path, name: &str) -> (&'a Path, PathBuf) { 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 rel_location = source.strip_prefix(&self.source).unwrap().parent().unwrap();
let destination = Path::new(&self.result).join(rel_location).join(name); let destination = Path::new(&self.result).join(rel_location).join(name);
(source, destination) (source, destination)
} }
// helper fn to copy files
fn copy(&self, source: &Path, name: &str) { fn copy(&self, source: &Path, name: &str) {
let rel_location = source.strip_prefix(&self.source).unwrap().parent().unwrap(); let rel_location = source.strip_prefix(&self.source).unwrap().parent().unwrap();
let destination = Path::new(&self.result).join(rel_location).join(name); let destination = Path::new(&self.result).join(rel_location).join(name);
fs::copy(source, &destination).unwrap(); fs::copy(source, &destination).unwrap();
} }
// helper fn to create directory structure in self.base_dir
fn create_dir_structure(&self, path: &Path) -> Result<(), Error> { fn create_dir_structure(&self, path: &Path) -> Result<(), Error> {
for entry in WalkDir::new(&path) for entry in WalkDir::new(&path)
.follow_links(self.follow_links) .follow_links(self.follow_links)
@ -193,15 +244,14 @@ pub mod tests {
let config = BusterBuilder::default() let config = BusterBuilder::default()
.source("./dist") .source("./dist")
.result("./prod") .result("./prod56")
.mime_types(types) .mime_types(types)
.copy(true) .copy(true)
.follow_links(true) .follow_links(true)
.build() .build()
.unwrap(); .unwrap();
config.init().unwrap(); let mut files = config.process().unwrap();
let mut files = config.hash().unwrap();
for (k, v) in files.map.drain() { for (k, v) in files.map.drain() {
let src = Path::new(&k); let src = Path::new(&k);
@ -213,7 +263,7 @@ pub mod tests {
} }
#[test] #[test]
fn try_hash_works() { fn try_process_works() {
let types = vec![ let types = vec![
mime::IMAGE_PNG, mime::IMAGE_PNG,
mime::IMAGE_SVG, mime::IMAGE_SVG,
@ -230,8 +280,7 @@ pub mod tests {
.build() .build()
.unwrap(); .unwrap();
config.init().unwrap(); let mut files = config.try_process().unwrap();
let mut files = config.try_hash().unwrap();
for (k, v) in files.map.drain() { for (k, v) in files.map.drain() {
let src = Path::new(&k); let src = Path::new(&k);