Skip to main content

dittolive_ditto/
fs.rs

1//! Ditto Root Directory Wrapper
2//!
3//! A utility module for creating, managing, and removing the Ditto directory
4//! structures on the local file system
5
6use_prelude!();
7use std::{
8    env, fs,
9    path::{Path, PathBuf},
10};
11
12use crate::error::{DittoError, ErrorKind};
13
14/// Interface provided by the Filesystem directory into which Ditto will store
15/// its local data
16pub trait DittoRoot: Send + Sync {
17    /// Return the root path of the Ditto directory
18    fn root_path(&self) -> &Path;
19
20    /// Return the path of the data in the Ditto directory
21    #[deprecated(note = "Use root_path instead")]
22    fn data_path(&self) -> &Path {
23        self.root_path()
24    }
25
26    /// Emits the Ditto root directory path as a null-terminated UTF-8 C-string
27    fn root_dir_to_c_str(&self) -> Result<char_p::Box, DittoError> {
28        Ok(char_p::new(self.root_path_as_str()?))
29    }
30
31    /// Emits the Ditto data directory path as a null-terminated UTF-8 C-string
32    #[deprecated(note = "Use root_dir_to_c_str instead")]
33    fn data_dir_to_c_str(&self) -> Result<char_p::Box, DittoError> {
34        self.root_dir_to_c_str()
35    }
36
37    /// Return true if the Root path exists
38    fn exists(&self) -> bool;
39
40    /// Return true if the provided Root path is a valid path
41    fn is_valid(&self) -> Result<(), DittoError>;
42
43    /// Return the Root path as a `&str`
44    fn root_path_as_str(&self) -> Result<&str, DittoError> {
45        self.root_path().to_str().ok_or_else(|| {
46            DittoError::new(
47                ErrorKind::InvalidInput,
48                "Path is not valid UTF-8".to_string(),
49            )
50        })
51    }
52
53    /// Returns the path of the Ditto data directory as an `&str`
54    #[deprecated(note = "Use root_path_as_str instead")]
55    fn data_path_as_str(&self) -> Result<&str, DittoError> {
56        self.root_path_as_str()
57    }
58}
59
60/// A persistent working directory where Ditto will store its data across restarts of the host
61/// process on the local file system
62pub struct PersistentRoot {
63    root: PathBuf,
64}
65
66/// A Temporary Ditto root which will clean itself up on exit
67pub struct TempRoot {
68    root: PathBuf,
69}
70
71impl Drop for TempRoot {
72    fn drop(&mut self) {
73        // we need to step up one level
74        let ditto_root = &mut self.root; // tempdir/jitter/ditto_root
75        ditto_root.pop(); // tempdir/jitter <-- remove here
76        debug!(path = %ditto_root.display(), "removing TempRoot");
77        let _ = ::std::fs::remove_dir_all(&ditto_root);
78    }
79}
80
81impl Default for PersistentRoot {
82    fn default() -> Self {
83        PersistentRoot::from_current_exe().unwrap() // This should never fail
84    }
85}
86
87impl DittoRoot for PersistentRoot {
88    fn root_path(&self) -> &Path {
89        self.root.as_path()
90    }
91    fn exists(&self) -> bool {
92        self.root.exists()
93    }
94    fn is_valid(&self) -> Result<(), DittoError> {
95        Ok(()) // TODO
96    }
97}
98
99// Constructors
100impl PersistentRoot {
101    /// Manually construct a root directory
102    pub fn new(root_dir: impl Into<PathBuf>) -> Result<Self, DittoError> {
103        let root = root_dir.into();
104        #[allow(clippy::disallowed_methods)]
105        std::fs::create_dir_all(&root).map_err(DittoError::from)?;
106        let dir = PersistentRoot { root };
107
108        if let Err(e) = dir.is_valid() {
109            Err(e)
110        } else {
111            Ok(dir)
112        }
113    }
114
115    /// Get a Ditto directory from the path of the current executable
116    pub fn from_current_exe() -> Result<Self, DittoError> {
117        let root_dir = env::current_exe()
118            .ok()
119            .and_then(|abspath| abspath.parent().map(|x| x.to_path_buf()))
120            .ok_or_else(|| {
121                DittoError::new(
122                    ErrorKind::InvalidInput,
123                    "Unable to resolve a default data directory on this platform".to_string(),
124                )
125            })?;
126        Self::new(root_dir)
127    }
128
129    /// Create a Ditto directory from env vars
130    pub fn from_env() -> Result<Self, DittoError> {
131        let root_dir: PathBuf = env::var_os("DITTO_ROOT_PATH")
132            .map(PathBuf::from)
133            .ok_or_else(|| {
134                DittoError::new(
135                    ErrorKind::InvalidInput,
136                    "The DITTO_ROOT_PATH env var is not set".to_string(),
137                )
138            })?;
139        Self::new(root_dir)
140    }
141}
142
143impl DittoRoot for TempRoot {
144    fn root_path(&self) -> &Path {
145        self.root.as_path()
146    }
147    fn exists(&self) -> bool {
148        self.root.exists()
149    }
150    fn is_valid(&self) -> Result<(), DittoError> {
151        Ok(()) // TODO
152    }
153}
154
155impl TempRoot {
156    // Temp randomization impl adapted from temp_dir crate but with a contemporary version of the
157    // rand crate
158    const NUM_RETRIES: u32 = 1 << 31;
159    const NUM_RAND_CHARS: usize = 12;
160
161    /// Create a new Ditto root which will be deleted when `TempRoot` is dropped
162    pub fn new() -> Self {
163        use std::iter;
164
165        use rand::{distributions::Alphanumeric, Rng};
166        let mut ditto_root;
167        let tmpdir = std::env::temp_dir(); // the OS provided temp root from TMPDIR
168        if !tmpdir.is_absolute() {
169            let cur_dir = env::current_exe()
170                .ok()
171                .and_then(|abspath| abspath.parent().map(|x| x.to_path_buf()))
172                .unwrap();
173            ditto_root = cur_dir;
174            ditto_root.push(tmpdir);
175        } else {
176            ditto_root = tmpdir;
177        }
178
179        let mut rng = rand::thread_rng();
180        for _ in 0..Self::NUM_RETRIES {
181            let jitter: String = iter::repeat(())
182                .map(|()| rng.sample(Alphanumeric))
183                .map(char::from)
184                .take(Self::NUM_RAND_CHARS)
185                .collect();
186            ditto_root.push(jitter);
187            #[allow(clippy::disallowed_methods)]
188            match fs::create_dir_all(&ditto_root) {
189                Ok(_) => {
190                    ditto_root.push("ditto");
191                    #[allow(clippy::disallowed_methods)]
192                    fs::create_dir_all(&ditto_root).unwrap();
193                    return TempRoot { root: ditto_root };
194                }
195                Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
196                Err(e) => {
197                    panic!("Unable to create tempdir {:?}", e);
198                }
199            }
200        }
201        panic!("TempRoot {:?} already exists!", ditto_root.display());
202    }
203}
204
205impl Default for TempRoot {
206    fn default() -> Self {
207        TempRoot::new()
208    }
209}
210
211/// In v2 of the Ditto SDK, data used to be stored in a subdirectory named `ditto_data`.
212///
213/// This function will:
214///   - if the `ditto_data` directory still exists
215///     - drain its content to the parent directory
216///     - delete the `ditto_data` directory
217///   - else it will do nothing.
218pub(crate) fn drain_ditto_data_dir(root: &Arc<dyn DittoRoot>) {
219    let root: PathBuf = root.root_path().into();
220    let old_data: PathBuf = root.join("ditto_data");
221    if old_data.exists() {
222        debug!(old_path = ?old_data, new_path = ?root, "migrating `ditto_data` to 'ditto' dir");
223        let mut copy_options = fs_extra::dir::CopyOptions::new();
224        copy_options.overwrite = true;
225        // read_dir can only fail if:
226        // * old_data is not a directory
227        // * lacking permissions
228        // * old_data do not exists
229        // Unwrapping here is safe then.
230        let data_children: Vec<_> = std::fs::read_dir(&old_data)
231            .unwrap_or_else(|err| panic!("Failed to access {old_data:?}: {err}"))
232            .filter_map(|child| child.ok())
233            .map(|child| child.path())
234            .collect();
235        // Copy recursively all directories and their content to the root folder
236        fs_extra::copy_items(&data_children, &root, &copy_options).unwrap();
237        std::fs::remove_dir_all(old_data).unwrap();
238    }
239}