use serde::{Serialize, de::DeserializeOwned};
use std::fs::File;
use std::io::BufWriter;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use crate::entities::runs::Merge;
use crate::{CACHE_DIR, LOGS_DIR};
pub static VERBOSE: OnceLock<bool> = OnceLock::new();
const LOG_FILE_DELIMETER: &str = "================================================================";
pub fn read<T: DeserializeOwned + Default>(folder: impl AsRef<Path>, file: impl AsRef<Path>) -> T {
let mut path = PathBuf::new();
path.push(folder.as_ref());
path.push(file.as_ref());
read_checked(&path).unwrap_or_else(|e| {
log(format!("Error on file read: {e:?}"));
Default::default()
})
}
pub fn read_versioned<T: DeserializeOwned + Default>(folder: impl AsRef<Path>, file: &str, default_version: u8) -> T {
let mut path = PathBuf::new();
path.push(folder.as_ref());
path.push(file);
if !path.exists() {
return Default::default();
}
match read_checked::<T>(&path) {
Ok(res) => res,
Err(e) => {
eprintln!("{e:?}");
let content = std::fs::read_to_string(&path).unwrap();
let version = crate::globals::extract_version(content).unwrap();
if version != default_version {
eprintln!("Version is incorrect! Migrate configuration file to v{default_version}!");
std::process::exit(1);
} else {
panic!("Version is correct, but config is invalid!")
}
}
}
}
pub fn read_checked<T: DeserializeOwned>(filepath: impl AsRef<Path>) -> anyhow::Result<T> {
let content = std::fs::read_to_string(filepath.as_ref())?;
log(format!("Trying to read {:?} file...", filepath.as_ref()));
serde_pretty_yaml::from_str(&content).map_err(|e| anyhow::anyhow!("{}", e))
}
pub fn write<T: Serialize>(folder: impl AsRef<Path>, file: impl AsRef<Path>, config: &T) {
let mut path = PathBuf::new();
path.push(folder);
path.push(file.as_ref());
let content = match serde_pretty_yaml::to_string_pretty(config).map_err(|e| anyhow::anyhow!("{}", e)) {
Ok(s) => s,
Err(e) => {
log(format!(
"Can't save `{:?}` config file: {:?}!",
file.as_ref().as_os_str(),
e
));
return;
}
};
match std::fs::write(path, content) {
Ok(()) => log(format!("Written `{:?}` config file", file.as_ref().as_os_str())),
Err(e) => log(format!(
"Can't save `{:?}` config file: {:?}!",
file.as_ref().as_os_str(),
e
)),
}
}
pub fn write_merge<T: Serialize + Merge + Default + DeserializeOwned + Clone>(
folder: impl AsRef<Path>,
file: impl AsRef<Path>,
config: &T,
) {
let mut path = PathBuf::new();
path.push(folder);
path.push(file.as_ref());
let other = read_checked(&path).unwrap_or_default();
let merged: T = config.merge(other).unwrap_or((*config).clone());
let content = match serde_pretty_yaml::to_string(&merged).map_err(|e| anyhow::anyhow!("{}", e)) {
Ok(s) => s,
Err(e) => {
log(format!(
"Can't save `{:?}` config file: {:?}!",
file.as_ref().as_os_str(),
e
));
return;
}
};
match std::fs::write(path, content) {
Ok(()) => {}
Err(e) => log(format!(
"Can't save `{:?}` config file: {:?}!",
file.as_ref().as_os_str(),
e
)),
}
}
pub fn copy_all(
root: impl AsRef<Path>,
src: impl AsRef<Path>,
dst: impl AsRef<Path>,
ignore: &[impl AsRef<Path>],
) -> anyhow::Result<Vec<PathBuf>> {
if src.as_ref().is_file() {
if let Some(parent) = dst.as_ref().parent() {
std::fs::create_dir_all(parent)?;
}
if let Err(e) = std::fs::copy(src.as_ref(), dst.as_ref()) {
log(format!("-> {:?} :: {}", src.as_ref(), e));
}
return Ok(vec![dst.as_ref().to_path_buf()]);
}
let mut copied = vec![];
let mut root_created = false;
for entry in std::fs::read_dir(src.as_ref())? {
let entry = entry?;
let entry_path = entry.path();
let relative_entry = entry_path.strip_prefix(root.as_ref())?;
if ignore.iter().any(|v| {
v.as_ref().eq(relative_entry) || {
let s = v.as_ref().to_string_lossy();
if s.contains('*') {
let parts = s.split('*').collect::<Vec<_>>();
if parts.len() != 2 {
false
} else {
relative_entry.to_string_lossy().starts_with(parts[0])
&& relative_entry.to_string_lossy().ends_with(parts[1])
}
} else {
false
}
}
}) {
continue;
}
log(format!("-> {relative_entry:?}"));
if !root_created {
std::fs::create_dir_all(&dst)?;
root_created = true;
}
let ty = entry.file_type()?;
let dst = dst.as_ref().join(entry.file_name());
if ty.is_dir() {
copied.extend_from_slice(©_all(root.as_ref(), entry.path(), dst, ignore)?);
} else if ty.is_file() {
copied.extend_from_slice(©_if_different(entry.path(), dst)?);
} else if ty.is_symlink() {
symlink(std::fs::canonicalize(entry.path())?, dst);
}
}
Ok(copied)
}
pub fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) {
use std::os::unix::fs::symlink as os_symlink;
match os_symlink(src.as_ref(), dst) {
Ok(_) => (),
Err(e) => {
log(format!("Skip `{}` due to: {:?}", src.as_ref().to_str().unwrap(), e));
}
}
}
fn copy_if_different(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> anyhow::Result<Vec<PathBuf>> {
use std::io::Read;
let src_path = src.as_ref();
let dst_path = dst.as_ref();
let copied = vec![dst_path.to_path_buf()];
if !dst_path.exists() {
std::fs::copy(src_path, dst_path)?;
return Ok(copied);
}
if src_path.metadata()?.len() != dst_path.metadata()?.len() {
std::fs::copy(src_path, dst_path)?;
return Ok(copied);
}
let mut src_file = std::fs::File::open(src_path)?;
let mut dst_file = std::fs::File::open(dst_path)?;
let mut src_buffer = [0; 8192]; let mut dst_buffer = [0; 8192];
loop {
let src_bytes = src_file.read(&mut src_buffer)?;
let dst_bytes = dst_file.read(&mut dst_buffer)?;
if src_bytes != dst_bytes {
std::fs::copy(src_path, dst_path)?;
return Ok(copied);
}
if src_bytes == 0 {
break;
}
if src_buffer[..src_bytes] != dst_buffer[..dst_bytes] {
std::fs::copy(src_path, dst_path)?;
return Ok(copied);
}
}
Ok(copied)
}
pub fn log(s: impl AsRef<str>) {
if VERBOSE.get().is_some_and(|v| *v) {
println!("{}", s.as_ref());
}
}
pub fn generate_build_log_filepath(project_name: &str, pipeline_short_name: &str, cache_dir: &Path) -> PathBuf {
use chrono::Local;
let mut logs_path = PathBuf::new();
logs_path.push(cache_dir);
logs_path.push(CACHE_DIR);
logs_path.push(LOGS_DIR);
if !logs_path.exists() {
std::fs::create_dir_all(logs_path.as_path()).unwrap_or_else(|_| panic!("Can't create `{logs_path:?}` folder!"));
}
let curr_dt = Local::now();
let log_path = logs_path.join(format!(
"{}-{}-{}.txt",
project_name.replace('/', "-"),
pipeline_short_name,
curr_dt.format("%Y-%m-%d-%H:%M")
));
if log_path.exists() {
build_log(&log_path, &[LOG_FILE_DELIMETER.to_string()]).expect("Current log file is unwriteable!");
}
log_path
}
pub fn build_log(path: &Path, output: &[String]) -> anyhow::Result<()> {
use std::io::Write;
let file = File::options().create(true).append(true).open(path)?;
let mut writer = BufWriter::new(file);
for line in output {
let line = strip_ansi_escapes::strip(line.as_bytes());
writer.write_all(&line)?;
writer.write_all("\n".as_bytes())?;
}
Ok(())
}