dove-core 0.1.1

The shared library behind dove — client-side-encrypted, expiring file sharing from a cloud you own.
Documentation
//! The backend factory: the plugin seam. `resolve` turns the registry's
//! active backend into a live [`Transfer`] — `self-hosted` is built straight
//! into this crate; any other `kind` is expected to be served by a signed
//! plugin binary (`dove-<kind>`) discovered in the plugins dir and dispatched
//! as a subprocess.
//!
//! The plugin *wire protocol* is out of scope here — it gets specified when
//! the first external backend actually ships. This module only proves two
//! things: discovery (does `dove-<kind>` exist where we'd look for it?) and
//! the failure mode when it doesn't (a legible error naming what to install).

use crate::backend::SelfHosted;
use crate::config::{Backend, Registry};
use crate::error::{Error, Result};
use crate::progress::Progress;
use crate::transfer::{BackendStatus, Transfer};
use crate::transfer::{Fetched, GetRequest, Share, ShareInfo, ShareRequest};
use std::path::PathBuf;

/// Resolve the active backend into a live `Transfer`. `self-hosted` is built in;
/// any other `kind` is served by a signed plugin binary `dove-<kind>` in the
/// plugins dir — discovered here, dispatched as a subprocess. Missing plugin →
/// a legible error naming what to install.
pub fn resolve(reg: &Registry) -> Result<Box<dyn Transfer>> {
    let active = reg.active_backend()?;
    match active.kind.as_str() {
        "self-hosted" => Ok(Box::new(SelfHosted::from_backend(active)?)),
        kind => resolve_plugin(kind, active),
    }
}

fn resolve_plugin(kind: &str, active: &Backend) -> Result<Box<dyn Transfer>> {
    let plugin = plugins_dir()?.join(format!("dove-{kind}"));
    if plugin.exists() {
        // Dispatch to the helper as a subprocess. The wire protocol is out of
        // scope for this task (specified when the first external backend
        // ships); only discovery + the missing-plugin error are wired and
        // tested here.
        Ok(Box::new(PluginTransfer {
            path: plugin,
            backend: active.name.clone(),
        }))
    } else {
        Err(Error::Config(format!(
            "this backend needs the {kind} plugin — run `dove install {kind}`"
        )))
    }
}

/// Where plugin binaries live: `~/.config/dove/plugins/`, honoring
/// `$XDG_CONFIG_HOME` the same way [`crate::config`]'s registry path does. A
/// `$DOVE_PLUGINS` override pins the exact directory — for tests, and for
/// anyone who wants plugins somewhere else.
fn plugins_dir() -> Result<PathBuf> {
    if let Ok(p) = std::env::var("DOVE_PLUGINS") {
        if !p.is_empty() {
            return Ok(PathBuf::from(p));
        }
    }
    if let Ok(x) = std::env::var("XDG_CONFIG_HOME") {
        if !x.is_empty() {
            return Ok(PathBuf::from(x).join("dove/plugins"));
        }
    }
    let home = std::env::var("HOME").map_err(|_| Error::Config("HOME is not set".into()))?;
    Ok(PathBuf::from(home).join(".config/dove/plugins"))
}

/// A backend served by an external plugin binary, dispatched as a subprocess.
/// The wire protocol isn't specified yet (it lands with the first real
/// external backend) — every method here is a stub that fails loudly rather
/// than silently no-op'ing, so a half-wired plugin backend can never look
/// like a working one.
#[derive(Debug)]
struct PluginTransfer {
    #[allow(dead_code)]
    path: PathBuf,
    #[allow(dead_code)]
    backend: String,
}

impl Transfer for PluginTransfer {
    fn share(&self, _req: ShareRequest, _progress: &dyn Progress) -> Result<Share> {
        Err(Error::Other("plugin dispatch not yet implemented".into()))
    }

    fn get(&self, _req: GetRequest, _progress: &dyn Progress) -> Result<Fetched> {
        Err(Error::Other("plugin dispatch not yet implemented".into()))
    }

    fn list(&self) -> Result<Vec<ShareInfo>> {
        Err(Error::Other("plugin dispatch not yet implemented".into()))
    }

    fn revoke(&self, _id: &str) -> Result<()> {
        Err(Error::Other("plugin dispatch not yet implemented".into()))
    }

    fn status(&self) -> Result<BackendStatus> {
        Err(Error::Other("plugin dispatch not yet implemented".into()))
    }
}