notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }