use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use xtalforge_core::Error;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunRecord {
pub id: String,
pub module: String,
pub input: String,
pub status: RunStatus,
pub outputs: Vec<String>,
pub data: serde_json::Value,
pub created: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RunStatus {
Pending,
Running,
Done,
Failed,
}
#[derive(Debug, Clone)]
pub struct Workspace {
root: PathBuf,
}
impl Workspace {
#[cfg(not(target_arch = "wasm32"))]
pub fn open(root: impl Into<PathBuf>) -> Result<Self, Error> {
let root = root.into();
std::fs::create_dir_all(&root).map_err(|error| {
Error::InvalidInput(format!(
"could not create workspace {}: {error}",
root.display()
))
})?;
Ok(Self { root })
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
#[must_use]
pub fn run_dir(&self, id: &str) -> PathBuf {
self.root.join("runs").join(id)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn write_run(&self, record: &RunRecord) -> Result<PathBuf, Error> {
let directory = self.run_dir(&record.id);
std::fs::create_dir_all(&directory).map_err(|error| {
Error::InvalidInput(format!(
"could not create run {}: {error}",
directory.display()
))
})?;
let path = directory.join("run.json");
let text = serde_json::to_string_pretty(record)
.map_err(|error| Error::InvalidInput(format!("could not serialize run: {error}")))?;
std::fs::write(&path, text).map_err(|error| {
Error::InvalidInput(format!("could not write {}: {error}", path.display()))
})?;
Ok(path)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn read_run(&self, id: &str) -> Result<RunRecord, Error> {
let path = self.run_dir(id).join("run.json");
let text = std::fs::read_to_string(&path).map_err(|error| {
Error::InvalidInput(format!("could not read {}: {error}", path.display()))
})?;
serde_json::from_str(&text).map_err(|error| {
Error::InvalidInput(format!("invalid run record {}: {error}", path.display()))
})
}
#[cfg(not(target_arch = "wasm32"))]
pub fn runs(&self) -> Result<Vec<String>, Error> {
let directory = self.root.join("runs");
let entries = match std::fs::read_dir(&directory) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => {
return Err(Error::InvalidInput(format!(
"could not read {}: {error}",
directory.display()
)));
}
};
let mut ids = Vec::new();
for entry in entries.flatten() {
if entry.path().is_dir()
&& let Some(name) = entry.file_name().to_str()
{
ids.push(name.to_owned());
}
}
ids.sort();
Ok(ids)
}
}
pub fn archive_records(records: &[RunRecord]) -> Result<Vec<u8>, Error> {
use std::io::Write;
let mut buffer = std::io::Cursor::new(Vec::new());
{
let mut archive = zip::ZipWriter::new(&mut buffer);
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o644);
for record in records {
let text = serde_json::to_vec_pretty(record).map_err(|error| {
Error::InvalidInput(format!("could not serialize run {}: {error}", record.id))
})?;
archive
.start_file(format!("runs/{}/run.json", record.id), options)
.map_err(archive_error)?;
archive.write_all(&text).map_err(archive_error)?;
}
archive.finish().map_err(archive_error)?;
}
Ok(buffer.into_inner())
}
pub fn read_archived_records(bytes: &[u8]) -> Result<Vec<RunRecord>, Error> {
use std::io::Read;
let cursor = std::io::Cursor::new(bytes);
let mut archive = zip::ZipArchive::new(cursor).map_err(archive_error)?;
let mut records: Vec<RunRecord> = Vec::new();
for index in 0..archive.len() {
let mut file = archive.by_index(index).map_err(archive_error)?;
let name = file.name().map_err(archive_error)?;
if !name.ends_with("/run.json") {
continue;
}
let mut text = String::new();
file.read_to_string(&mut text).map_err(archive_error)?;
records.push(
serde_json::from_str(&text)
.map_err(|error| Error::InvalidInput(format!("invalid run record: {error}")))?,
);
}
records.sort_by(|first, second| first.id.cmp(&second.id));
Ok(records)
}
fn archive_error(error: impl std::fmt::Display) -> Error {
Error::InvalidInput(format!("workspace archive: {error}"))
}