#[cfg(test)]
mod test;
use crate::prelude::*;
use super::dir;
use anyhow::{format_err, Context};
use async_fd_lock::{LockRead, LockWrite};
use tokio::fs::{self, File};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use std::cmp;
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
pub type Projects = BTreeSet<Project>;
#[derive(
Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
pub struct Project {
pub last_closed_time: cmp::Reverse<Option<Timestamp>>,
pub path: PathBuf,
}
#[derive(Debug, Error)]
#[error("Another file-lock is blocking")]
pub struct BlockingLockError;
pub async fn read_projects() -> Result<Projects, ProjectsFileErr> {
let path = projects_path();
let path_str = path.to_string_lossy();
let file = match File::options().create(false).read(true).open(&path).await
{
Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => {
return Ok(Projects::default());
}
result => result
.with_context(|| format!("Opening projects file ({path_str})"))
.map_err(OneOf::new)?,
};
let mut read_guard = match file.try_lock_read().await {
Err(err)
if matches!(err.error.kind(), std::io::ErrorKind::WouldBlock) =>
{
return Err(OneOf::new(BlockingLockError));
}
result => result
.map_err(|e| {
format_err!(
"Obtaining projects file read-lock failed with {e:?} ({path_str})"
)
})
.map_err(OneOf::new)?,
};
let mut buffer = Vec::new();
read_guard
.read_to_end(&mut buffer)
.await
.with_context(|| format!("Reading projects file ({path_str})"))
.map_err(OneOf::new)?;
let stored: ProjectsStored = toml_edit::de::from_slice(&buffer)
.with_context(|| format!("Decoding projects file ({path_str})"))
.map_err(OneOf::new)?;
Ok(from_stored(stored))
}
pub async fn upsert_project(
project: Project,
) -> Result<(), (ProjectsFileErr, Project)> {
let project_clone = project.clone();
with_projects(move |mut projects| {
let Project {
last_closed_time,
path,
} = project_clone;
let last_closed_time = if let Some(ProjectStored {
last_closed_time: current_last_time,
..
}) = projects.remove(&path)
{
cmp::min(last_closed_time, current_last_time)
} else {
last_closed_time
};
let project = ProjectStored { last_closed_time };
projects.insert(path, project);
projects
})
.await
.map_err(|e| (e, project))
}
pub fn updated_closed_time_blocking(
path: PathBuf,
) -> Result<(), ProjectsFileErr> {
with_projects_blocking(move |mut projects| {
let project = ProjectStored {
last_closed_time: cmp::Reverse(Some(Timestamp::now())),
};
projects.insert(path, project);
projects
})
}
pub async fn rm_project(path: PathBuf) -> Result<(), ProjectsFileErr> {
with_projects(move |mut projects| {
projects.remove(&path);
projects
})
.await
}
type ProjectsStored = BTreeMap<PathBuf, ProjectStored>;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ProjectStored {
last_closed_time: cmp::Reverse<Option<Timestamp>>,
}
fn from_stored(state: ProjectsStored) -> Projects {
state
.into_iter()
.map(|(path, ProjectStored { last_closed_time })| Project {
last_closed_time,
path,
})
.collect()
}
async fn with_projects<F: FnOnce(ProjectsStored) -> ProjectsStored>(
f: F,
) -> Result<(), ProjectsFileErr> {
let path = projects_path();
let path_str = path.to_string_lossy();
fs::create_dir_all(path.parent().unwrap())
.await
.with_context(|| {
format!("Creating projects file store directories ({path_str})")
})
.map_err(OneOf::new)?;
let read_state = async || {
let file = match File::options()
.create(false)
.read(true)
.open(&path)
.await
{
Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => {
return Ok(ProjectsStored::default());
}
result => result
.with_context(|| format!("Opening projects file ({path_str})"))
.map_err(OneOf::new)?,
};
let mut read_guard = match file.lock_read().await {
Err(err)
if matches!(err.error.kind(), std::io::ErrorKind::WouldBlock) =>
{
return Err(OneOf::new(BlockingLockError));
}
result => result
.map_err(|e| {
format_err!(
"Obtaining projects file read-lock failed with {e:?} ({path_str})"
)
})
.map_err(OneOf::new)?,
};
let mut buffer = Vec::new();
read_guard
.read_to_end(&mut buffer)
.await
.with_context(|| format!("Reading projects file ({path_str})"))
.map_err(OneOf::new)?;
let stored: ProjectsStored = toml_edit::de::from_slice(&buffer)
.with_context(|| format!("Decoding projects file ({path_str})"))
.map_err(OneOf::new)?;
Ok(stored)
};
let state = read_state().await?;
let file = File::options()
.create(true)
.write(true)
.truncate(true)
.open(&path)
.await
.with_context(|| format!("Opening projects file ({path_str})"))
.map_err(OneOf::new)?;
let mut write_guard = match file.lock_write().await {
Err(err)
if matches!(err.error.kind(), std::io::ErrorKind::WouldBlock) =>
{
return Err(OneOf::new(BlockingLockError));
}
result => result
.map_err(|e| {
format_err!(
"Obtaining projects file write-lock failed with {e:?} ({path_str})"
)
})
.map_err(OneOf::new)?,
};
let state = f(state);
let state_bytes = toml_edit::ser::to_string_pretty(&state)
.with_context(|| format!("Encoding projects file ({path_str})"))
.map_err(OneOf::new)?;
write_guard
.write_all(state_bytes.as_bytes())
.await
.with_context(|| format!("Writing projects file ({path_str})"))
.map_err(OneOf::new)
}
fn with_projects_blocking<F: FnOnce(ProjectsStored) -> ProjectsStored>(
f: F,
) -> Result<(), ProjectsFileErr> {
use async_fd_lock::blocking::{LockRead, LockWrite};
use std::fs;
use std::io::{Read, Write};
let path = projects_path();
let path_str = path.to_string_lossy();
fs::create_dir_all(path.parent().unwrap())
.with_context(|| {
format!("Creating projects file store directories ({path_str})")
})
.map_err(OneOf::new)?;
let read_state = || {
let file = match fs::File::options()
.create(false)
.read(true)
.open(&path)
{
Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => {
return Ok(ProjectsStored::default());
}
result => result
.with_context(|| format!("Opening projects file ({path_str})"))
.map_err(OneOf::new)?,
};
let mut read_guard = match file.lock_read() {
Err(err)
if matches!(err.error.kind(), std::io::ErrorKind::WouldBlock) =>
{
return Err(OneOf::new(BlockingLockError));
}
result => result
.map_err(|e| {
format_err!(
"Obtaining projects file read-lock failed with {e:?} ({path_str})"
)
})
.map_err(OneOf::new)?,
};
let mut buffer = Vec::new();
read_guard
.read_to_end(&mut buffer)
.with_context(|| format!("Reading projects file ({path_str})"))
.map_err(OneOf::new)?;
let stored: ProjectsStored = toml_edit::de::from_slice(&buffer)
.with_context(|| format!("Decoding projects file ({path_str})"))
.map_err(OneOf::new)?;
Ok(stored)
};
let state = read_state()?;
let file = fs::File::options()
.create(true)
.write(true)
.truncate(true)
.open(&path)
.with_context(|| format!("Opening projects file ({path_str})"))
.map_err(OneOf::new)?;
let mut write_guard = match file.lock_write() {
Err(err)
if matches!(err.error.kind(), std::io::ErrorKind::WouldBlock) =>
{
return Err(OneOf::new(BlockingLockError));
}
result => result
.map_err(|e| {
format_err!(
"Obtaining projects file write-lock failed with {e:?} ({path_str})"
)
})
.map_err(OneOf::new)?,
};
let state = f(state);
let state_bytes = toml_edit::ser::to_string_pretty(&state)
.with_context(|| format!("Encoding projects file ({path_str})"))
.map_err(OneOf::new)?;
write_guard
.write_all(state_bytes.as_bytes())
.with_context(|| format!("Writing projects file ({path_str})"))
.map_err(OneOf::new)
}
pub type ProjectsFileErr = OneOf<(BlockingLockError, anyhow::Error)>;
fn projects_path() -> PathBuf {
PathBuf::from_iter([dir::data().as_path(), Path::new(PROJECTS_FILE)])
}
const PROJECTS_FILE: &str = "projects.toml";