zccache-watcher 1.7.0

Filesystem watching subsystem for zccache
Documentation
use std::path::PathBuf;
use std::time::Duration;

use pyo3::exceptions::{PyOSError, PyRuntimeError};
use pyo3::prelude::*;

use crate::{PollWatchBatch, PollingWatcher, PollingWatcherConfig};

fn io_to_py_err(e: std::io::Error) -> PyErr {
    PyErr::new::<PyOSError, _>(e.to_string())
}

fn runtime_to_py_err(message: impl Into<String>) -> PyErr {
    PyErr::new::<PyRuntimeError, _>(message.into())
}

#[pyclass(module = "zccache.watcher._native")]
#[derive(Clone, Debug)]
pub struct WatchBatch {
    #[pyo3(get)]
    changed: Vec<String>,
    #[pyo3(get)]
    removed: Vec<String>,
    #[pyo3(get)]
    overflow: bool,
}

impl From<PollWatchBatch> for WatchBatch {
    fn from(value: PollWatchBatch) -> Self {
        Self {
            changed: value
                .changed
                .into_iter()
                .map(|path| path.to_string_lossy().into_owned())
                .collect(),
            removed: value
                .removed
                .into_iter()
                .map(|path| path.to_string_lossy().into_owned())
                .collect(),
            overflow: value.overflow,
        }
    }
}

#[pyclass(module = "zccache.watcher._native")]
pub struct NativeWatcher {
    watcher: PollingWatcher,
}

unsafe impl Send for NativeWatcher {}

#[pymethods]
impl NativeWatcher {
    #[new]
    #[pyo3(signature = (
        root,
        include_folders=vec![],
        include_globs=vec![],
        excluded_patterns=vec![],
        poll_interval_ms=100,
        debounce_ms=200
    ))]
    fn new(
        root: String,
        include_folders: Vec<String>,
        include_globs: Vec<String>,
        excluded_patterns: Vec<String>,
        poll_interval_ms: u64,
        debounce_ms: u64,
    ) -> PyResult<Self> {
        let mut config = PollingWatcherConfig::new(PathBuf::from(root));
        config.include_folders = include_folders.into_iter().map(Into::into).collect();
        config.include_globs = include_globs;
        config.excluded_patterns = excluded_patterns;
        config.poll_interval = Duration::from_millis(poll_interval_ms.max(1));
        config.debounce = Duration::from_millis(debounce_ms);

        let watcher = PollingWatcher::new(config).map_err(io_to_py_err)?;
        Ok(Self { watcher })
    }

    fn start(&self) -> PyResult<()> {
        self.watcher.start().map_err(io_to_py_err)
    }

    fn stop(&self) -> PyResult<()> {
        self.watcher.stop().map_err(io_to_py_err)
    }

    fn resume(&self) -> PyResult<()> {
        self.watcher.resume().map_err(io_to_py_err)
    }

    fn is_running(&self) -> bool {
        self.watcher.is_running()
    }

    #[pyo3(signature = (timeout_ms=0))]
    fn poll_batch(&self, timeout_ms: u64) -> PyResult<Option<WatchBatch>> {
        let batch = self
            .watcher
            .poll_timeout(Duration::from_millis(timeout_ms))
            .map_err(|_| runtime_to_py_err("watcher polling failed"))?;
        Ok(batch.map(WatchBatch::from))
    }
}

#[pymodule]
fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<WatchBatch>()?;
    m.add_class::<NativeWatcher>()?;
    Ok(())
}