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::{
13    debug,
14    error::{DittoError, ErrorKind},
15};
16
17/// Interface provided by the Filesystem directory into which Ditto will store
18/// its local data
19pub trait DittoRoot: Send + Sync {
20    /// Return the root path of the Ditto directory
21    fn root_path(&self) -> &Path;
22
23    /// Emits the Ditto root directory path as a null-terminated UTF-8 C-string
24    fn root_dir_to_c_str(&self) -> Result<char_p::Box, DittoError> {
25        Ok(char_p::new(self.root_path_as_str()?))
26    }
27
28    /// Return true if the Root path exists
29    fn exists(&self) -> bool;
30
31    /// Return true if the provided Root path is a valid path
32    fn is_valid(&self) -> Result<(), DittoError>;
33
34    /// Return the Root path as a `&str`
35    fn root_path_as_str(&self) -> Result<&str, DittoError> {
36        self.root_path().to_str().ok_or_else(|| {
37            DittoError::new(
38                ErrorKind::InvalidInput,
39                "Path is not valid UTF-8".to_string(),
40            )
41        })
42    }
43}
44
45/// A persistent working directory where Ditto will store its data across restarts of the host
46/// process on the local file system
47pub struct PersistentRoot {
48    root: PathBuf,
49}
50
51/// A Temporary Ditto persistence directory which will be deleted from storage when dropped.
52pub struct TempRoot {
53    root: PathBuf,
54}
55
56impl Drop for TempRoot {
57    fn drop(&mut self) {
58        // we need to step up one level
59        let ditto_root = &mut self.root; // tempdir/jitter/ditto_root
60        ditto_root.pop(); // tempdir/jitter <-- remove here
61        #[allow(deprecated)] // Workaround for patched tracing
62        {
63            debug!(path = %ditto_root.display(), "removing TempRoot");
64        }
65        let _ = ::std::fs::remove_dir_all(&ditto_root);
66    }
67}
68
69impl Default for PersistentRoot {
70    fn default() -> Self {
71        PersistentRoot::from_current_exe().unwrap() // This should never fail
72    }
73}
74
75impl DittoRoot for PersistentRoot {
76    fn root_path(&self) -> &Path {
77        self.root.as_path()
78    }
79    fn exists(&self) -> bool {
80        self.root.exists()
81    }
82    fn is_valid(&self) -> Result<(), DittoError> {
83        Ok(()) // TODO
84    }
85}
86
87// Constructors
88impl PersistentRoot {
89    /// Manually construct a root directory
90    pub fn new(root_dir: impl Into<PathBuf>) -> Result<Self, DittoError> {
91        let root = root_dir.into();
92        #[allow(clippy::disallowed_methods)]
93        std::fs::create_dir_all(&root).map_err(DittoError::from)?;
94        let dir = PersistentRoot { root };
95
96        if let Err(e) = dir.is_valid() {
97            Err(e)
98        } else {
99            Ok(dir)
100        }
101    }
102
103    /// Get a Ditto directory from the path of the current executable
104    pub fn from_current_exe() -> Result<Self, DittoError> {
105        let root_dir = env::current_exe()
106            .ok()
107            .and_then(|abspath| abspath.parent().map(|x| x.to_path_buf()))
108            .ok_or_else(|| {
109                DittoError::new(
110                    ErrorKind::InvalidInput,
111                    "Unable to resolve a default data directory on this platform".to_string(),
112                )
113            })?;
114        Self::new(root_dir)
115    }
116
117    /// Create a Ditto directory from env vars
118    pub fn from_env() -> Result<Self, DittoError> {
119        let root_dir: PathBuf = env::var_os("DITTO_ROOT_PATH")
120            .map(PathBuf::from)
121            .ok_or_else(|| {
122                DittoError::new(
123                    ErrorKind::InvalidInput,
124                    "The DITTO_ROOT_PATH env var is not set".to_string(),
125                )
126            })?;
127        Self::new(root_dir)
128    }
129}
130
131impl DittoRoot for TempRoot {
132    fn root_path(&self) -> &Path {
133        self.root.as_path()
134    }
135    fn exists(&self) -> bool {
136        self.root.exists()
137    }
138    fn is_valid(&self) -> Result<(), DittoError> {
139        Ok(()) // TODO
140    }
141}
142
143impl TempRoot {
144    // Temp randomization impl adapted from temp_dir crate but with a contemporary version of the
145    // rand crate
146    const NUM_RETRIES: u32 = 1 << 31;
147    const NUM_RAND_CHARS: usize = 12;
148
149    /// Create a new Ditto root which will be deleted when `TempRoot` is dropped
150    pub fn new() -> Self {
151        use std::iter;
152
153        use rand::{distr::Alphanumeric, Rng};
154        let mut ditto_root;
155        let tmpdir = std::env::temp_dir(); // the OS provided temp root from TMPDIR
156        if !tmpdir.is_absolute() {
157            let cur_dir = env::current_exe()
158                .ok()
159                .and_then(|abspath| abspath.parent().map(|x| x.to_path_buf()))
160                .unwrap();
161            ditto_root = cur_dir;
162            ditto_root.push(tmpdir);
163        } else {
164            ditto_root = tmpdir;
165        }
166
167        let mut rng = rand::rng();
168        for _ in 0..Self::NUM_RETRIES {
169            let jitter: String = iter::repeat(())
170                .map(|()| rng.sample(Alphanumeric))
171                .map(char::from)
172                .take(Self::NUM_RAND_CHARS)
173                .collect();
174            ditto_root.push(jitter);
175            #[allow(clippy::disallowed_methods)]
176            match fs::create_dir_all(&ditto_root) {
177                Ok(_) => {
178                    ditto_root.push("ditto");
179                    #[allow(clippy::disallowed_methods)]
180                    fs::create_dir_all(&ditto_root).unwrap();
181                    return TempRoot { root: ditto_root };
182                }
183                Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
184                Err(e) => {
185                    panic!("Unable to create tempdir {:?}", e);
186                }
187            }
188        }
189        panic!("TempRoot {:?} already exists!", ditto_root.display());
190    }
191}
192
193impl Default for TempRoot {
194    fn default() -> Self {
195        TempRoot::new()
196    }
197}