aboutsummaryrefslogtreecommitdiffstats
path: root/src/bin/tp.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/tp.rs')
-rw-r--r--src/bin/tp.rs206
1 files changed, 206 insertions, 0 deletions
diff --git a/src/bin/tp.rs b/src/bin/tp.rs
new file mode 100644
index 0000000..e33c357
--- /dev/null
+++ b/src/bin/tp.rs
@@ -0,0 +1,206 @@
+//! 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] <command> [args]");
+ eprintln!();
+ eprintln!("commands:");
+ eprintln!(" put [-t ttl] [-p] read stdin, store paste");
+ eprintln!(" -p public (no encryption)");
+ eprintln!(" get <key> retrieve paste to stdout");
+ eprintln!(" del <key> delete paste");
+ eprintln!(" pin <key> pin (never expires)");
+ eprintln!(" unpin <key> 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<u64, String> {
+ let s = s.trim();
+ if let Some(h) = s.strip_suffix('h') {
+ h.parse::<u64>()
+ .map(|v| v * 3600)
+ .map_err(|e| e.to_string())
+ } else if let Some(m) = s.strip_suffix('m') {
+ m.parse::<u64>().map(|v| v * 60).map_err(|e| e.to_string())
+ } else if let Some(sec) = s.strip_suffix('s') {
+ sec.parse::<u64>().map_err(|e| e.to_string())
+ } else {
+ s.parse::<u64>().map_err(|e| e.to_string())
+ }
+}
+
+fn main() {
+ let args: Vec<String> = 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);
+ }
+ }
+}