//! tp — tesseras-paste CLI client. //! //! Sends commands to the `tpd` daemon over a Unix socket. //! Reads paste content from stdin (put) and writes it to //! stdout (get). use std::io::{BufRead, BufReader, Read, Write}; use std::os::unix::net::UnixStream; use std::path::PathBuf; #[path = "../base58.rs"] mod base58; fn default_socket() -> PathBuf { PathBuf::from("/var/tesseras-paste/daemon.sock") } fn usage() { eprintln!("usage: tp [-s sock] [args]"); eprintln!(); eprintln!("commands:"); eprintln!(" put [-t ttl] [-p] read stdin, store paste"); eprintln!(" -p public (no encryption)"); eprintln!(" get retrieve paste to stdout"); eprintln!(" del delete paste"); eprintln!(" pin pin (never expires)"); eprintln!(" unpin unpin"); eprintln!(" status show daemon status"); eprintln!(); eprintln!(" -s sock Unix socket path"); eprintln!(" -t ttl time-to-live (e.g. 24h 30m 3600)"); } fn parse_ttl(s: &str) -> Result { let s = s.trim(); if let Some(h) = s.strip_suffix('h') { h.parse::() .map(|v| v * 3600) .map_err(|e| e.to_string()) } else if let Some(m) = s.strip_suffix('m') { m.parse::().map(|v| v * 60).map_err(|e| e.to_string()) } else if let Some(sec) = s.strip_suffix('s') { sec.parse::().map_err(|e| e.to_string()) } else { s.parse::().map_err(|e| e.to_string()) } } fn main() { let args: Vec = std::env::args().collect(); let mut sock_path = default_socket(); let mut cmd_start = 1; // Parse global options before command let mut i = 1; while i < args.len() { match args[i].as_str() { "-s" => { i += 1; sock_path = args.get(i).map(PathBuf::from).unwrap_or_else(|| { eprintln!("error: -s requires path"); std::process::exit(1); }); cmd_start = i + 1; } "-h" | "--help" => { usage(); return; } _ => break, } i += 1; } let cmd_args = &args[cmd_start..]; if cmd_args.is_empty() { usage(); std::process::exit(1); } let command = &cmd_args[0]; let mut is_get = false; let request = match command.as_str() { "put" => { let mut ttl = "24h".to_string(); let mut public = false; let mut j = 1; while j < cmd_args.len() { match cmd_args[j].as_str() { "-t" => { j += 1; if j < cmd_args.len() { ttl = cmd_args[j].clone(); } } "-p" => public = true, _ => {} } j += 1; } let ttl_secs = match parse_ttl(&ttl) { Ok(s) => s, Err(e) => { eprintln!("error: bad TTL: {e}"); std::process::exit(1); } }; let mut content = Vec::new(); if let Err(e) = std::io::stdin().read_to_end(&mut content) { eprintln!("error: reading stdin: {e}"); std::process::exit(1); } if content.is_empty() { eprintln!("error: empty input"); std::process::exit(1); } let cmd = if public { "PUTP" } else { "PUT" }; format!("{cmd} {ttl_secs} {}\n", base58::encode(&content)) } "get" => { let key = cmd_args.get(1).unwrap_or_else(|| { eprintln!("error: get requires a key"); std::process::exit(1); }); is_get = true; format!("GET {key}\n") } "del" => { let key = cmd_args.get(1).unwrap_or_else(|| { eprintln!("error: del requires a key"); std::process::exit(1); }); format!("DEL {key}\n") } "pin" => { let key = cmd_args.get(1).unwrap_or_else(|| { eprintln!("error: pin requires a key"); std::process::exit(1); }); format!("PIN {key}\n") } "unpin" => { let key = cmd_args.get(1).unwrap_or_else(|| { eprintln!("error: unpin requires a key"); std::process::exit(1); }); format!("UNPIN {key}\n") } "status" => "STATUS\n".to_string(), other => { eprintln!("unknown command: {other}"); usage(); std::process::exit(1); } }; let stream = match UnixStream::connect(&sock_path) { Ok(s) => s, Err(e) => { eprintln!("error: cannot connect to {}: {e}", sock_path.display(),); eprintln!("hint: is tpd running?"); std::process::exit(1); } }; stream .set_read_timeout(Some(std::time::Duration::from_secs(60))) .ok(); let mut writer = &stream; if let Err(e) = writer.write_all(request.as_bytes()) { eprintln!("error: writing to socket: {e}"); std::process::exit(1); } let reader = BufReader::new(&stream); for line in reader.lines() { let line = match line { Ok(l) => l, Err(_) => break, }; if let Some(data) = line.strip_prefix("OK ") { if is_get { // Decode base58 → raw bytes → stdout match base58::decode(data) { Some(bytes) => { if let Err(e) = std::io::stdout().write_all(&bytes) { eprintln!("error: writing to stdout: {e}"); std::process::exit(1); } } None => println!("{data}"), } } else { println!("{data}"); } break; } else if let Some(msg) = line.strip_prefix("ERR ") { eprintln!("error: {msg}"); std::process::exit(1); } } }