pants_store/
file.rs

1use std::{
2    error::Error,
3    fs::{self, File},
4    io::{Read, Write},
5    marker::PhantomData,
6    path::PathBuf,
7};
8
9use chrono::{DateTime, Local};
10use glob::glob;
11use serde::{Deserialize, Serialize};
12
13use crate::{
14    errors::SaveError,
15    schema::Schema,
16    utils::{format_date, now, read_date},
17    vault_encrypted::{RecordEncrypted, VaultEncrypted},
18};
19
20// TODO: create encrypted files that need to be given a password/key to open
21// applies to the vault, vault backup, and record files
22
23pub trait ProjectFile<'de, Data>
24where
25    Data: Serialize + Deserialize<'de>,
26{
27    fn base_path() -> PathBuf {
28        if let Some(project_dirs) = directories_next::ProjectDirs::from("com", "bski", "pants") {
29            project_dirs.data_dir().into()
30        } else {
31            std::env::current_dir().unwrap_or_default()
32        }
33    }
34
35    fn path(&self) -> PathBuf;
36    fn create(&self) -> Result<File, Box<dyn Error>> {
37        let path = self.path();
38        if let Some(dir) = path.parent() {
39            fs::create_dir_all(dir)?;
40        }
41
42        let file = File::create(path)?;
43        Ok(file)
44    }
45
46    fn delete(&self) -> Result<(), Box<dyn Error>> {
47        let path = self.path();
48        Ok(fs::remove_file(path)?)
49    }
50
51    fn open(&self) -> Result<File, Box<dyn Error>> {
52        let path = self.path();
53        let file = File::open(path)?;
54        Ok(file)
55    }
56    // NOTE: Couldn't figure out making the reading and writing generic with serde
57    // also making all the trait inheritance work with blanket implementations was
58    // too much of a headache, all of which just seemed better to copy and paste the
59    // implementations
60    fn write(&mut self, data: &Data) -> Result<(), Box<dyn Error>> {
61        let mut file = self.create()?;
62        let output = serde_json::to_string(data)?;
63        file.write_all(output.as_ref())
64            .map_err(|_| SaveError::Write)?;
65
66        Ok(())
67    }
68
69    fn read(&self) -> Result<ReadIn<Data>, Box<dyn Error>> {
70        let mut file = self.open()?;
71        let mut content = String::new();
72        file.read_to_string(&mut content)?;
73        Ok(ReadIn {
74            data: content,
75            data_type: PhantomData,
76        })
77    }
78}
79
80pub struct ReadIn<Data> {
81    data: String,
82    data_type: PhantomData<Data>,
83}
84
85impl<'de, Data: Deserialize<'de>> ReadIn<Data> {
86    pub fn deserialize(&'de self) -> Data {
87        serde_json::from_str(&self.data).unwrap()
88    }
89}
90
91#[derive(Debug, Clone)]
92pub struct TimestampedFile<Data> {
93    name: String,
94    timestamp: DateTime<Local>,
95    data_type: PhantomData<Data>,
96}
97
98#[derive(Debug, Clone)]
99pub struct NonTimestampedFile<Data> {
100    name: String,
101    data_type: PhantomData<Data>,
102}
103
104impl<'de, Data> ProjectFile<'de, Data> for TimestampedFile<Data>
105where
106    Data: Serialize + Deserialize<'de>,
107{
108    fn path(&self) -> PathBuf {
109        let mut path = Self::base_path();
110        path.push(self.name.clone());
111        path.push(format!("{}-{}", self.name, format_date(self.timestamp)));
112        path.set_extension("json");
113        path
114    }
115}
116
117impl<'de, Data> ProjectFile<'de, Data> for NonTimestampedFile<Data>
118where
119    Data: Serialize + Deserialize<'de>,
120{
121    fn path(&self) -> PathBuf {
122        let mut path = Self::base_path();
123        path.push(self.name.clone());
124        path.push(self.name.clone());
125        path.set_extension("json");
126        path
127    }
128}
129
130impl<'a, Data> TimestampedFile<Data>
131where
132    Self: Name,
133    Data: Serialize + Deserialize<'a>,
134{
135    fn new(timestamp: DateTime<Local>) -> Self {
136        Self {
137            name: Self::name(),
138            timestamp,
139            data_type: PhantomData,
140        }
141    }
142
143    fn now() -> Self {
144        Self::new(now())
145    }
146
147    pub fn last() -> Option<Self> {
148        let mut path = Self::base_path();
149        path.push(&Self::name());
150        path.push(format!("{}-*.json", Self::name()));
151        glob(path.to_str().unwrap())
152            .expect("Failed to read glob pattern")
153            .fold(None, |acc, entry| match entry {
154                Ok(p) => {
155                    let file_name = p.file_stem().unwrap().to_str().unwrap();
156                    let split = file_name.split_once('-').unwrap();
157                    let time = read_date(split.1).unwrap();
158                    match acc {
159                        None => Some(Self::new(time)),
160                        Some(ref f) => {
161                            if f.timestamp < time {
162                                Some(Self::new(time))
163                            } else {
164                                acc
165                            }
166                        }
167                    }
168                }
169                _ => acc,
170            })
171    }
172
173    pub fn all() -> Vec<Self> {
174        let mut path = Self::base_path();
175        path.push(&Self::name());
176        path.push(format!("{}-*.json", Self::name()));
177        let mut paths = vec![];
178        for entry in glob(path.to_str().unwrap())
179            .expect("Failed to read glob pattern")
180            .flatten()
181        {
182            let file_name = entry.file_stem().unwrap().to_str().unwrap();
183            let split = file_name.split_once('-').unwrap();
184            let _name = split.0.to_owned();
185            let timestamp = read_date(split.1);
186            match timestamp {
187                Err(err) => println!("Malformed timestamp in filename: {:?}. {:?}", entry, err),
188                Ok(t) => paths.push(Self::new(t)),
189            }
190        }
191        paths
192    }
193}
194
195impl<'a, Data> NonTimestampedFile<Data>
196where
197    Self: Name,
198    Data: Serialize + Deserialize<'a>,
199{
200    fn new() -> Self {
201        Self {
202            name: Self::name(),
203            data_type: PhantomData,
204        }
205    }
206
207    pub fn check(&self) -> bool {
208        self.path().exists()
209    }
210}
211
212pub type VaultFile = NonTimestampedFile<VaultEncrypted>;
213pub type RecordFile = TimestampedFile<RecordEncrypted>;
214pub type BackupFile = TimestampedFile<VaultEncrypted>;
215pub type SchemaFile = NonTimestampedFile<Schema>;
216
217pub trait Name {
218    fn name() -> String;
219}
220
221impl Name for VaultFile {
222    fn name() -> String {
223        "vault".to_string()
224    }
225}
226
227impl Name for RecordFile {
228    fn name() -> String {
229        "record".to_string()
230    }
231}
232
233impl Name for BackupFile {
234    fn name() -> String {
235        "backup".to_string()
236    }
237}
238
239impl Name for SchemaFile {
240    fn name() -> String {
241        "schema".to_string()
242    }
243}
244
245impl<'a, Data> Default for TimestampedFile<Data>
246where
247    TimestampedFile<Data>: Name,
248    Data: Serialize + Deserialize<'a>,
249{
250    fn default() -> Self {
251        Self::now()
252    }
253}
254
255impl<'a, Data> Default for NonTimestampedFile<Data>
256where
257    NonTimestampedFile<Data>: Name,
258    Data: Serialize + Deserialize<'a>,
259{
260    fn default() -> Self {
261        Self::new()
262    }
263}