use std::fs;
use std::path::{Component, Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use crate::clock::utc_now_rfc3339;
use super::error::ExecutionScopeError;
static EXECUTION_SEQUENCE: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExecutionScope {
directory: PathBuf,
created_at_utc: Option<String>,
}
impl ExecutionScope {
pub fn open_or_create(recording_root: impl AsRef<Path>) -> Result<Self, ExecutionScopeError> {
let directory = recording_root.as_ref().to_path_buf();
fs::create_dir_all(&directory).map_err(|source| ExecutionScopeError::Io {
operation: "create deterministic",
path: directory.clone(),
source,
})?;
Ok(Self {
directory,
created_at_utc: None,
})
}
pub fn create_generated(recording_root: impl AsRef<Path>) -> Result<Self, ExecutionScopeError> {
let recording_root = recording_root.as_ref();
fs::create_dir_all(recording_root).map_err(|source| ExecutionScopeError::Io {
operation: "create recording root for",
path: recording_root.to_path_buf(),
source,
})?;
let created_at_utc =
utc_now_rfc3339().map_err(|source| ExecutionScopeError::Timestamp { source })?;
let compact_timestamp = compact_timestamp(&created_at_utc);
for _ in 0..1024 {
let sequence = EXECUTION_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let name = format!(
"execution-{compact_timestamp}-{}-{sequence}",
std::process::id()
);
let directory = recording_root.join(name);
match fs::create_dir(&directory) {
Ok(()) => {
return Ok(Self {
directory,
created_at_utc: Some(created_at_utc),
});
}
Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(source) => {
return Err(ExecutionScopeError::Io {
operation: "create generated",
path: directory,
source,
});
}
}
}
Err(ExecutionScopeError::IdentityExhausted {
root: recording_root.to_path_buf(),
})
}
pub fn create_named(
recording_root: impl AsRef<Path>,
name: impl Into<String>,
) -> Result<Self, ExecutionScopeError> {
let recording_root = recording_root.as_ref();
let name = name.into();
validate_name(&name)?;
let created_at_utc =
utc_now_rfc3339().map_err(|source| ExecutionScopeError::Timestamp { source })?;
fs::create_dir_all(recording_root).map_err(|source| ExecutionScopeError::Io {
operation: "create recording root for",
path: recording_root.to_path_buf(),
source,
})?;
let directory = recording_root.join(&name);
fs::create_dir(&directory).map_err(|source| ExecutionScopeError::Io {
operation: "create named",
path: directory.clone(),
source,
})?;
Ok(Self {
directory,
created_at_utc: Some(created_at_utc),
})
}
pub fn open_existing(directory: impl Into<PathBuf>) -> Result<Self, ExecutionScopeError> {
let directory = directory.into();
let metadata = fs::metadata(&directory).map_err(|source| ExecutionScopeError::Io {
operation: "inspect existing",
path: directory.clone(),
source,
})?;
if !metadata.is_dir() {
return Err(ExecutionScopeError::Io {
operation: "open non-directory",
path: directory,
source: std::io::Error::new(
std::io::ErrorKind::NotADirectory,
"execution scope must be a directory",
),
});
}
Ok(Self {
directory,
created_at_utc: None,
})
}
pub fn directory(&self) -> &Path {
&self.directory
}
pub fn created_at_utc(&self) -> Option<&str> {
self.created_at_utc.as_deref()
}
pub fn task_recording_directory(&self, ordinal: u64) -> PathBuf {
self.directory.join(format!("task-{ordinal:06}"))
}
pub fn named_task_recording_directory(
&self,
name: &str,
) -> Result<PathBuf, ExecutionScopeError> {
validate_relative_name(name)?;
Ok(self.directory.join(name))
}
}
fn validate_name(name: &str) -> Result<(), ExecutionScopeError> {
validate_path_components(name, false)
}
fn validate_relative_name(name: &str) -> Result<(), ExecutionScopeError> {
validate_path_components(name, true)
}
fn validate_path_components(name: &str, allow_relative: bool) -> Result<(), ExecutionScopeError> {
let path = Path::new(name);
let mut components = path.components();
let valid = !name.trim().is_empty()
&& matches!(components.next(), Some(Component::Normal(_)))
&& if allow_relative {
components.all(|component| matches!(component, Component::Normal(_)))
} else {
components.next().is_none()
};
if valid {
Ok(())
} else {
Err(ExecutionScopeError::InvalidName {
name: name.to_owned(),
})
}
}
fn compact_timestamp(timestamp: &str) -> String {
timestamp
.chars()
.filter(|character| character.is_ascii_alphanumeric())
.collect()
}