ringdrop 0.4.3

P2P streamed file transfer with ring-based access control, built on iroh and bao protocols
Documentation
use std::path::Path;

use anyhow::{anyhow, Result};

use crate::registry::{Registry, OPEN_RING_NAME};
use crate::util::parse_peer_id;

use super::RingCmd;

pub fn run(cmd: RingCmd, data_dir: &Path) -> Result<()> {
    std::fs::create_dir_all(data_dir)?;
    let cfg = crate::config::Config::load_or_create(data_dir)?;
    let public_id = cfg.public_id();
    let registry = Registry::open(data_dir.join("registry.redb"))?;

    match cmd {
        RingCmd::New { name } => {
            registry.create_ring(&name)?;
            println!("Ring created: {name}");
            println!("Add peers: rdrop ring add {name} <peer-id>");
        }
        RingCmd::List => {
            let rings = registry.list_rings()?;
            println!("{} rings:", rings.len());
            for r in rings {
                if r.is_open() {
                    println!(
                        "  {}  — publicly accessible (no membership required)",
                        r.as_str()
                    );
                } else {
                    let members = registry.list_members(r.as_str())?;
                    println!("  {}  ({} members)", r.as_str(), members.len());
                }
            }
        }
        RingCmd::Add {
            ring,
            peer,
            nickname,
        } => {
            if ring == OPEN_RING_NAME {
                println!("The open ring has no membership list — everyone is welcome by default.");
                return Ok(());
            }
            let peer = parse_peer_id(&peer)?;
            if peer == public_id {
                return Err(anyhow!("cannot add yourself to a ring"));
            }
            registry.add_member(&ring, peer, nickname.as_deref())?;
            match &nickname {
                Some(nick) => println!("Added {peer} ({nick}) to ring {ring}"),
                None => println!("Added {peer} to ring {ring}"),
            }
        }
        RingCmd::Remove { ring, peer } => {
            if ring == OPEN_RING_NAME {
                println!("The open ring has no membership list to remove from.");
                return Ok(());
            }
            let peer = parse_peer_id(&peer)?;
            registry.remove_member(&ring, peer)?;
            println!("Removed {peer} from ring {ring}");
        }
        RingCmd::Members { ring } => {
            if ring == OPEN_RING_NAME {
                println!("The open ring is public — any peer may access blobs tagged with it.");
                return Ok(());
            }
            let members = registry.list_members(&ring)?;
            if members.is_empty() {
                println!("Ring '{ring}' has no members yet.");
                println!("Add peers: rdrop ring add {ring} <peer-id>");
                println!("Peers print their peer-id with: rdrop id");
            } else {
                println!("Ring '{ring}' — {} members:", members.len());
                for (peer, nick) in members {
                    match nick {
                        Some(n) => println!("  {peer}  ({n})"),
                        None => println!("  {peer}"),
                    }
                }
            }
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use tempfile::TempDir;

    use super::*;

    #[test]
    fn ring_add_self_is_rejected() {
        let dir = TempDir::new().unwrap();
        let cfg = crate::config::Config::load_or_create(dir.path()).unwrap();
        let public_id = cfg.public_id();
        Registry::open(dir.path().join("registry.redb"))
            .unwrap()
            .create_ring("friends")
            .unwrap();

        let err = run(
            RingCmd::Add {
                ring: "friends".into(),
                peer: public_id.to_string(),
                nickname: None,
            },
            dir.path(),
        )
        .unwrap_err();
        assert!(err.to_string().contains("yourself"));
    }

    #[test]
    fn ring_add_to_open_ring_does_not_add_member() {
        let dir = TempDir::new().unwrap();
        crate::config::Config::load_or_create(dir.path()).unwrap();
        let peer = iroh::SecretKey::generate().public();

        run(
            RingCmd::Add {
                ring: OPEN_RING_NAME.into(),
                peer: peer.to_string(),
                nickname: None,
            },
            dir.path(),
        )
        .unwrap();

        assert_eq!(
            Registry::open(dir.path().join("registry.redb"))
                .unwrap()
                .list_members(OPEN_RING_NAME)
                .unwrap()
                .len(),
            0
        );
    }
}