use std::{fmt, str::FromStr}; use camino::{Utf8Path, Utf8PathBuf}; use time::OffsetDateTime; use tokio::sync::RwLock; use crate::{WikError, WikFile, WikPath, fs::Open, git::GitContext, quotes_decode, quotes_encode}; #[derive(Copy, Clone, Debug, PartialEq, Hash)] pub enum JournalName { Created, Modified, } impl JournalName { pub fn filename(&self) -> &'static str { match self { Self::Created => "created", Self::Modified => "modified", } } } pub struct Journal { files: Vec, } impl Journal { const PATH_JOURNAL: &'static str = ".wikarden/journal"; pub async fn init(root: &Utf8Path) -> Result { let journal_root = root.join(Self::PATH_JOURNAL); std::fs::create_dir_all(&journal_root).unwrap(); Ok(Self { files: vec![ JournalFile::init(JournalName::Created, &journal_root).await?, JournalFile::init(JournalName::Modified, &journal_root).await?, ], }) } pub fn journal(&self, name: JournalName) -> &JournalFile { self.files.iter().find(|jf| jf.name == name).unwrap() } pub async fn journal_tail(&self, name: JournalName, count: usize) -> Vec { let lock = self.journal(name).entries.read().await; let len = lock.len(); let start = len.saturating_sub(count); lock[start..len].to_vec() } pub async fn push(&self, name: JournalName, path: WikPath) -> Result<(), WikError> { let journal = self.journal(name); journal.push(path).await; journal.save().await } pub async fn del(&self, name: JournalName, path: WikPath) -> Result<(), WikError> { let journal = self.journal(name); match journal.del(path).await { None => return Ok(()), Some(_) => { journal.save().await?; Ok(()) } } } pub async fn rename( &self, name: JournalName, from: WikPath, to: WikPath, ) -> Result { let journal = self.journal(name); journal.rename(from, to).await } pub fn git_add(&self, gtx: &mut GitContext) { for jrnl in &self.files { jrnl.git_add(gtx); } } } pub struct JournalFile { name: JournalName, path: Utf8PathBuf, entries: RwLock>, } impl JournalFile { pub async fn init(name: JournalName, root: &Utf8Path) -> Result { let path = root.join(name.filename()); let file = WikFile::open(path.clone(), crate::Open::CreateAppend).await?; let mut entries = vec![]; for line in file.read_to_string().await?.lines() { entries.push(line.parse()?); } Self::sort(&mut entries); Ok(Self { name, path, entries: RwLock::new(entries), }) } pub async fn push(&self, path: WikPath) { let line = JournalLine::now(path.as_ref()); let mut lock = self.entries.write().await; if let Some(idx) = lock.iter().position(|jl| jl.path == path.as_ref()) { lock[idx] = line; } else { lock.push(line); } //TODO: insert the line at the correct position to begin with Self::sort(&mut lock); } pub async fn del(&self, path: WikPath) -> Option { let mut lock = self.entries.write().await; match lock.iter().position(|jl| jl.path.eq(path.as_ref())) { None => None, Some(idx) => Some(lock.remove(idx)), } } pub async fn rename(&self, from: WikPath, to: WikPath) -> Result { let mut lock = self.entries.write().await; match lock.iter_mut().find(|jl| jl.path.eq(from.as_ref())) { None => Ok(false), Some(item) => { item.path = to.as_ref().into(); Self::sort(&mut lock); drop(lock); self.save().await?; Ok(true) } } } pub async fn save(&self) -> Result<(), WikError> { let lock = self.entries.read().await; let mut file = WikFile::open(self.path.clone(), Open::Create).await?; file.write_all_strs(lock.iter()).await } pub fn git_add(&self, gtx: &mut GitContext) { gtx.add_rel(&self.path).unwrap(); } fn sort(slice: &mut [JournalLine]) { slice.sort_by(|a, b| a.time.cmp(&b.time)); } } #[derive(Clone, Debug)] pub struct JournalLine { path: Utf8PathBuf, time: OffsetDateTime, } impl JournalLine { pub fn now>(path: P) -> Self { Self { path: path.into(), time: OffsetDateTime::now_utc(), } } pub fn path(&self) -> String { self.path.to_string() } pub fn epoch(&self) -> i64 { (self.time - OffsetDateTime::UNIX_EPOCH).whole_seconds() } } impl fmt::Display for JournalLine { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "{} {}", quotes_encode(self.path.as_str()), self.epoch()) } } impl FromStr for JournalLine { type Err = WikError; fn from_str(raw: &str) -> Result { let decode = quotes_decode(raw); match decode { None => Err(WikError::journal("string does not start with quote")), Some((path, rest)) => { let time_str = rest.trim_start(); let time: i64 = match time_str.parse() { Err(_) => return Err(WikError::journal("time is not a number")), Ok(stamp) => stamp, }; match OffsetDateTime::from_unix_timestamp(time) { Err(_) => Err(WikError::journal("timestamp is not valid")), Ok(time) => Ok(Self { path: path.into(), time, }), } } } } }