diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Cargo.lock | 7 | ||||
-rw-r--r-- | Cargo.toml | 8 | ||||
-rw-r--r-- | src/main.rs | 262 |
4 files changed, 278 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5fc74b0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "git-annex-remote-remarkable2" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4fb32cf --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "git-annex-remote-remarkable2" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b500951 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,262 @@ +use std::{io::{Stdin, IsTerminal}, process::{exit, Command, Stdio}}; + + +struct Config { + ssh_destination: String, + xochitl_path: String, + ssh_key_path: Option<String>, + extensions: Extensions +} + +#[derive(Default, Clone)] +struct Extensions { + unavailable_response: bool, + info: bool +} + +fn get_line(stdin: &mut Stdin) -> Option<String> { + let mut buf = String::new(); + match stdin.read_line(&mut buf) { + Ok(0) => None, + Err(err) => { + eprintln!("read encountered error: {}", err); + None + } + _ => Some(buf) + } +} + +fn get_value(stdin: &mut Stdin) -> Option<String> { + if let Some(line) = get_line(stdin) { + if let Some(value) = line.strip_prefix("VALUE ") { + if value.split_ascii_whitespace().count() == 0 { + return None + } else { + return Some(value.to_owned()); + } + } + if line == "VALUE" { + return None; + } + } + eprintln!("did not receive value when expected"); + exit(1); +} + +fn get_config_value(stdin: &mut Stdin, key: &str) -> Option<String> { + println!("GETCONFIG {}", key); + get_value(stdin) +} + +fn get_config(stdin: &mut Stdin, extensions: &Extensions) -> Config { + Config { + ssh_destination: get_config_value(stdin, "ssh_destination").unwrap() + .strip_suffix("\n").unwrap().to_string(), + xochitl_path: get_config_value(stdin, "xochitl_directory") + .map(|s| s.strip_suffix("\n").unwrap().to_string()) + .unwrap_or_else(|| ".local/share/remarkable/xochitl".to_owned()), + ssh_key_path: get_config_value(stdin, "ssh_key"), + extensions: extensions.to_owned() + } +} + +fn startup(stdin: &mut Stdin) -> Config { + let mut extensions = Extensions::default(); + while let Some(line) = get_line(stdin) { + let mut words = line.split_ascii_whitespace(); + let verb = words.next().expect("missing command verb"); + match verb { + "EXTENSIONS" => { + let supported: Vec<_> = words.collect(); + extensions.unavailable_response = supported.contains(&"UNAVAILABLERESPONSE"); + extensions.info = supported.contains(&"INFO"); + println!( + "EXTENSIONS {} {}", + if extensions.unavailable_response { "UNAVAILABLERESPONSE" } else { "" }, + if extensions.info { "INFO" } else { "" } + ) + } + "PREPARE" => { + let config = get_config(stdin, &extensions); + println!("PREPARE-SUCCESS"); + return config; + }, + "LISTCONFIGS" => { + println!("CONFIG ssh_destination ssh destination under which the reMarkable tablet can be reached."); + println!("CONFIG ssh_key the path to an ssh key to use (optional)."); + println!("CONFIG xochitl_directory the directory on the remarkable device in which data is stored (default .local/share/remarkable/xochitl)."); + println!("CONFIGEND") + }, + "INITREMOTE" => { + let config = get_config(stdin, &extensions); + let check = Command::new("ssh") + .arg(config.ssh_destination) + .arg("ls") + .arg(config.xochitl_path) + .stdout(Stdio::null()) + .status(); + if let Ok(status) = check { + if status.success() { + println!("INITREMOTE-SUCCESS"); + } else { + println!("INITREMOTE-FAILURE ssh check returned {}", status); + } + } else { + println!("INITREMOTE-FAILURE failed to spawn ssh check command"); + } + } + _ => { + println!("UNSUPPORTED-REQUEST"); + eprintln!("got unsupported verb while getting config: {}", line); + } + } + } + println!("DEBUG done early?"); + exit(0); +} + + +fn key_to_uuid(key: &str) -> String { + let maybe = Command::new("uuidgen") + .arg("-s") + .arg("-n") + .arg("435bb27b-6704-416f-bc5b-3e2d02d0cf8f") + .arg("-N") + .arg(key) + .output(); + match maybe { + Ok(uuid) => String::from_utf8_lossy(&uuid.stdout) + .split_ascii_whitespace().next().unwrap().to_owned(), + Err(e) => { + println!("DEBUG conversion to uuid failed: {}", e); + exit(1) + } + } +} + +fn check_key_present<'a>(config: &Config, mut words: impl Iterator<Item=&'a str>) { + let key = words.next().unwrap(); + assert_no_args_left(words); + let uuid = key_to_uuid(key); + let check = Command::new("ssh") + .arg(&config.ssh_destination) + .arg(format!("test -f {dir}/{uuid}.pdf && test ! -f {dir}/{uuid}.tombstone", dir=config.xochitl_path, uuid=uuid)) + .status(); + if let Ok(code) = check { + if code.success() { + println!("CHECKPRESENT-SUCCESS {}", key); + } else { + println!("CHECKPRESENT-FAILURE {}", key); // TODO: check if remote present or not + } + } else { + println!("CHECKPRESENT-UNKNOWN {} ssh failed", key); + } +} + +fn store_key<'a>(config: &Config, mut words: impl Iterator<Item=&'a str>) { + let key = words.next().unwrap(); + let file = words.next().unwrap(); + assert_no_args_left(words); + let uuid = key_to_uuid(key); + let check = Command::new("scp") + .arg(file) + .arg(format!("{}:{}/{}.pdf", config.ssh_destination, config.xochitl_path, uuid)) + .status(); + if let Ok(code) = check { + if code.success() { + println!("TRANSFER-SUCCESS STORE {}", key); + } else { + println!("TRANSFER-FAILURE STORE {}", key); // TODO: check if remote present or not + } + } else { + println!("TRANSFER-FAILURE STORE {} ssh failed", key); + } +} + +fn retrieve_key<'a>(config: &Config, mut words: impl Iterator<Item=&'a str>) { + let key = words.next().unwrap(); + let file = words.next().unwrap(); + assert_no_args_left(words); + let uuid = key_to_uuid(key); + let check = Command::new("scp") + .arg(format!("{}:{}/{}.pdf", config.ssh_destination, config.xochitl_path, uuid)) + .arg(file) + .status(); + if let Ok(code) = check { + if code.success() { + println!("TRANSFER-SUCCESS RETRIEVE {}", key); + } else { + println!("TRANSFER-FAILURE RETRIEVE {}", key); // TODO: check if remote present or not + } + } else { + println!("TRANSFER-FAILURE RETRIEVE {} ssh failed", key); + } +} + +fn remove_key<'a>(config: &Config, mut words: impl Iterator<Item=&'a str>) { + let key = words.next().unwrap(); + assert_no_args_left(words); + let uuid = key_to_uuid(key); + + let cmd = Command::new("ssh") + .arg(&config.ssh_destination) + .arg(format!("rm -r {dir}/{uuid}*", dir=config.xochitl_path, uuid=uuid)) + .status(); + if let Ok(code) = cmd { + if code.success() { + println!("REMOVE-SUCCESS {}", key); + } else { + println!("REMOVE-FAILURE {} command failed", key); // TODO: check if remote present or not + } + } else { + println!("REMOVE-FAILURE {} ssh failed", key); + } +} + +fn whereis<'a>(config: &Config, mut words: impl Iterator<Item=&'a str>) { + let key = words.next().unwrap(); + assert_no_args_left(words); + let uuid = key_to_uuid(key); + println!("WHEREIS-SUCCESS {ssh}:{dir}/{uuid}.pdf", ssh=config.ssh_destination, dir=config.xochitl_path, uuid=uuid); +} + +fn assert_no_args_left<'a>(mut words: impl Iterator<Item=&'a str>) { + if words.next().is_some() { + println!("UNSUPPORTED-REQUEST"); + println!("ERROR"); + exit(1); + } +} + +fn main() { + let mut stdin = std::io::stdin(); + if stdin.is_terminal() { + eprintln!("warning: this program is not meant to be invoked by hand."); + } + + println!("VERSION 2"); + + let config = startup(&mut stdin); + + while let Some(line) = get_line(&mut stdin) { + let mut words = line.split_ascii_whitespace(); + let verb = words.next().expect("missing command verb"); + match verb { + "CHECKPRESENT" => check_key_present(&config, words), + "TRANSFER" => match words.next().expect("missing STORE/RETRIEVE") { + "STORE" => store_key(&config, words), + "RETRIEVE" => retrieve_key(&config, words), + _ => println!("UNSUPPORTED-REQUEST") + } + "REMOVE" => remove_key(&config, words), + "WHEREIS" => whereis(&config, words), + "GETAVAILABILITY" => { + println!("ERROR"); + unimplemented!() + // check routing table if usb network device is present? + } + _ => println!("UNSUPPORTED-REQUEST") + } + } + +} |