use std::fs::{self, OpenOptions};
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use crate::entry::{Entry, EntryKind, EntryLine, Interval};
use crate::error::StorageError;
use crate::repository::{QueryOpts, Repository};
use anyhow::{Result, anyhow};
use chrono::{DateTime, TimeDelta, Utc};
use std::io::{self, BufRead, BufReader, Read, Seek, Write};
pub struct FileRepository {
path: Option<PathBuf>,
entries: Vec<Entry>,
initial_entry_count: usize,
}
impl FileRepository {
pub fn new(path: Option<PathBuf>) -> anyhow::Result<FileRepository> {
let reader: Box<dyn io::Read> = match &path {
Some(p) => Box::new(
OpenOptions::new()
.read(true)
.create(true)
.truncate(false)
.write(true)
.open(p)?,
),
None => Box::new(io::stdin()),
};
let entries = FileRepository::load(reader)?;
let initial_entry_count = entries.len();
Ok(FileRepository {
path,
entries,
initial_entry_count,
})
}
fn load(reader: Box<dyn io::Read>) -> anyhow::Result<Vec<Entry>> {
let lines: Vec<EntryLine> = BufReader::new(reader)
.lines()
.filter_map(|line| {
let line = line.ok()?;
(!line.trim().is_empty()).then(|| EntryLine::from_str(&line))
})
.collect::<Result<_, _>>()?;
let (pairs, tail) = if lines.last().is_some_and(|l| l.kind == EntryKind::Start) {
(&lines[..lines.len() - 1], lines.last())
} else {
(&lines[..], None)
};
let mut entries: Vec<Entry> = pairs
.chunks(2)
.map(|pair| Entry::new(&pair[0], &pair[1]).map_err(Into::into))
.collect::<Result<_, StorageError>>()?;
if let Some(ongoing) = tail {
entries.push(Entry {
desc: ongoing.desc.clone(),
interval: Interval {
start: ongoing.dt,
end: None,
},
});
}
Ok(entries)
}
fn append_lines(&self, lines: &[EntryLine]) -> Result<(), StorageError> {
let mut writer: Box<dyn Write> = match &self.path {
Some(path) => {
if let Ok(metadata) = fs::metadata(path)
&& metadata.len() > 0
{
let mut file = fs::File::open(path)?;
file.seek(io::SeekFrom::End(-1))?;
let mut buf = [0u8; 1];
file.read_exact(&mut buf)?;
if buf[0] != b'\n' {
let mut f = fs::OpenOptions::new().append(true).open(path)?;
writeln!(f)?;
}
}
Box::new(fs::OpenOptions::new().append(true).open(path)?)
}
None => return Ok(()),
};
for line in lines {
writeln!(writer, "{line}")?;
}
Ok(())
}
}
impl Repository for FileRepository {
fn list(&self, opts: QueryOpts) -> Result<Vec<Entry>, StorageError> {
Ok(self
.entries
.iter()
.filter(|e| {
e.interval.start >= opts.from.unwrap_or(DateTime::<Utc>::MIN_UTC)
&& e.interval.start <= opts.to.unwrap_or(DateTime::<Utc>::MAX_UTC)
})
.skip(opts.offset.unwrap_or(0))
.take(opts.limit.unwrap_or(usize::MAX))
.map(|e| e.to_owned())
.collect())
}
fn start_session(&mut self, desc: Arc<str>, dt: DateTime<Utc>) -> Result<()> {
let mut to_write: Vec<EntryLine> = Vec::with_capacity(2);
if let Some(entry) = self.entries.pop_if(|e| e.interval.end.is_none()) {
let end_dt = dt - TimeDelta::seconds(1);
to_write.push(EntryLine {
kind: EntryKind::End,
desc: entry.desc.clone(),
dt: end_dt,
});
self.entries.push(Entry {
desc: entry.desc,
interval: Interval {
start: entry.interval.start,
end: Some(end_dt),
},
});
}
to_write.push(EntryLine {
kind: EntryKind::Start,
desc: desc.clone(),
dt,
});
self.append_lines(&to_write)?;
self.entries.push(Entry {
desc: desc.clone(),
interval: Interval {
start: dt,
end: None,
},
});
Ok(())
}
fn end_session(&mut self, dt: DateTime<Utc>) -> Result<()> {
let entry = self
.entries
.pop_if(|e| e.interval.end.is_none())
.ok_or_else(|| anyhow!("tried to end but nothing was started"))?;
self.append_lines(&[EntryLine {
kind: EntryKind::End,
desc: entry.desc.clone(),
dt,
}])?;
self.entries.push(Entry {
desc: entry.desc,
interval: Interval {
start: entry.interval.start,
end: Some(dt),
},
});
Ok(())
}
fn flush(&mut self) -> Result<()> {
if self.path.is_some() {
return Ok(());
}
let mut stdout = io::stdout();
for entry in &self.entries[self.initial_entry_count..] {
writeln!(
stdout,
"{}",
EntryLine {
kind: EntryKind::Start,
desc: entry.desc.clone(),
dt: entry.interval.start,
}
)?;
if let Some(end) = entry.interval.end {
writeln!(
stdout,
"{}",
EntryLine {
kind: EntryKind::End,
desc: entry.desc.clone(),
dt: end,
}
)?;
}
}
Ok(())
}
fn rename_current(&mut self, new_desc: Arc<str>) -> Result<()> {
if let Some(entry) = self.entries.pop_if(|e| e.interval.end.is_none()) {
if let Some(path) = self.path.as_ref() {
let last_line =
get_last_line(path)?.and_then(|s| EntryLine::from_str(s.as_ref()).ok());
if let Some(EntryLine {
kind: EntryKind::Start,
..
}) = last_line
{
delete_last_line(path)?;
}
self.append_lines(&[EntryLine {
desc: new_desc.clone(),
kind: EntryKind::Start,
dt: entry.interval.start,
}])?;
}
self.entries.push(Entry {
desc: new_desc.clone(),
..entry
});
}
Ok(())
}
}
fn get_last_line(path: &PathBuf) -> io::Result<Option<String>> {
let file = fs::File::open(path)?;
let file_size = file.metadata()?.len();
if file_size == 0 {
return Ok(None);
}
let mut reader = BufReader::new(file);
let mut pos = file_size;
let mut last_line = String::new();
while pos > 0 {
pos -= 1;
reader.seek(io::SeekFrom::Start(pos))?;
let mut buffer = [0; 1];
reader.read_exact(&mut buffer)?;
if buffer[0] == b'\n' && pos < file_size - 1 {
break;
}
}
reader.read_line(&mut last_line)?;
Ok(Some(last_line.trim_end().to_string()))
}
fn delete_last_line(path: &PathBuf) -> io::Result<()> {
let mut file = OpenOptions::new().read(true).write(true).open(path)?;
let file_size = file.metadata()?.len();
if file_size == 0 {
return Ok(());
}
let mut pos = file_size - 1;
let mut buffer = [0; 1];
while pos > 0 {
pos -= 1;
file.seek(io::SeekFrom::Start(pos))?;
file.read_exact(&mut buffer)?;
if buffer[0] == b'\n' {
pos += 1;
break;
}
}
Ok(file.set_len(pos)?)
}