vsm/utils/
fs.rs

1//! Wrapper around the standard file-system module.
2
3use std::path::{Path, PathBuf};
4use std::{fs, io};
5
6use derive_getters::Getters;
7use log::debug;
8use serde::Serialize;
9
10use crate::config::TomlConfigFile;
11use crate::error::VsmRuntimeFault;
12
13/// Provides a simplified constrained interface to locations on disk and actions
14/// for directories and files that vsm requires to work.
15#[derive(Debug, Getters)]
16pub struct FilesystemManager {
17    /// Absolute path to configuration file storage directory
18    config_dir: String,
19    /// Absolute path to configure file
20    config_file: String,
21    /// Absolute path to session storage directory
22    vim_session_dir: String,
23}
24
25impl FilesystemManager {
26    /// Builds a new FilesystemManager object, relies on the static ENVIRONMENT
27    /// state object to get parameters.
28    ///
29    /// # Arguments
30    ///     * config_dir Absolute path to the configuration directory.
31    ///     * config_file Absolute path to the configuration file.
32    ///     * vim_session_dir Absolute path to the vim sessions directory.
33    pub fn new(config_dir: &str, config_file: &str, vim_session_dir: &str) -> Self {
34        Self {
35            config_dir: String::from(config_dir),
36            config_file: String::from(config_file),
37            vim_session_dir: String::from(vim_session_dir),
38        }
39    }
40
41    /// Check if the configuration directory exists on disk.
42    ///
43    /// # Returns
44    ///     * true if the directory exists.
45    ///     * false if the path points to a broken symlink,
46    ///        or the program does not have proper permissions.
47    fn config_dir_exists(&self) -> bool {
48        Path::new(self.config_dir()).is_dir()
49    }
50
51    /// Check if the configuration file exists on disk.
52    ///
53    /// # Returns
54    ///     * true if the file exists.
55    ///     * false if the path points to a broken symlink, or the
56    ///        program does not have proper permissions.
57    pub fn config_file_exists(&self) -> bool {
58        Path::new(self.config_file()).is_file()
59    }
60
61    /// Check if the vim_sessions directory exists on disk.
62    ///
63    /// # Returns
64    ///     * true if the directory exists.
65    ///     * false if the path points to a broken symlink,
66    ///        or the program does not have proper permissions.
67    pub fn vim_session_dir_exists(&self) -> bool {
68        Path::new(self.vim_session_dir()).is_dir()
69    }
70
71    /// Collects all vim session files into a Vector of Path Buffers.
72    ///
73    /// # Returns
74    ///     * Option<Vec<PathBuf>> the vector
75    ///       of paths is an option in-case the directory is empty,
76    ///       this is how we can tell if we have session files, to list,
77    ///       or remove or open.
78    ///
79    /// # Errors
80    ///     * io::Error
81    pub fn load_vim_session_files(&self) -> Result<Option<Vec<PathBuf>>, io::Error> {
82        // if the sessions directory doesn't exist, create it and all parent directories before
83        // it, and return a result of None, since we know there aren't any session files to load from a
84        // directory we just created.
85        if !self.vim_session_dir_exists() {
86            fs::create_dir_all(self.vim_session_dir())?;
87            return Ok(None);
88        }
89
90        // the directory exists, so lets read it.
91        let mut session_files = fs::read_dir(self.vim_session_dir())?
92            .map(|res| res.map(|session| session.path()))
93            .collect::<Result<Vec<PathBuf>, io::Error>>()?;
94
95        // this gaurds against the case where the directory existed already,
96        // but there were no session files found.
97        if session_files.is_empty() {
98            return Ok(None);
99        }
100
101        // Now we know we have sessions.
102        // `session_files` is just a vector of PathBuf's loaded from the VIM_SESSIONS directory.
103        // We can't be sure that the user hasn't put files other than .vim in there, or
104        // created directories, the existence of either would create unwanted bugs when opening
105        // sessions with vim variants. Therefore, we only keep paths that are files, and have a "vim" file extension.
106        // anything else gets dropped.
107        session_files.retain(|path| {
108            if path.is_file() {
109                path.extension().map_or_else(|| false, |ext| ext == "vim")
110            } else {
111                false
112            }
113        });
114
115        session_files.sort();
116        Ok(Some(session_files))
117    }
118
119    /// Serializes a config structure and writes it to disk as .. Does not check
120    /// if the configure file already exists. Assumes it is being used in tandem
121    /// with config_file_exists().
122    ///
123    /// # Arguments
124    ///     * config_struct Any type which implements the
125    ///       serde::Serialize + Sized traits.
126    ///
127    /// # Returns
128    ///     * Result<()> on success.
129    ///
130    /// # Errors
131    ///     * VsmRuntimeFault::TomlConfigFileWrite captures all
132    ///        possible errors during io and serialization.
133    pub fn write_config<T>(&self, config_struct: T) -> Result<(), VsmRuntimeFault>
134    where
135        T: Serialize + Sized,
136    {
137        if !self.config_dir_exists() {
138            if let Err(e) = fs::create_dir_all(self.config_dir()) {
139                return Err(VsmRuntimeFault::TomlConfigFileWrite { msg: e.to_string() });
140            }
141        }
142        match toml::to_string(&config_struct) {
143            Ok(serialized_string) => {
144                debug!("Writing config file => {}", self.config_file());
145                if let Err(e) = fs::write(self.config_file(), serialized_string) {
146                    Err(VsmRuntimeFault::TomlConfigFileWrite { msg: e.to_string() })
147                } else {
148                    Ok(())
149                }
150            }
151            Err(e) => Err(VsmRuntimeFault::TomlConfigFileWrite { msg: e.to_string() }),
152        }
153    }
154
155    /// Deserializes the config.toml from disk.
156    ///
157    /// # Returns
158    ///     * Ok(TomlConfigFile) if nothing went wrong.
159    ///
160    /// # Errors
161    ///     * Err(VsmRuntimeFault::TomlConfigFileRead) containing
162    ///        the error message generated from either the serde or toml
163    ///        libraries respectively.
164    pub fn read_config(&self) -> Result<TomlConfigFile, VsmRuntimeFault> {
165        debug!("Reading {}", self.config_file());
166        match fs::read_to_string(self.config_file()) {
167            Ok(contents) => {
168                let mut error_string = String::from("");
169                let config_option: Option<TomlConfigFile> = match toml::from_str(&contents) {
170                    Ok(c) => Some(c),
171                    Err(e) => {
172                        error_string = e.to_string();
173                        None
174                    }
175                };
176                config_option.map_or(
177                    Err(VsmRuntimeFault::TomlConfigFileRead { msg: error_string }),
178                    Ok,
179                )
180            }
181            Err(e) => Err(VsmRuntimeFault::TomlConfigFileRead { msg: e.to_string() }),
182        }
183    }
184
185    /// Wrapper around the built in filesystem remove file function.
186    ///
187    /// # Errors
188    ///     * VsmRuntimeFault::SessionFileRemoval if fs::remove_file fails.
189    pub fn remove_file(&self, session: &PathBuf) -> Result<(), VsmRuntimeFault> {
190        if let Err(e) = fs::remove_file(session) {
191            let msg = format!("Failed to remove {}\n{}", session.to_string_lossy(), e);
192            return Err(VsmRuntimeFault::SessionFileRemoval { msg });
193        }
194        Ok(())
195    }
196}