file_storage.rs (8275B)
1 use std::{ 2 collections::{HashMap, VecDeque}, 3 fs::{self, File}, 4 io::{self, BufRead}, 5 path::{Path, PathBuf}, 6 time::SystemTime, 7 }; 8 9 use crate::Error; 10 11 #[derive(Debug, Clone)] 12 pub struct DataPath { 13 base: PathBuf, 14 } 15 16 impl DataPath { 17 pub fn new(base: impl AsRef<Path>) -> Self { 18 let base = base.as_ref().to_path_buf(); 19 Self { base } 20 } 21 22 pub fn default_base() -> Option<PathBuf> { 23 dirs::data_local_dir().map(|pb| pb.join("notedeck")) 24 } 25 } 26 27 pub enum DataPathType { 28 Log, 29 Setting, 30 Keys, 31 SelectedKey, 32 Db, 33 Cache, 34 } 35 36 impl DataPath { 37 pub fn rel_path(&self, typ: DataPathType) -> PathBuf { 38 match typ { 39 DataPathType::Log => PathBuf::from("logs"), 40 DataPathType::Setting => PathBuf::from("settings"), 41 DataPathType::Keys => PathBuf::from("storage").join("accounts"), 42 DataPathType::SelectedKey => PathBuf::from("storage").join("selected_account"), 43 DataPathType::Db => PathBuf::from("db"), 44 DataPathType::Cache => PathBuf::from("cache"), 45 } 46 } 47 48 pub fn path(&self, typ: DataPathType) -> PathBuf { 49 self.base.join(self.rel_path(typ)) 50 } 51 } 52 53 #[derive(Debug, PartialEq)] 54 pub struct Directory { 55 pub file_path: PathBuf, 56 } 57 58 impl Directory { 59 pub fn new(file_path: PathBuf) -> Self { 60 Self { file_path } 61 } 62 63 /// Get the files in the current directory where the key is the file name and the value is the file contents 64 pub fn get_files(&self) -> Result<HashMap<String, String>, Error> { 65 let dir = fs::read_dir(self.file_path.clone())?; 66 let map = dir 67 .filter_map(|f| f.ok()) 68 .filter(|f| f.path().is_file()) 69 .filter_map(|f| { 70 let file_name = f.file_name().into_string().ok()?; 71 let contents = fs::read_to_string(f.path()).ok()?; 72 Some((file_name, contents)) 73 }) 74 .collect(); 75 76 Ok(map) 77 } 78 79 pub fn get_file_names(&self) -> Result<Vec<String>, Error> { 80 let dir = fs::read_dir(self.file_path.clone())?; 81 let names = dir 82 .filter_map(|f| f.ok()) 83 .filter(|f| f.path().is_file()) 84 .filter_map(|f| f.file_name().into_string().ok()) 85 .collect(); 86 87 Ok(names) 88 } 89 90 pub fn get_file(&self, file_name: String) -> Result<String, Error> { 91 let filepath = self.file_path.clone().join(file_name.clone()); 92 93 if filepath.exists() && filepath.is_file() { 94 let filepath_str = filepath 95 .to_str() 96 .ok_or_else(|| Error::Generic("Could not turn path to string".to_owned()))?; 97 Ok(fs::read_to_string(filepath_str)?) 98 } else { 99 Err(Error::Generic(format!( 100 "Requested file was not found: {}", 101 file_name 102 ))) 103 } 104 } 105 106 pub fn get_file_last_n_lines(&self, file_name: String, n: usize) -> Result<FileResult, Error> { 107 let filepath = self.file_path.clone().join(file_name.clone()); 108 109 if filepath.exists() && filepath.is_file() { 110 let file = File::open(&filepath)?; 111 let reader = io::BufReader::new(file); 112 113 let mut queue: VecDeque<String> = VecDeque::with_capacity(n); 114 115 let mut total_lines_in_file = 0; 116 for line in reader.lines() { 117 let line = line?; 118 119 queue.push_back(line); 120 121 if queue.len() > n { 122 queue.pop_front(); 123 } 124 total_lines_in_file += 1; 125 } 126 127 let output_num_lines = queue.len(); 128 let output = queue.into_iter().collect::<Vec<String>>().join("\n"); 129 Ok(FileResult { 130 output, 131 output_num_lines, 132 total_lines_in_file, 133 }) 134 } else { 135 Err(Error::Generic(format!( 136 "Requested file was not found: {}", 137 file_name 138 ))) 139 } 140 } 141 142 /// Get the file name which is most recently modified in the directory 143 pub fn get_most_recent(&self) -> Result<Option<String>, Error> { 144 let mut most_recent: Option<(SystemTime, String)> = None; 145 146 for entry in fs::read_dir(&self.file_path)? { 147 let entry = entry?; 148 let metadata = entry.metadata()?; 149 if metadata.is_file() { 150 let modified = metadata.modified()?; 151 let file_name = entry.file_name().to_string_lossy().to_string(); 152 153 match most_recent { 154 Some((last_modified, _)) if modified > last_modified => { 155 most_recent = Some((modified, file_name)); 156 } 157 None => { 158 most_recent = Some((modified, file_name)); 159 } 160 _ => {} 161 } 162 } 163 } 164 165 Ok(most_recent.map(|(_, file_name)| file_name)) 166 } 167 } 168 169 pub struct FileResult { 170 pub output: String, 171 pub output_num_lines: usize, 172 pub total_lines_in_file: usize, 173 } 174 175 /// Write the file to the directory 176 pub fn write_file(directory: &Path, file_name: String, data: &str) -> Result<(), Error> { 177 if !directory.exists() { 178 fs::create_dir_all(directory)? 179 } 180 181 std::fs::write(directory.join(file_name), data)?; 182 Ok(()) 183 } 184 185 pub fn delete_file(directory: &Path, file_name: String) -> Result<(), Error> { 186 let file_to_delete = directory.join(file_name.clone()); 187 if file_to_delete.exists() && file_to_delete.is_file() { 188 fs::remove_file(file_to_delete).map_err(Error::Io) 189 } else { 190 Err(Error::Generic(format!( 191 "Requested file to delete was not found: {}", 192 file_name 193 ))) 194 } 195 } 196 197 #[cfg(test)] 198 mod tests { 199 use std::path::PathBuf; 200 201 use crate::{ 202 storage::file_storage::{delete_file, write_file}, 203 Error, 204 }; 205 206 use super::Directory; 207 208 static CREATE_TMP_DIR: fn() -> Result<PathBuf, Error> = 209 || Ok(tempfile::TempDir::new()?.path().to_path_buf()); 210 211 #[test] 212 fn test_add_get_delete() { 213 if let Ok(path) = CREATE_TMP_DIR() { 214 let directory = Directory::new(path); 215 let file_name = "file_test_name.txt".to_string(); 216 let file_contents = "test"; 217 let write_res = write_file(&directory.file_path, file_name.clone(), file_contents); 218 assert!(write_res.is_ok()); 219 220 if let Ok(asserted_file_contents) = directory.get_file(file_name.clone()) { 221 assert_eq!(asserted_file_contents, file_contents); 222 } else { 223 panic!("File not found"); 224 } 225 226 let delete_res = delete_file(&directory.file_path, file_name); 227 assert!(delete_res.is_ok()); 228 } else { 229 panic!("could not get interactor") 230 } 231 } 232 233 #[test] 234 fn test_get_multiple() { 235 if let Ok(path) = CREATE_TMP_DIR() { 236 let directory = Directory::new(path); 237 238 for i in 0..10 { 239 let file_name = format!("file{}.txt", i); 240 let write_res = write_file(&directory.file_path, file_name, "test"); 241 assert!(write_res.is_ok()); 242 } 243 244 if let Ok(files) = directory.get_files() { 245 for i in 0..10 { 246 let file_name = format!("file{}.txt", i); 247 assert!(files.contains_key(&file_name)); 248 assert_eq!(files.get(&file_name).unwrap(), "test"); 249 } 250 } else { 251 panic!("Files not found"); 252 } 253 254 if let Ok(file_names) = directory.get_file_names() { 255 for i in 0..10 { 256 let file_name = format!("file{}.txt", i); 257 assert!(file_names.contains(&file_name)); 258 } 259 } else { 260 panic!("File names not found"); 261 } 262 263 for i in 0..10 { 264 let file_name = format!("file{}.txt", i); 265 assert!(delete_file(&directory.file_path, file_name).is_ok()); 266 } 267 } else { 268 panic!("could not get interactor") 269 } 270 } 271 }