use std::{
collections::HashMap,
fs::{read, write},
io::Write,
path::{Path, PathBuf},
};
use log::debug;
use serde::{Deserialize, Serialize};
use crate::{
action::{TrustedAction, UnsafeAction},
tildepathbuf::TildePathBuf,
};
#[derive(Debug, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct Projects {
projects: HashMap<String, Project>,
}
impl Projects {
pub fn from_file(filename: &Path) -> Result<Self, ProjectError> {
let dirname = if let Some(parent) = filename.parent() {
parent.to_path_buf()
} else {
return Err(ProjectError::Parent(filename.into()));
};
let yaml = read(filename).map_err(|e| ProjectError::Read(filename.into(), e))?;
let mut projects: Self =
serde_yml::from_slice(&yaml).map_err(|e| ProjectError::Yaml(filename.into(), e))?;
for (_, p) in projects.projects.iter_mut() {
p.expand_tilde(&dirname)?;
}
Ok(projects)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &Project)> {
self.projects.iter().map(|(k, v)| (k.as_str(), v))
}
}
#[derive(Debug, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct Project {
source: TildePathBuf,
#[serde(skip)]
expanded_source: PathBuf,
image: TildePathBuf,
#[serde(skip)]
expanded_image: PathBuf,
pre_plan: Option<Vec<TrustedAction>>,
plan: Option<Vec<UnsafeAction>>,
post_plan: Option<Vec<TrustedAction>>,
artifact_max_size: Option<u64>,
cache_max_size: Option<u64>,
}
impl Project {
fn expand_tilde(&mut self, basedir: &Path) -> Result<(), ProjectError> {
self.expanded_source = Self::abspath(basedir.join(self.source.path()))?;
self.expanded_image = Self::abspath(basedir.join(self.image.path()))?;
Ok(())
}
pub fn from_file(filename: &Path) -> Result<Self, ProjectError> {
let dirname = if let Some(parent) = filename.parent() {
parent.to_path_buf()
} else {
return Err(ProjectError::Parent(filename.into()));
};
let yaml = read(filename).map_err(|e| ProjectError::Read(filename.into(), e))?;
let mut project: Project =
serde_yml::from_slice(&yaml).map_err(|e| ProjectError::Yaml(filename.into(), e))?;
project.expand_tilde(&dirname)?;
debug!("project from file {}: {:#?}", filename.display(), project);
Ok(project)
}
fn abspath(path: PathBuf) -> Result<PathBuf, ProjectError> {
path.canonicalize()
.map_err(|e| ProjectError::Canonicalize(path, e))
}
pub fn source(&self) -> &Path {
&self.expanded_source
}
pub fn image(&self) -> &Path {
&self.expanded_image
}
pub fn artifact_max_size(&self) -> Option<u64> {
self.artifact_max_size
}
pub fn cache_max_size(&self) -> Option<u64> {
self.cache_max_size
}
pub fn pre_plan(&self) -> &[TrustedAction] {
if let Some(plan) = &self.pre_plan {
plan.as_slice()
} else {
&[]
}
}
pub fn plan(&self) -> &[UnsafeAction] {
if let Some(plan) = &self.plan {
plan.as_slice()
} else {
&[]
}
}
pub fn post_plan(&self) -> &[TrustedAction] {
if let Some(plan) = &self.post_plan {
plan.as_slice()
} else {
&[]
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[allow(dead_code)]
pub struct State {
#[serde(skip)]
filename: PathBuf,
#[serde(skip)]
statedir: PathBuf,
latest_commit: Option<String>,
}
impl State {
pub fn from_file(statedir: &Path, project: &str) -> Result<Self, ProjectError> {
let statedir = statedir.join(project);
let filename = statedir.join("meta.yaml");
debug!("load project state from {}", filename.display());
if filename.exists() {
let yaml = read(&filename).map_err(|e| ProjectError::ReadState(filename.clone(), e))?;
let mut state: Self = serde_yml::from_slice(&yaml)
.map_err(|e| ProjectError::ParseState(filename.clone(), e))?;
state.filename = filename;
state.statedir = statedir;
Ok(state)
} else {
Ok(Self {
filename,
statedir,
latest_commit: None,
})
}
}
pub fn write_to_file(&self) -> Result<(), ProjectError> {
debug!("write project state to {}", self.filename.display());
let yaml = serde_yml::to_string(&self)
.map_err(|e| ProjectError::SerializeState(self.clone(), e))?;
if !self.statedir.exists() {
std::fs::create_dir(&self.statedir)
.map_err(|e| ProjectError::CreateState(self.statedir.clone(), e))?;
}
write(&self.filename, yaml)
.map_err(|e| ProjectError::WriteState(self.filename.clone(), e))?;
Ok(())
}
pub fn statedir(&self) -> &Path {
&self.statedir
}
pub fn artifactsdir(&self) -> PathBuf {
self.statedir.join("artifacts")
}
pub fn cachedir(&self) -> PathBuf {
self.statedir.join("cache")
}
pub fn dependenciesdir(&self) -> PathBuf {
self.statedir.join("dependencies")
}
pub fn latest_commit(&self) -> Option<&str> {
self.latest_commit.as_deref()
}
pub fn set_latest_commot(&mut self, commit: Option<&str>) {
self.latest_commit = commit.map(|s| s.into());
}
fn run_log_filename(&self) -> PathBuf {
self.statedir.join("run.log")
}
pub fn remove_run_log(&self) -> Result<(), ProjectError> {
let filename = self.run_log_filename();
debug!("removing run log file {}", filename.display());
debug!(
"statedir is {}, exists? {}",
self.statedir.display(),
self.statedir.exists()
);
if filename.exists() {
std::fs::remove_file(&filename)
.map_err(|err| ProjectError::RemoveRunLog(filename, err))?;
}
Ok(())
}
pub fn create_run_log(&self) -> Result<PathBuf, ProjectError> {
let filename = self.run_log_filename();
debug!("creating run log file {}", filename.display());
std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&filename)
.map_err(|err| ProjectError::CreateRunLog(filename.clone(), err))?;
debug!("created run log file {} OK", filename.display());
Ok(filename)
}
pub fn append_to_run_log(&self, data: &[u8]) -> Result<(), ProjectError> {
let filename = self.run_log_filename();
let mut file = std::fs::OpenOptions::new()
.append(true)
.open(&filename)
.map_err(|err| ProjectError::CreateRunLog(filename.clone(), err))?;
file.write_all(data)
.map_err(|err| ProjectError::AppendToRunLog(filename, err))?;
Ok(())
}
pub fn read_run_log(&self) -> Result<Vec<u8>, ProjectError> {
let filename = self.run_log_filename();
let data =
std::fs::read(&filename).map_err(|err| ProjectError::ReadRunLog(filename, err))?;
Ok(data)
}
}
#[derive(Debug, thiserror::Error)]
pub enum ProjectError {
#[error("failed to determine directory containing project file {0}")]
Parent(PathBuf),
#[error("failed to make filename absolute: {0}")]
Canonicalize(PathBuf, #[source] std::io::Error),
#[error("failed top read project file {0}")]
Read(PathBuf, #[source] std::io::Error),
#[error("failed to parse project file as YAML: {0}")]
Yaml(PathBuf, #[source] serde_yml::Error),
#[error("failed to serialize project state as YAML: {0:#?}")]
SerializeState(State, #[source] serde_yml::Error),
#[error("failed to write project state to file {0}")]
WriteState(PathBuf, #[source] std::io::Error),
#[error("failed to read project state from file {0}")]
ReadState(PathBuf, #[source] std::io::Error),
#[error("failed to parse project state file as YAML: {0}")]
ParseState(PathBuf, #[source] serde_yml::Error),
#[error("failed to create project state directory {0}")]
CreateState(PathBuf, #[source] std::io::Error),
#[error("failed to remove run log file {0}")]
RemoveRunLog(PathBuf, #[source] std::io::Error),
#[error("failed to create run log file {0}")]
CreateRunLog(PathBuf, #[source] std::io::Error),
#[error("failed to append to run log file {0}")]
AppendToRunLog(PathBuf, #[source] std::io::Error),
#[error("failed to read run log file {0}")]
ReadRunLog(PathBuf, #[source] std::io::Error),
}