use crate::cli_state::{file_stem, CliState, CliStateError};
use fs2::FileExt;
use ockam_core::errcode::{Kind, Origin};
use ockam_core::{async_trait, Error};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use super::Result;
pub const DATA_DIR_NAME: &str = "data";
#[async_trait]
pub trait StateDirTrait: Sized + Send + Sync {
type Item: StateItemTrait;
const DEFAULT_FILENAME: &'static str;
const DIR_NAME: &'static str;
const HAS_DATA_DIR: bool;
fn new(root_path: &Path) -> Self;
fn default_filename() -> &'static str {
Self::DEFAULT_FILENAME
}
fn build_dir(root_path: &Path) -> PathBuf {
root_path.join(Self::DIR_NAME)
}
fn has_data_dir() -> bool {
Self::HAS_DATA_DIR
}
async fn init(root_path: &Path) -> Result<Self> {
let root = Self::load(root_path)?;
for path in root.list_items_paths()? {
root.migrate(path.as_path()).await?;
}
Ok(root)
}
async fn migrate(&self, _path: &Path) -> Result<()> {
Ok(())
}
fn load(root_path: &Path) -> Result<Self> {
Self::create_dirs(root_path)?;
Ok(Self::new(root_path))
}
fn reset(&self, root_path: &Path) -> Result<PathBuf> {
Self::create_dirs(root_path)
}
fn create_dirs(root_path: &Path) -> Result<PathBuf> {
let dir = Self::build_dir(root_path);
if Self::has_data_dir() {
std::fs::create_dir_all(dir.join(DATA_DIR_NAME))?;
} else {
std::fs::create_dir_all(&dir)?;
};
Ok(dir)
}
fn dir(&self) -> &PathBuf;
fn dir_as_string(&self) -> String {
self.dir().to_string_lossy().to_string()
}
fn path(&self, name: impl AsRef<str>) -> PathBuf {
self.dir().join(format!("{}.json", name.as_ref()))
}
fn overwrite(
&self,
name: impl AsRef<str>,
config: <<Self as StateDirTrait>::Item as StateItemTrait>::Config,
) -> Result<Self::Item> {
let path = self.path(&name);
let state = with_lock(&path, || Self::Item::new(path.clone(), config))?;
if !self.default_path()?.exists() {
self.set_default(&name)?;
}
Ok(state)
}
fn create(
&self,
name: impl AsRef<str>,
config: <<Self as StateDirTrait>::Item as StateItemTrait>::Config,
) -> Result<Self::Item> {
debug!(name = %name.as_ref(), "Creating new config resource");
if self.exists(&name) {
return Err(CliStateError::AlreadyExists {
resource: Self::default_filename().to_string(),
name: name.as_ref().to_string(),
});
}
trace!(name = %name.as_ref(), "Creating config resource instance");
let state = Self::Item::new(self.path(&name), config)?;
if !self.default_path()?.exists() {
self.set_default(&name)?;
}
info!(name = %name.as_ref(), "Created new config resource");
Ok(state)
}
fn get(&self, name: impl AsRef<str>) -> Result<Self::Item> {
if !self.exists(&name) {
return Err(CliStateError::ResourceNotFound {
resource: Self::default_filename().to_string(),
name: name.as_ref().to_string(),
});
}
Self::Item::load(self.path(&name))
}
fn list(&self) -> Result<Vec<Self::Item>> {
let mut items = Vec::default();
for name in self.list_items_names()? {
if let Ok(item) = self.get(name) {
items.push(item);
}
}
Ok(items)
}
fn list_items_names(&self) -> Result<Vec<String>> {
let mut items = Vec::default();
let iter = std::fs::read_dir(self.dir()).map_err(|e| {
let dir = self.dir().as_path().to_string_lossy();
error!(%dir, %e, "Unable to read state directory");
CliStateError::InvalidOperation(format!("Unable to read state from directory {dir}"))
})?;
for entry in iter {
let entry_path = entry?.path();
if self.is_item_path(&entry_path)? {
items.push(file_stem(&entry_path)?);
}
}
Ok(items)
}
fn is_item_path(&self, path: &PathBuf) -> Result<bool> {
let name = file_stem(path)?;
Ok(path.eq(&self.path(name)))
}
fn list_items_paths(&self) -> Result<Vec<PathBuf>> {
let mut items = Vec::default();
for name in self.list_items_names()? {
let path = self.path(name);
items.push(path);
}
Ok(items)
}
fn delete(&self, name: impl AsRef<str>) -> Result<()> {
let s = match self.get(&name) {
Ok(project) => project,
Err(CliStateError::ResourceNotFound { .. }) => return Ok(()),
Err(e) => return Err(e),
};
if let Ok(default) = self.default() {
if default.path() == s.path() {
let _ = std::fs::remove_file(self.default_path()?);
}
}
s.delete()
}
fn default_path(&self) -> Result<PathBuf> {
let root_path = self.dir().parent().expect("Should have parent");
Ok(CliState::defaults_dir(root_path)?.join(Self::default_filename()))
}
fn default(&self) -> Result<Self::Item> {
let path = std::fs::canonicalize(self.default_path()?)?;
Self::Item::load(path)
}
fn set_default(&self, name: impl AsRef<str>) -> Result<()> {
debug!(name = %name.as_ref(), "Setting default item");
if !self.exists(&name) {
return Err(CliStateError::ResourceNotFound {
resource: Self::default_filename().to_string(),
name: name.as_ref().to_string(),
});
}
let original = self.path(&name);
let link = self.default_path()?;
info!("removing link {:?}", link);
let _ = std::fs::remove_file(&link);
info!("symlink to {:?}", original);
std::fs::create_dir_all(link.parent().unwrap())
.map_err(|e| Error::new(Origin::Node, Kind::Io, e))?;
std::os::unix::fs::symlink(original, link)?;
info!(name = %name.as_ref(), "Set default item");
Ok(())
}
fn is_default(&self, name: impl AsRef<str>) -> Result<bool> {
if !self.exists(&name) {
return Ok(false);
}
let default_name = {
let path = std::fs::canonicalize(self.default_path()?)?;
file_stem(&path)?
};
Ok(default_name.eq(name.as_ref()))
}
fn is_empty(&self) -> Result<bool> {
for entry in std::fs::read_dir(self.dir())? {
let name = file_stem(&entry?.path())?;
if self.get(name).is_ok() {
return Ok(false);
}
}
Ok(true)
}
fn exists(&self, name: impl AsRef<str>) -> bool {
self.path(&name).exists()
}
}
#[async_trait]
pub trait StateItemTrait: Sized + Send {
type Config: Serialize + for<'a> Deserialize<'a> + Send;
fn new(path: PathBuf, config: Self::Config) -> Result<Self>;
fn load(path: PathBuf) -> Result<Self>;
fn persist(&self) -> Result<()> {
with_lock(self.path(), || {
let contents = serde_json::to_string(self.config())?;
std::fs::write(self.path(), contents)?;
Ok(())
})
}
fn delete(&self) -> Result<()> {
with_lock(self.path(), || {
std::fs::remove_file(self.path())?;
Ok(())
})?;
let _ = std::fs::remove_file(self.path().with_extension("lock"));
Ok(())
}
fn path(&self) -> &PathBuf;
fn config(&self) -> &Self::Config;
}
fn with_lock<T>(path: &Path, f: impl FnOnce() -> Result<T>) -> Result<T> {
let lock_file = std::fs::OpenOptions::new()
.write(true)
.read(true)
.create(true)
.open(path.with_extension("lock"))?;
lock_file.lock_exclusive()?;
let res = f();
lock_file.unlock()?;
res
}
#[cfg(test)]
mod tests {
use crate::cli_state::{StateDirTrait, StateItemTrait};
use std::path::{Path, PathBuf};
#[test]
fn test_is_item_path() {
let config = TestConfig::new(Path::new("dir"));
let path = config.path("name");
assert!(config.is_item_path(&path).unwrap())
}
struct TestConfig {
dir: PathBuf,
}
impl StateDirTrait for TestConfig {
type Item = TestConfigItem;
const DEFAULT_FILENAME: &'static str = "test";
const DIR_NAME: &'static str = "test";
const HAS_DATA_DIR: bool = false;
fn new(root_path: &Path) -> Self {
let dir = Self::build_dir(root_path);
std::fs::create_dir_all(&dir).unwrap();
Self { dir }
}
fn dir(&self) -> &PathBuf {
&self.dir
}
}
struct TestConfigItem {
path: PathBuf,
config: u32,
}
impl StateItemTrait for TestConfigItem {
type Config = u32;
fn new(path: PathBuf, config: Self::Config) -> crate::cli_state::Result<Self> {
let contents = serde_json::to_string(&config)?;
std::fs::write(&path, contents)?;
Ok(Self { path, config })
}
fn load(path: PathBuf) -> crate::cli_state::Result<Self> {
let contents = std::fs::read_to_string(&path)?;
let config = serde_json::from_str(&contents)?;
Ok(TestConfigItem { path, config })
}
fn path(&self) -> &PathBuf {
&self.path
}
fn config(&self) -> &Self::Config {
&self.config
}
}
}