dittolive-ditto 4.0.0-beta1

Ditto is a peer to peer cross-platform database that allows mobile, web, IoT and server apps to sync with or without an internet connection.
Documentation
//! Ditto Root Directory Wrapper
//!
//! A utility module for creating, managing, and removing the Ditto directory
//! structures on the local file system

use_prelude!();
use std::{
    env, fs,
    path::{Path, PathBuf},
};

use crate::error::{DittoError, ErrorKind};

/// Interface provided by the Filesystem directory into which Ditto will store
/// its local data
pub trait DittoRoot: Send + Sync {
    fn root_path(&self) -> &Path;

    #[deprecated(note = "Use root_path instead")]
    fn data_path(&self) -> &Path {
        self.root_path()
    }

    /// Emits the Ditto Root Directory Path as a null-terminated UTF-8 C-string
    fn root_dir_to_c_str(&self) -> Result<char_p::Box, DittoError> {
        Ok(char_p::new(self.root_path_as_str()?))
    }

    /// Emits the Ditto Data Directory Path as a null-terminated UTF-8 C-string
    #[deprecated(note = "Use root_dir_to_c_str instead")]
    fn data_dir_to_c_str(&self) -> Result<char_p::Box, DittoError> {
        self.root_dir_to_c_str()
    }

    fn exists(&self) -> bool;

    fn is_valid(&self) -> Result<(), DittoError>;

    fn root_path_as_str(&self) -> Result<&str, DittoError> {
        self.root_path().to_str().ok_or_else(|| {
            DittoError::new(
                ErrorKind::InvalidInput,
                "Path is not valid UTF-8".to_string(),
            )
        })
    }

    /// Returns the path of the Ditto Data Directory as a &str
    #[deprecated(note = "Use root_path_as_str instead")]
    fn data_path_as_str(&self) -> Result<&str, DittoError> {
        self.root_path_as_str()
    }
}

// A persistent working directory where Ditto will store its data
// across restarts of the host process on the local file system
pub struct PersistentRoot {
    root: PathBuf,
}

// A Temporary Ditto Root which will clean itself up on exit
pub struct TempRoot {
    root: PathBuf,
}

impl Drop for TempRoot {
    fn drop(&mut self) {
        // we need to step up one level
        let mut ditto_root = self.root.clone(); // tempdir/jitter/ditto_root
        ditto_root.pop(); // tempdir/jitter <-- remove here
        ::log::debug!("Removing TempRoot {}", ditto_root.display());
        let _ = ::std::fs::remove_dir_all(&ditto_root);
    }
}

impl Default for PersistentRoot {
    fn default() -> Self {
        PersistentRoot::from_current_exe().unwrap() // This should never fail
    }
}

impl DittoRoot for PersistentRoot {
    fn root_path(&self) -> &Path {
        self.root.as_path()
    }
    fn exists(&self) -> bool {
        self.root.exists()
    }
    fn is_valid(&self) -> Result<(), DittoError> {
        Ok(()) // TODO
    }
}

// Constructors
impl PersistentRoot {
    /// Manually construct a root directory
    pub fn new(root_dir: impl Into<PathBuf>) -> Result<Self, DittoError> {
        let root = root_dir.into();
        std::fs::create_dir_all(&root).map_err(DittoError::from)?;
        let dir = PersistentRoot { root };

        if let Err(e) = dir.is_valid() {
            Err(e)
        } else {
            Ok(dir)
        }
    }
    /// Get a Ditto Directory from the path of the current executable
    pub fn from_current_exe() -> Result<Self, DittoError> {
        let root_dir = env::current_exe()
            .ok()
            .and_then(|abspath| abspath.parent().map(|x| x.to_path_buf()))
            .ok_or_else(|| {
                DittoError::new(
                    ErrorKind::InvalidInput,
                    "Unable to resolve a default data directory on this platform".to_string(),
                )
            })?;
        Self::new(root_dir)
    }

    /// Create a Ditto Directory from Env Vars
    pub fn from_env() -> Result<Self, DittoError> {
        let root_dir: PathBuf = env::var_os("DITTO_ROOT_PATH")
            .map(PathBuf::from)
            .ok_or_else(|| {
                DittoError::new(
                    ErrorKind::InvalidInput,
                    "The DITTO_ROOT_PATH env var is not set".to_string(),
                )
            })?;
        Self::new(root_dir)
    }
}

impl DittoRoot for TempRoot {
    fn root_path(&self) -> &Path {
        self.root.as_path()
    }
    fn exists(&self) -> bool {
        self.root.exists()
    }
    fn is_valid(&self) -> Result<(), DittoError> {
        Ok(()) // TODO
    }
}
impl TempRoot {
    // Temp randomization impl adapted from temp_dir crate
    // but with a contemporary version of the rand crate
    const NUM_RETRIES: u32 = 1 << 31;
    const NUM_RAND_CHARS: usize = 12;
    /// Create a new Ditto Root which will be deleted when TempRoot is dropped
    pub fn new() -> Self {
        use std::iter;

        use rand::{distributions::Alphanumeric, Rng};
        let mut ditto_root;
        let tmpdir = std::env::temp_dir(); // the OS provided temp root from TMPDIR
        if !tmpdir.is_absolute() {
            let cur_dir = env::current_exe()
                .ok()
                .and_then(|abspath| abspath.parent().map(|x| x.to_path_buf()))
                .unwrap();
            ditto_root = cur_dir;
            ditto_root.push(tmpdir);
        } else {
            ditto_root = tmpdir;
        }

        let mut rng = rand::thread_rng();
        for _ in 0..Self::NUM_RETRIES {
            let jitter: String = iter::repeat(())
                .map(|()| rng.sample(Alphanumeric))
                .map(char::from)
                .take(Self::NUM_RAND_CHARS)
                .collect();
            ditto_root.push(jitter);
            match fs::create_dir_all(&ditto_root) {
                Ok(_) => {
                    ditto_root.push("ditto");
                    fs::create_dir_all(&ditto_root).unwrap();
                    return TempRoot { root: ditto_root };
                }
                Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
                Err(e) => {
                    panic!("Unable to create tempdir {:?}", e);
                }
            }
        }
        panic!("TempRoot {:?} already exists!", ditto_root.display());
    }
}

impl Default for TempRoot {
    fn default() -> Self {
        TempRoot::new()
    }
}

/// In ditto V2, the Rust SDK used to store data in a subdirectory `ditto_data`.
/// This function will :
/// * if the `ditto_data` directory still exist
///     * drain its content to the parent folder
///     * delete the ditto_data directory
/// * else it will do nothing.
pub(crate) fn drain_ditto_data_dir(root: &Arc<dyn DittoRoot>) {
    let root: PathBuf = root.root_path().into();
    let old_data: PathBuf = root.join("ditto_data");
    if old_data.exists() {
        log::debug!("Migrating `ditto_data` to 'ditto' dir ({old_data:?}->{root:?})");
        let mut copy_options = fs_extra::dir::CopyOptions::new();
        copy_options.overwrite = true;
        // read_dir can only fail if:
        // * old_data is not a directory
        // * lacking permissions
        // * old_data do not exists
        // Unwrapping here is safe then.
        let data_children: Vec<_> = std::fs::read_dir(&old_data)
            .unwrap_or_else(|err| panic!("Failed to access {old_data:?}: {err}"))
            .filter_map(|child| child.ok())
            .map(|child| child.path())
            .collect();
        // Copy recursively all directories and their content to the root folder
        fs_extra::copy_items(&data_children, &root, &copy_options).unwrap();
        std::fs::remove_dir_all(old_data).unwrap();
    }
}