use crate::error_mod::{Error, Result};
use crate::public_api_mod::{RED, RESET, YELLOW};
use chrono::DateTime;
use chrono::Timelike;
use chrono::{Datelike, Utc};
use serde_derive::{Deserialize, Serialize};
use sha2::Digest;
use std::str::FromStr;
#[derive(Serialize, Deserialize)]
pub struct FileMetaData {
filename: String,
filehash: String,
}
#[derive(Serialize, Deserialize)]
pub struct AutoVersionFromDate {
pub vec_file_metadata: Vec<FileMetaData>,
}
pub fn auto_version_from_date() -> Result<()> {
auto_version_from_date_internal(false)
}
pub fn auto_version_from_date_forced() -> Result<()> {
auto_version_from_date_internal(true)
}
fn auto_version_from_date_internal(force_version: bool) -> Result<()> {
let date = Utc::now();
let new_version = version_from_date(date);
let vec_of_metadata = read_file_metadata()?;
let is_files_equal = if force_version {
false
} else {
let js_struct = read_json_file(".automation_tasks_rs_file_hashes.json")?;
are_files_equal(&vec_of_metadata, &js_struct.vec_file_metadata)
};
if !is_files_equal {
write_version_to_cargo_and_modify_metadata(&new_version, vec_of_metadata)?;
}
modify_service_js(&new_version)?;
Ok(())
}
fn modify_service_js(new_version: &str) -> Result<()> {
let start_dir = camino::Utf8Path::new("./");
for js_filename in &crate::utils_mod::traverse_dir_with_exclude_dir(
start_dir.as_std_path(),
"/service_worker.js",
&["/.git".to_string(), "/target".to_string()],
)? {
let mut js_content = std::fs::read_to_string(js_filename)?;
if js_content.contains("\r\n") {
return Err(Error::ErrorFromString(format!(
"{RED}Error: {js_filename} has CRLF line endings instead of LF. Correct the file! {RESET}"
)));
}
let delimiter = r#"const CACHE_NAME = '"#;
let delimiter_len = delimiter.len();
let option_location = js_content.find(delimiter);
if let Some(location) = option_location {
let start_version = location + delimiter_len;
let option_end_quote = find_from(js_content.as_str(), start_version, r#"';"#);
if let Ok(end_version) = option_end_quote {
let old_version: String = js_content.drain(start_version..end_version).collect();
if new_version != old_version {
println!(" {YELLOW}Modify version: {old_version} -> {new_version}{RESET}");
js_content.insert_str(start_version, new_version);
let _x = std::fs::write(js_filename, js_content);
}
} else {
return Err(Error::ErrorFromString(format!("{RED}no end quote for version{RESET}")));
}
} else {
return Err(Error::ErrorFromString(format!("{RED}service_worker.js has no version{RESET}")));
}
}
Ok(())
}
fn write_version_to_cargo_and_modify_metadata(new_version: &str, mut vec_of_metadata: Vec<FileMetaData>) -> Result<()> {
let cargo_filename = "Cargo.toml";
let mut cargo_content = std::fs::read_to_string(cargo_filename)?;
if cargo_content.contains("\r\n") {
return Err(Error::ErrorFromString(format!(
"{RED}Error: {} has CRLF line endings instead of LF. Correct the file! {RESET}",
cargo_filename
)));
}
let delimiter = r#"version = ""#;
let delimiter_len = delimiter.len();
let option_location = cargo_content.find(delimiter);
if let Some(location) = option_location {
let start_version = location + delimiter_len;
let option_end_quote = find_from(cargo_content.as_str(), start_version, r#"""#);
if let Ok(end_version) = option_end_quote {
let old_version: String = cargo_content.drain(start_version..end_version).collect();
if new_version != old_version.as_str() {
println!(" {YELLOW}Modify version: {old_version} -> {new_version}{RESET}");
cargo_content.insert_str(start_version, new_version);
let _x = std::fs::write(cargo_filename, cargo_content);
correct_file_metadata_for_cargo_tom_inside_vec(&mut vec_of_metadata)?;
save_json_file_for_file_meta_data(vec_of_metadata)?;
}
} else {
return Err(Error::ErrorFromString(format!("{RED}no end quote for version{RESET}")));
}
} else {
return Err(Error::ErrorFromString(format!("{RED}Cargo.toml has no version{RESET}")));
}
Ok(())
}
pub fn correct_file_metadata_for_cargo_tom_inside_vec(vec_of_metadata: &mut [FileMetaData]) -> Result<()> {
let filename = "Cargo.toml".to_string();
let filehash = sha256_digest(std::path::PathBuf::from_str(&filename)?.as_path())?;
vec_of_metadata
.get_mut(0)
.ok_or(Error::ErrorFromStr("error vec_of_metadata.get_mut(0)"))?
.filehash = filehash;
Ok(())
}
pub fn are_files_equal(vec_of_metadata: &[FileMetaData], js_vec_of_metadata: &[FileMetaData]) -> bool {
let mut is_files_equal = true;
for x in vec_of_metadata.iter() {
let mut is_one_equal = false;
for y in js_vec_of_metadata.iter() {
if x.filename == y.filename && x.filehash == y.filehash {
is_one_equal = true;
break;
}
}
if !is_one_equal {
is_files_equal = false;
break;
}
}
is_files_equal
}
pub fn read_file_metadata() -> Result<Vec<FileMetaData>> {
let mut vec_of_metadata: Vec<FileMetaData> = Vec::new();
let filename = "Cargo.toml".to_string();
let filehash = sha256_digest(std::path::PathBuf::from_str(&filename)?.as_path())?;
vec_of_metadata.push(FileMetaData { filename, filehash });
let files_paths = crate::utils_mod::traverse_dir_with_exclude_dir(
camino::Utf8Path::new("src").as_std_path(),
"/*.rs",
&[],
)?;
for filename in files_paths {
let filehash = sha256_digest(std::path::PathBuf::from_str(&filename)?.as_path())?;
vec_of_metadata.push(FileMetaData { filename, filehash });
}
Ok(vec_of_metadata)
}
fn sha256_digest(path: &std::path::Path) -> Result<String> {
let file = std::fs::File::open(path)?;
let mut reader = std::io::BufReader::new(file);
let mut hasher = sha2::Sha256::new();
let mut buffer = [0; 1024];
use std::io::Read;
loop {
let count = reader.read(&mut buffer)?;
if count == 0 {
break;
}
hasher.update(&buffer[..count]);
}
let digest = hasher.finalize();
let hash_string = data_encoding::HEXLOWER.encode(digest.as_ref());
Ok(hash_string)
}
pub fn read_json_file(json_filepath: &str) -> Result<AutoVersionFromDate> {
let js_struct: AutoVersionFromDate;
let f = std::fs::read_to_string(json_filepath);
match f {
Ok(x) => {
if x.contains("\r\n") {
js_struct = AutoVersionFromDate {
vec_file_metadata: Vec::new(),
}
} else {
js_struct = serde_json::from_str(x.as_str())?;
}
}
Err(_error) => {
js_struct = AutoVersionFromDate {
vec_file_metadata: Vec::new(),
}
}
};
Ok(js_struct)
}
pub fn save_json_file_for_file_meta_data(vec_of_metadata: Vec<FileMetaData>) -> Result<()> {
let x = AutoVersionFromDate {
vec_file_metadata: vec_of_metadata,
};
let y = serde_json::to_string_pretty(&x)?;
let json_filepath = ".automation_tasks_rs_file_hashes.json";
let _f = std::fs::write(json_filepath, y);
Ok(())
}
fn version_from_date(date: DateTime<chrono::Utc>) -> String {
if date.hour() == 0 {
format!("{:04}.{}{:02}.{}", date.year(), date.month(), date.day(), date.minute())
} else {
format!(
"{:04}.{}{:02}.{}{:02}",
date.year(),
date.month(),
date.day(),
date.hour(),
date.minute()
)
}
}
fn find_from(rs_content: &str, from: usize, find: &str) -> Result<usize> {
let slice01 = rs_content.get(from..).ok_or_else(|| Error::ErrorFromStr("get from is None"))?;
let option_location = slice01.find(find);
if let Some(location) = option_location {
Ok(from + location)
} else {
Err(Error::ErrorFromStr("location not found"))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
pub fn test_date_to_version() {
let date_time = chrono::TimeZone::with_ymd_and_hms(&Utc, 2020, 5, 22, 00, 34, 0).unwrap();
let version = version_from_date(date_time);
assert_eq!(version, "2020.522.34");
}
#[test]
pub fn test_sha256_digest() -> Result<()> {
let digest = sha256_digest(camino::Utf8Path::new("LICENSE").as_std_path())?;
let hash_string = data_encoding::HEXLOWER.encode(digest.as_ref());
let expected_hex = "66343964363936663834636237373465396336653537646333646433633537386532643333623130613539663837326634383134373337386462303038653035";
assert_eq!(&hash_string, expected_hex);
Ok(())
}
}