diff options
Diffstat (limited to '')
-rw-r--r-- | src/main.rs | 386 |
1 files changed, 301 insertions, 85 deletions
diff --git a/src/main.rs b/src/main.rs index b500951..e2fede0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,19 @@ -use std::{io::{Stdin, IsTerminal}, process::{exit, Command, Stdio}}; +use std::{ + fs::File, io::{IsTerminal, Stdin, Write}, path::Path, process::{exit, Command, Stdio}, time::UNIX_EPOCH +}; + +mod parser; +use parser::Parser; struct Config { ssh_destination: String, xochitl_path: String, + // TODO: remove maybe? ssh_key_path: Option<String>, - extensions: Extensions + extensions: Extensions, + tmpdir: String, + uuid: String, } #[derive(Default, Clone)] @@ -19,10 +27,13 @@ fn get_line(stdin: &mut Stdin) -> Option<String> { match stdin.read_line(&mut buf) { Ok(0) => None, Err(err) => { - eprintln!("read encountered error: {}", err); + println!("ERROR read from stdin encountered error: {}", err); None } - _ => Some(buf) + _ => match buf.strip_suffix('\n') { + None => Some(buf), + Some(stripped) => Some(stripped.to_string()) + } } } @@ -39,10 +50,15 @@ fn get_value(stdin: &mut Stdin) -> Option<String> { return None; } } - eprintln!("did not receive value when expected"); + println!("ERROR did not receive value when expected"); exit(1); } +fn get_uuid(stdin: &mut Stdin) -> Option<String> { + println!("GETUUID"); + get_value(stdin) +} + fn get_config_value(stdin: &mut Stdin, key: &str) -> Option<String> { println!("GETCONFIG {}", key); get_value(stdin) @@ -50,13 +66,14 @@ fn get_config_value(stdin: &mut Stdin, key: &str) -> Option<String> { 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(), + ssh_destination: get_config_value(stdin, "ssh_destination").unwrap(), 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() + tmpdir: get_config_value(stdin, "tmpdir") + .unwrap_or_else(|| "/tmp/xochitl".to_owned()), + extensions: extensions.to_owned(), + uuid: get_uuid(stdin).unwrap(), } } @@ -90,7 +107,7 @@ fn startup(stdin: &mut Stdin) -> Config { "INITREMOTE" => { let config = get_config(stdin, &extensions); let check = Command::new("ssh") - .arg(config.ssh_destination) + .arg(&config.ssh_destination) .arg("ls") .arg(config.xochitl_path) .stdout(Stdio::null()) @@ -105,22 +122,26 @@ fn startup(stdin: &mut Stdin) -> Config { println!("INITREMOTE-FAILURE failed to spawn ssh check command"); } } + "EXPORTSUPPORTED" => { + println!("EXPORTSUPPORTED-SUCCESS"); + }, _ => { println!("UNSUPPORTED-REQUEST"); eprintln!("got unsupported verb while getting config: {}", line); } } } - println!("DEBUG done early?"); + // can't print here, pipe has already been closed most of the time + //println!("DEBUG done early?"); exit(0); } -fn key_to_uuid(key: &str) -> String { +fn key_to_uuid(config: &Config, key: &str) -> String { let maybe = Command::new("uuidgen") .arg("-s") .arg("-n") - .arg("435bb27b-6704-416f-bc5b-3e2d02d0cf8f") + .arg(&config.uuid) .arg("-N") .arg(key) .output(); @@ -134,97 +155,271 @@ fn key_to_uuid(key: &str) -> String { } } -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); +fn get_current_time() -> u128 { + std::time::SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() +} + +fn get_exifdata(filepath: &str, key: &str) -> Option<String> { + let maybe = Command::new("exiftool") + .arg(filepath) + .arg("-b") + .arg(format!("-{}", key)) + .output(); + match maybe { + Ok(out) => { + let value = String::from_utf8_lossy(&out.stdout); + if value.split_ascii_whitespace().next().is_some() { + Some(value.to_string()) + } else { + println!("DEBUG could not get exif data for key {}: empty value returned", key); + return None + } + }, + Err(e) => { + println!("DEBUG could not get exif data for key {}: {}", key, e); + return None + } + } +} + +fn check_key_on_device(config: &Config, key: &str) -> bool { + let uuid = key_to_uuid(config, 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 - } + code.success() } else { - println!("CHECKPRESENT-UNKNOWN {} ssh failed", key); + false } } -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)) +fn check_key_present<'a>(config: &Config, parser: &mut Parser) { + let key = parser.next_word(); + eprintln!("{key}"); + parser.assert_end(); + let uuid = key_to_uuid(config, &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!("TRANSFER-SUCCESS STORE {}", key); - } else { - println!("TRANSFER-FAILURE STORE {}", key); // TODO: check if remote present or not + + match check { + Ok(code) if code.success() => + println!("CHECKPRESENT-SUCCESS {}", key), + Ok(_) => + println!("CHECKPRESENT-FAILURE {}", key), + _ => + println!("CHECKPRESENT-UNKNOWN {} ssh failed", key) + } +} + +// see https://www.ietf.org/rfc/rfc4627.txt +fn json_string_escape(str: &str) -> String { + let mut buf = String::with_capacity(str.len()); + for c in str.chars() { + match c { + '"' => buf.push_str("\\\""), + '\\' => buf.push_str("\\\\"), + // technically (maybe?) overshoots? the rfc seems to think only 0x00 + // to 0x1f are control characters + _ if c.is_control() => buf.push_str(&format!("\\u{:04x}", c as u32)), + _ => buf.push(c) } - } else { - println!("TRANSFER-FAILURE STORE {} ssh failed", key); } + buf } -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); +#[test] +fn test_json() { + assert_eq!(json_string_escape("abcd"), "abcd"); + assert_eq!(json_string_escape("ab\"d"), "ab\\\"d"); + assert_eq!(json_string_escape("ab\nd"), "ab\\u000ad"); +} + +fn setup_filestructure(config: &Config, path: &str, uuid: &str, file: &str, exportname: Option<&str>) -> Option<()> { + let title = match exportname { + Some(name) => name.to_string(), + None => match get_exifdata(file, "Title") { + Some(title) => title, + None => { + println!("DEBUG could not get title from exif data, using uuid instead"); + uuid.to_owned() + // TODO: this can't easily access the actual file name, since the file we + // get is actually the version in git annex's store, which is content-addressed, + // and special remotes aren't really supposed to know anything about the file + // they store. + // + // As a workaround, maybe iterate through the repo's file listing until a + // matching symlink is found, than use that one's file name? + } + } + }; + if config.extensions.info { + println!("INFO using title {:?}", title); + } + Command::new("mkdir") + .arg("-p") + .arg(&path) + .status().ok()?; + Command::new("ln") + .arg("-s") + .arg("-r") + .arg(file) + .arg(format!("{}/{}.pdf", path, uuid)) + .status().ok()?; + let mut metadata = File::create(format!("{}/{}.metadata", path, uuid)).ok()?; + metadata.write_all(format!(r#"{{ + "createdTime": "{time}", + "lastModified": "{time}", + "lastOpened": "{time}", + "lastOpenedPage": 0, + "parent": "", + "pinned": false, + "type": "DocumentType", + "visibleName": "{title}" + }}"#, time=get_current_time(), title=json_string_escape(&title)).as_bytes()).ok()?; + let mut content = File::create(format!("{}/{}.content", path, uuid)).ok()?; + content.write_all(format!(r#"{{ + "coverPageNumber": 0, + "documentMetadata": {{}}, + "extraMetadata": {{}}, + "fileType": "pdf", + "fontName": "", + "pageTags": [], + "tags": [], + "textAlignment": "justify", + "textScale": 1, + "zoomMode": "bestFit" + }}"#).as_bytes()).ok()?; + Some(()) +} + + +fn store_key<'a>(config: &Config, parser: &mut Parser, exportname: Option<&str>) { + let key = parser.next_word(); + let file = parser.remainder(); + parser.assert_end(); + + let uuid = key_to_uuid(config, &key); + let path = format!("{}/{}", config.tmpdir, uuid); + + if setup_filestructure(config, &path, &uuid, &file, exportname).is_none() { + println!("TRANSFER-FAILURE STORE {} could not set up xochitl file structure", key); + return + } + + // the spec does not say what should happen if we store an already-present key, + // but the test suite demands it will not fail. + // for simplicity, we delete & re-store the key + // TODO: check for hash-equivalence instead? + if check_key_on_device(config, &key) { + let _ = Command::new("ssh") + .arg(&config.ssh_destination) + .arg(format!("rm -r {dir}/{uuid}*", dir=config.xochitl_path, uuid=uuid)) + .status(); + } + + // TODO: check if anything with that uuid is present already, otherwise destructive + // (thus essentially a stripped-down checkpresent) + let check = Command::new("scp") + .arg("-r") + .arg(format!("{}/{}.pdf", path, uuid)) + .arg(format!("{}/{}.content", path, uuid)) + .arg(format!("{}/{}.metadata", path, uuid)) + .arg(format!("{0}:{1}", config.ssh_destination, config.xochitl_path)) + .status(); + + match check { + Ok(code) if code.success() => + println!("TRANSFER-SUCCESS STORE {}", key), + Ok(_) => + println!("TRANSFER-FAILURE STORE {} scp failed: {check:?}", key), + _ => + println!("TRANSFER-FAILURE STORE {} ssh failed", key) + } + + let cleanup = Command::new("rm") + .arg("-r") + .arg(&path) + .status(); + let maybe_info = if config.extensions.info { "INFO" } else { "DEBUG" }; + + match cleanup { + Ok(code) if !code.success() => + println!("{} could not clean up tmp path {}: `rm' returned code {}", maybe_info, path, code), + Err(_) => + println!("{} could not clean up tmp path {}: could not run `rm'", maybe_info, path), + _ => () + } +} + +fn retrieve_key<'a>(config: &Config, parser: &mut Parser) { + let key = parser.next_word(); + let file = parser.remainder(); + + let uuid = key_to_uuid(config, 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); + + match check { + Ok(code) if code.success() => + println!("TRANSFER-SUCCESS RETRIEVE {}", key), + Ok(_) => + println!("TRANSFER-FAILURE RETRIEVE {} {} {}", key, config.xochitl_path, uuid), + Err(_) => + 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); +fn remove_key<'a>(config: &Config, parser: &mut Parser) { + let key = parser.next_word(); + parser.assert_end(); + let uuid = key_to_uuid(config, key); + + // trivially done if we don't have the key at all + // (calling this "success" is explicitely allowed by the spec) + if !check_key_on_device(config, key) { + println!("REMOVE-SUCCESS {}", key); + return + } 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); + + match cmd { + Ok(code) if code.success() => + println!("REMOVE-SUCCESS {}", key), + Ok(_) => + println!("REMOVE-FAILURE {} command failed", key), + Err(_) => + 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); +fn whereis(config: &Config, parser: &mut Parser) { + let key = parser.next_word(); + parser.assert_end(); + let uuid = key_to_uuid(config, 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 is_available(config: &Config) { + let cmd = Command::new("ssh") + .arg("-o ConnectTimeout=1") + .arg(&config.ssh_destination) + .arg(format!("ls {}", config.xochitl_path)) + .status(); + + match cmd { + Ok(code) if code.success() => println!("AVAILABILITY LOCAL"), + _ => println!("AVAILABILITY UNAVAILABLE") } } @@ -239,21 +434,42 @@ fn main() { 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), + + let mut parser = Parser::new(&line); + + match parser.next_word() { + "CHECKPRESENT" => check_key_present(&config, &mut parser), + "TRANSFER" => match parser.next_word() { + "STORE" => store_key(&config, &mut parser, None), + "RETRIEVE" => retrieve_key(&config, &mut parser), _ => 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? + "REMOVE" => remove_key(&config, &mut parser), + "WHEREIS" => whereis(&config, &mut parser), + "GETAVAILABILITY" => is_available(&config), + "EXPORT" => { + // the 'path' is only used as a name + let filepath = parser.remainder(); + let name = Path::new(&filepath).file_stem().unwrap().to_string_lossy(); + + let line = get_line(&mut stdin) + .expect("no command after EXPORT"); + + let mut parser = Parser::new(&line); + match parser.next_word() { + "TRANSFEREXPORT" => match parser.next_word() { + "STORE" => store_key(&config, &mut parser, Some(&name)), + "RETRIEVE" => retrieve_key(&config, &mut parser), + _ => println!("UNSUPPORTED-REQUEST") + } + "CHECKPRESENTEXPORT" => check_key_present(&config, &mut parser), + "REMOVEEXPORT" => remove_key(&config, &mut parser), + // REMOVEEXPORTDIRECTORY is left unimplemented + // (no directories on the reMarkable device) + // TODO implement RENAMEEXPORT? + // (would require editing json from under xochitl) + _ => println!("UNSUPPORTED-REQUEST") + } } _ => println!("UNSUPPORTED-REQUEST") } |