Skip to main content

git_remote_iroh/
client.rs

1use std::str::FromStr;
2
3use anyhow::Context;
4use clap::Parser;
5use iroh::{Endpoint, EndpointAddr, SecretKey};
6use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, stdin};
7use tracing::debug;
8
9use crate::{GIT_IROH_ALPN, GitOp, keys, parse_iroh_url};
10
11#[derive(Parser, Debug)]
12#[command()]
13pub struct ClientArgs {
14    remote_name: String,
15    url: String,
16
17    #[arg(env = "GIT_DIR")]
18    git_dir: String,
19}
20
21pub struct Client {
22    args: ClientArgs,
23    key: SecretKey,
24}
25
26impl Client {
27    pub fn new(args: ClientArgs) -> Result<Self, anyhow::Error> {
28        let key = keys::load_or_create_key(&args.git_dir)?;
29        Ok(Self { args, key })
30    }
31
32    pub async fn run<R: AsyncRead + Unpin, W: AsyncWrite + Unpin>(
33        self,
34        input: R,
35        mut output: W,
36    ) -> Result<(), anyhow::Error> {
37        let bufr = BufReader::new(input);
38        let mut lines = bufr.lines();
39        loop {
40            let line = match lines.next_line().await? {
41                Some(line) => line,
42                None => break,
43            };
44            let command: Vec<&str> = line.split_whitespace().collect();
45            if command.is_empty() {
46                continue;
47            }
48            debug!(?command, "client");
49            match command[0] {
50                "capabilities" => self.capabilities(&mut output).await?,
51                "connect" => {
52                    if command.len() < 2 {
53                        anyhow::bail!("connect: missing <url>");
54                    }
55                    self.connect(&mut output, command[1]).await?
56                }
57                s => anyhow::bail!("unsupported command: {:?}", s),
58            }
59            output.write(b"\n").await?;
60        }
61        Ok(())
62    }
63
64    async fn capabilities<W: AsyncWrite + Unpin>(
65        &self,
66        output: &mut W,
67    ) -> Result<(), anyhow::Error> {
68        output.write(b"connect\n").await?;
69        Ok(())
70    }
71
72    async fn connect<W: AsyncWrite + Unpin>(
73        &self,
74        output: &mut W,
75        cmd: &str,
76    ) -> Result<(), anyhow::Error> {
77        let remote_node_id = parse_iroh_url(self.args.url.as_str())?;
78        let remote_op = GitOp::from_str(cmd)?;
79        let ep = Endpoint::builder(iroh::endpoint::presets::N0)
80            .secret_key(self.key.clone())
81            .bind()
82            .await?;
83
84        ep.online().await;
85
86        // connect to iroh URL at url
87        let addr = EndpointAddr::new(remote_node_id);
88        let conn = ep.connect(addr, GIT_IROH_ALPN).await?;
89        let (mut send, mut recv) = conn.open_bi().await?;
90
91        // send op
92        send.write_u8(remote_op.to_byte()).await?;
93
94        // Send a single newline to indicate we're connecting the remote stream
95        println!();
96
97        // Connect local stdin/stdout to remote socket send/recv pair
98        let mut stdin = stdin();
99        let (r1, r2) = tokio::join!(
100            tokio::io::copy(&mut stdin, &mut send),
101            tokio::io::copy(&mut recv, output),
102        );
103        r1.context("stdin -> send")?;
104        r2.context("recv -> stdout")?;
105        ep.close().await;
106        Ok(())
107    }
108}