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
20pub 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 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}