use std::io::Write;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use scout::LiteClient;
mod registry;
#[derive(Parser)]
#[command(name = "scout", version, about = "A light Swarm client: read via a gateway, write via a stamped node")]
struct Cli {
#[arg(long, env = "BEE_GATEWAY", default_value = "https://download.gateway.ethswarm.org", global = true)]
gateway: String,
#[arg(long, env = "BEE_NODE", default_value = "http://localhost:1633", global = true)]
node: String,
#[arg(long, env = "BEE_STAMP", global = true)]
stamp: Option<String>,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
Cat { reference: String, path: Option<String> },
Get { reference: String, out: PathBuf, path: Option<String> },
Bytes { reference: String },
Up {
file: PathBuf,
#[arg(long)]
name: Option<String>,
#[arg(long, default_value = "application/octet-stream")]
content_type: String,
},
UpBytes { file: PathBuf },
UpDir {
folder: PathBuf,
#[arg(long, default_value = "index.html")]
index: String,
},
Publish {
reference: String,
#[arg(long, env = "SCOUT_KEY")]
key: String,
#[arg(long, short)]
topic: String,
},
Keygen,
Share {
file: PathBuf,
#[arg(long = "to")]
to: Vec<String>,
#[arg(long, default_value = "application/octet-stream")]
content_type: String,
},
Shares,
Revoke {
id: String,
#[arg(long = "grantee")]
grantee: Vec<String>,
},
Grantees { id: String },
Fetch {
file_ref: String,
#[arg(long)]
publisher: String,
#[arg(long)]
history: String,
out: Option<PathBuf>,
},
}
fn client(cli: &Cli) -> anyhow::Result<LiteClient> {
let mut c = LiteClient::read(&cli.gateway)?;
if let Some(stamp) = &cli.stamp {
c = c.with_write(&cli.node, stamp)?;
}
Ok(c)
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let scout = client(&cli)?;
match &cli.cmd {
Cmd::Cat { reference, path } => {
let data = fetch(&scout, reference, path.as_deref()).await?;
std::io::stdout().write_all(&data)?;
}
Cmd::Get { reference, out, path } => {
let data = fetch(&scout, reference, path.as_deref()).await?;
std::fs::write(out, &data)?;
eprintln!("wrote {} bytes to {}", data.len(), out.display());
}
Cmd::Bytes { reference } => {
let data = scout.bytes(reference).await?;
std::io::stdout().write_all(&data)?;
}
Cmd::Up { file, name, content_type } => {
let data = std::fs::read(file)?;
let n = name.clone().unwrap_or_else(|| {
file.file_name().and_then(|s| s.to_str()).unwrap_or("file").to_string()
});
let reference = scout.up_file(&n, content_type, data).await?;
println!("{reference}");
}
Cmd::UpBytes { file } => {
let data = std::fs::read(file)?;
let reference = scout.up_bytes(data).await?;
println!("{reference}");
}
Cmd::UpDir { folder, index } => {
let path = folder.to_str().ok_or_else(|| anyhow::anyhow!("non-UTF-8 folder path"))?;
let reference = scout.up_dir(path, Some(index)).await?;
println!("{reference}");
}
Cmd::Publish { reference, key, topic } => {
let manifest = scout.publish(key, topic, reference).await?;
eprintln!("feed manifest (read latest with `scout cat <ref>`):");
println!("{manifest}");
}
Cmd::Keygen => {
let (key, owner, pubkey) = scout::generate_key()?;
println!("key: {key}");
println!("owner: {owner}");
println!("pubkey: {pubkey}");
}
Cmd::Share { file, to, content_type } => {
anyhow::ensure!(!to.is_empty(), "at least one --to <pubkey> is required");
let data = std::fs::read(file)?;
let name = file.file_name().and_then(|s| s.to_str()).unwrap_or("share").to_string();
let info = scout.share(&name, content_type, data, to).await?;
let id = format!("{:08x}", registry::now_secs() as u32);
let mut reg = registry::load();
reg.shares.push(registry::Share {
id: id.clone(),
file: name,
file_ref: info.file_ref.clone(),
history: info.history.clone(),
grantee_ref: info.grantee_ref.clone(),
grantee_history: info.grantee_history.clone(),
publisher: info.publisher.clone(),
grantees: to.clone(),
ts: registry::now_secs(),
});
registry::save(®)?;
println!("id: {id}");
println!("file_ref: {}", info.file_ref);
println!("history: {}", info.history);
println!("publisher: {}", info.publisher);
eprintln!("\nmanage: scout grantees {id} | scout revoke {id} --grantee <pk>");
eprintln!("recipient: scout fetch {} --publisher {} --history {}", info.file_ref, info.publisher, info.history);
}
Cmd::Shares => {
let reg = registry::load();
if reg.shares.is_empty() {
println!("(no shares yet)");
} else {
for s in ®.shares {
println!("{} {:<20} {} grantee(s) {}", s.id, s.file, s.grantees.len(), s.file_ref);
}
}
}
Cmd::Revoke { id, grantee } => {
anyhow::ensure!(!grantee.is_empty(), "at least one --grantee <pubkey> is required");
let mut reg = registry::load();
let s = reg
.find(id)
.ok_or_else(|| anyhow::anyhow!("no share with id {id} (see `scout shares`)"))?
.clone();
let (gr, gh) = scout.revoke(&s.grantee_ref, &s.history, grantee).await?;
if let Some(m) = reg.find_mut(id) {
m.grantee_ref = gr.clone();
m.grantee_history = gh;
m.grantees.retain(|g| !grantee.contains(g));
}
registry::save(®)?;
println!("revoked {} key(s) from {id}; new grantee_ref: {gr}", grantee.len());
}
Cmd::Grantees { id } => {
let reg = registry::load();
let s = reg
.find(id)
.ok_or_else(|| anyhow::anyhow!("no share with id {id} (see `scout shares`)"))?;
let live = scout.grantees(&s.grantee_ref).await?;
println!("share {id} ({}): {} live grantee(s)", s.file, live.len());
for g in &live {
println!(" {g}");
}
}
Cmd::Fetch { file_ref, publisher, history, out } => {
let data = scout.fetch_act(file_ref, publisher, history).await?;
match out {
Some(p) => {
std::fs::write(p, &data)?;
eprintln!("wrote {} bytes to {}", data.len(), p.display());
}
None => std::io::stdout().write_all(&data)?,
}
}
}
Ok(())
}
async fn fetch(scout: &LiteClient, reference: &str, path: Option<&str>) -> anyhow::Result<Vec<u8>> {
match path {
Some(p) => scout.cat_path(reference, p).await,
None => scout.cat(reference).await,
}
}