distant-local 0.20.0

Library implementing distant API for local interactions
Documentation
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::{fmt, io};

use distant_core::net::common::ConnectionId;
use distant_core::net::server::Reply;
use distant_core::protocol::{Change, ChangeKindSet, Error, Response};

/// Represents a path registered with a watcher that includes relevant state including
/// the ability to reply with
pub struct RegisteredPath {
    /// Unique id tied to the path to distinguish it
    id: ConnectionId,

    /// The raw path provided to the watcher, which is not canonicalized
    raw_path: PathBuf,

    /// The canonicalized path at the time of providing to the watcher,
    /// as all paths must exist for a watcher, we use this to get the
    /// source of truth when watching
    path: PathBuf,

    /// Whether or not the path was set to be recursive
    recursive: bool,

    /// Specific filter for path (only the allowed change kinds are tracked)
    /// NOTE: This is a combination of only and except filters
    allowed: ChangeKindSet,

    /// Used to send a reply through the connection watching this path
    reply: Box<dyn Reply<Data = Response>>,
}

impl fmt::Debug for RegisteredPath {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("RegisteredPath")
            .field("raw_path", &self.raw_path)
            .field("path", &self.path)
            .field("recursive", &self.recursive)
            .field("allowed", &self.allowed)
            .finish()
    }
}

impl PartialEq for RegisteredPath {
    /// Checks for equality using the id, canonicalized path, and allowed change kinds
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id && self.path == other.path && self.allowed == other.allowed
    }
}

impl Eq for RegisteredPath {}

impl Hash for RegisteredPath {
    /// Hashes using the id, canonicalized path, and allowed change kinds
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.id.hash(state);
        self.path.hash(state);
        self.allowed.hash(state);
    }
}

impl RegisteredPath {
    /// Registers a new path to be watched (does not actually do any watching)
    pub async fn register(
        id: ConnectionId,
        path: impl Into<PathBuf>,
        recursive: bool,
        only: impl Into<ChangeKindSet>,
        except: impl Into<ChangeKindSet>,
        reply: Box<dyn Reply<Data = Response>>,
    ) -> io::Result<Self> {
        let raw_path = path.into();
        let path = tokio::fs::canonicalize(raw_path.as_path()).await?;
        let only = only.into();
        let except = except.into();

        // Calculate the true list of kinds based on only and except filters
        let allowed = if only.is_empty() {
            ChangeKindSet::all() - except
        } else {
            only - except
        };

        Ok(Self {
            id,
            raw_path,
            path,
            recursive,
            allowed,
            reply,
        })
    }

    /// Represents a unique id to distinguish this path from other registrations
    /// of the same path
    pub fn id(&self) -> ConnectionId {
        self.id
    }

    /// Represents the path provided during registration before canonicalization
    pub fn raw_path(&self) -> &Path {
        self.raw_path.as_path()
    }

    /// Represents the canonicalized path used by watchers
    pub fn path(&self) -> &Path {
        self.path.as_path()
    }

    /// Returns true if this path represents a recursive watcher path
    pub fn is_recursive(&self) -> bool {
        self.recursive
    }

    /// Returns reference to set of [`ChangeKind`] that this path watches
    pub fn allowed(&self) -> &ChangeKindSet {
        &self.allowed
    }

    /// Sends a reply for a change tied to this registered path, filtering
    /// out any changes that are not applicable.
    ///
    /// Returns true if message was sent, and false if not.
    pub fn filter_and_send(&self, change: Change) -> io::Result<bool> {
        if !self.allowed().contains(&change.kind) {
            return Ok(false);
        }

        // Only send if this registered path applies to the changed path
        if self.applies_to_path(&change.path) {
            self.reply.send(Response::Changed(change)).map(|_| true)
        } else {
            Ok(false)
        }
    }

    /// Sends an error message and includes paths if provided, skipping sending the message if
    /// no paths match and `skip_if_no_paths` is true.
    ///
    /// Returns true if message was sent, and false if not.
    pub fn filter_and_send_error<T>(
        &self,
        msg: &str,
        paths: T,
        skip_if_no_paths: bool,
    ) -> io::Result<bool>
    where
        T: IntoIterator,
        T::Item: AsRef<Path>,
    {
        let paths: Vec<PathBuf> = paths
            .into_iter()
            .filter(|p| self.applies_to_path(p.as_ref()))
            .map(|p| p.as_ref().to_path_buf())
            .collect();

        if !paths.is_empty() || !skip_if_no_paths {
            self.reply
                .send(if paths.is_empty() {
                    Response::Error(Error::from(msg))
                } else {
                    Response::Error(Error::from(format!("{msg} about {paths:?}")))
                })
                .map(|_| true)
        } else {
            Ok(false)
        }
    }

    /// Returns true if this path applies to the given path.
    /// This is accomplished by checking if the path is contained
    /// within either the raw or canonicalized path of the watcher
    /// and ensures that recursion rules are respected
    pub fn applies_to_path(&self, path: &Path) -> bool {
        let check_path = |path: &Path| -> bool {
            let cnt = path.components().count();

            // 0 means exact match from strip_prefix
            // 1 means that it was within immediate directory (fine for non-recursive)
            // 2+ means it needs to be recursive
            cnt < 2 || self.recursive
        };

        match (
            path.strip_prefix(self.path()),
            path.strip_prefix(self.raw_path()),
        ) {
            (Ok(p1), Ok(p2)) => check_path(p1) || check_path(p2),
            (Ok(p), Err(_)) => check_path(p),
            (Err(_), Ok(p)) => check_path(p),
            (Err(_), Err(_)) => false,
        }
    }
}