pinto/storage/
file_repository.rs1use crate::backlog::{BacklogItem, ItemId};
5use crate::error::Error;
6use crate::error::Result;
7use crate::sprint::{Sprint, SprintId};
8use std::io;
9use std::path::{Path, PathBuf};
10use tokio::fs;
11
12mod items;
13mod sprints;
14
15#[derive(Debug, Clone)]
20pub struct FileRepository {
21 root: PathBuf,
22}
23
24type ItemRecord = (PathBuf, BacklogItem);
25type SprintRecord = (PathBuf, Sprint);
26
27impl FileRepository {
28 pub fn new(root: impl Into<PathBuf>) -> Self {
30 Self { root: root.into() }
31 }
32
33 #[must_use]
35 pub fn tasks_dir(&self) -> PathBuf {
36 self.root.join("tasks")
37 }
38
39 #[must_use]
41 pub fn sprints_dir(&self) -> PathBuf {
42 self.root.join("sprints")
43 }
44
45 pub(crate) fn sprint_path_for(&self, id: &SprintId) -> PathBuf {
47 self.sprints_dir().join(format!("{id}.md"))
48 }
49
50 pub(crate) fn path_for(&self, id: &ItemId) -> Result<PathBuf> {
52 self.safe_item_path(&self.tasks_dir(), id)
53 }
54
55 fn archive_dir(&self) -> PathBuf {
57 self.root.join("archive")
58 }
59
60 fn archive_path_for(&self, id: &ItemId) -> Result<PathBuf> {
62 self.safe_item_path(&self.archive_dir(), id)
63 }
64
65 fn safe_item_path(&self, directory: &Path, id: &ItemId) -> Result<PathBuf> {
67 let path = directory.join(format!("{id}.md"));
68 if path.parent() != Some(directory) || !path.starts_with(directory) {
69 return Err(Error::InvalidItemId(id.to_string()));
70 }
71 Ok(path)
72 }
73
74 async fn markdown_paths(&self, dir: &Path) -> Result<Option<Vec<PathBuf>>> {
78 let mut read_dir = match fs::read_dir(dir).await {
79 Ok(rd) => rd,
80 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
81 Err(e) => return Err(Error::io(dir, &e)),
82 };
83
84 let mut paths = Vec::new();
85 while let Some(entry) = read_dir
86 .next_entry()
87 .await
88 .map_err(|e| Error::io(dir, &e))?
89 {
90 let path = entry.path();
91 if Self::is_markdown(&path) {
92 paths.push(path);
93 }
94 }
95 Ok(Some(paths))
96 }
97
98 fn is_markdown(path: &Path) -> bool {
100 path.extension().and_then(|s| s.to_str()) == Some("md")
101 }
102}
103
104#[cfg(test)]
105mod tests;