#![allow(clippy::use_self)]
use crate::todo::ToDoUpdate;
pub use super::todo::{Priority, Status, ToDo, ToDoList};
use anyhow::{bail, Context, Result};
use tempfile::{NamedTempFile, PersistError};
use std::io::Write;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
pub trait State {}
pub struct Clean;
pub struct Dirty;
impl State for Clean {}
impl State for Dirty {}
pub struct Database<S: State> {
todos: ToDoList,
data_dir: PathBuf,
status: PhantomData<S>,
}
impl<S: State> Database<S> {
pub fn add(mut self, todo: ToDo) -> Database<Dirty> {
self.todos.push(todo);
println!("Successfully added the item.");
Database::<Dirty> {
todos: self.todos,
data_dir: self.data_dir,
status: PhantomData::default(),
}
}
pub fn delete(mut self, id: usize) -> Database<Dirty> {
if self.todos.get(id).is_some() {
self.todos.remove(id);
println!("Successfully removed the item");
}
Database::<Dirty> {
todos: self.todos,
data_dir: self.data_dir,
status: PhantomData::default(),
}
}
pub fn update(mut self, id: usize, update: ToDoUpdate) -> Database<Dirty> {
if let Some(todo) = self.todos.get_mut(id) {
if let Some(desc) = update.desc {
todo.desc = desc;
}
if let Some(due) = update.due {
todo.due = due;
}
if let Some(start) = update.start {
todo.start = start;
}
if let Some(prio) = update.prio {
todo.prio = prio;
}
if let Some(tags) = update.tags {
todo.tags = tags;
}
if let Some(recur) = update.recur {
todo.recur = recur;
}
if let Some(status) = update.status {
todo.status = status;
}
}
Database::<Dirty> {
todos: self.todos,
data_dir: self.data_dir,
status: PhantomData::default(),
}
}
pub fn todos(&self) -> &ToDoList {
&self.todos
}
}
impl Database<Clean> {
pub fn from_clido_dir() -> Result<Self> {
Self::from_path(clido_dir()?)
}
pub fn from_path<P: Into<PathBuf>>(data_dir: P) -> Result<Self> {
let data_dir = data_dir.into();
let path = list_path(&data_dir);
match std::fs::read(&path) {
Ok(buffer) => {
let todos = bincode::deserialize(&buffer).with_context(|| {
format!("Could not deserialize todo-list: {}", path.display())
})?;
Ok(Self {
todos,
data_dir,
status: PhantomData::default(),
})
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
std::fs::create_dir_all(&data_dir).expect("Unable to create dir");
Ok(Self {
todos: Vec::new().into(),
data_dir,
status: PhantomData::default(),
})
}
Err(e) => {
Err(e).with_context(|| format!("could not read from database: {}", path.display()))
}
}
}
}
impl Database<Dirty> {
pub fn save(&self) -> Result<()> {
let mut file = NamedTempFile::new_in(&self.data_dir).with_context(|| {
format!(
"Could not create temp. database in: {}",
self.data_dir.display()
)
})?;
let bytes = bincode::serialize(&self.todos)?;
let _ = file.as_file().set_len(bytes.len() as u64);
file.write_all(&bytes).with_context(|| {
format!("Couldn't write to temp database: {}", file.path().display(),)
})?;
let path = list_path(&self.data_dir);
persist(file, &path)
.with_context(|| format!("Couldn't replace temp database: {}", path.display(),))?;
Ok(())
}
}
#[cfg(windows)]
fn persist<P: AsRef<Path>>(mut file: NamedTempFile, path: P) -> Result<(), PersistError> {
use rand::distributions::{Distribution, Uniform};
use std::thread;
use std::time::Duration;
const MAX_TRIES: usize = 10;
let mut rng = None;
for _ in 0..MAX_TRIES {
match file.persist(&path) {
Ok(_) => break,
Err(e) if e.error.kind() == io::ErrorKind::PermissionDenied => {
let mut rng = rng.get_or_insert_with(rand::thread_rng);
let between = Uniform::from(50..150);
let duration = Duration::from_millis(between.sample(&mut rng));
thread::sleep(duration);
file = e.file;
}
Err(e) => return Err(e),
}
}
Ok(())
}
#[cfg(unix)]
fn persist<P: AsRef<Path>>(file: NamedTempFile, path: P) -> Result<(), PersistError> {
file.persist(&path)?;
Ok(())
}
fn list_path<P: AsRef<Path>>(data_dir: P) -> std::path::PathBuf {
const DB_FILENAME: &str = "clido.db";
data_dir.as_ref().join(DB_FILENAME)
}
fn clido_dir() -> Result<PathBuf> {
let data_dir = match std::env::var_os("_CLIDO_DIR") {
Some(os_str) => PathBuf::from(os_str),
None => match dirs_next::data_local_dir() {
Some(mut dir) => {
dir.push("clido");
dir
}
None => bail!("Could not find data directory. Please set _CLIDO_DIR."),
},
};
Ok(data_dir)
}