diff options
author | stuebinm | 2025-01-06 02:48:29 +0100 |
---|---|---|
committer | stuebinm | 2025-01-06 02:52:11 +0100 |
commit | c363db95be7e2520e5c69c49733544fe98dff838 (patch) | |
tree | 445f9b41f34a05e9d48506cd666bde376d289090 /src | |
parent | 6befe9f334a3384e6a6bf494211edb4e549efa2f (diff) |
new parser, support exporttree, generally nicer code
Diffstat (limited to 'src')
-rw-r--r-- | src/main.rs | 276 | ||||
-rw-r--r-- | src/parser.rs | 38 |
2 files changed, 216 insertions, 98 deletions
diff --git a/src/main.rs b/src/main.rs index 8085ec6..e2fede0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,19 @@ -use std::{io::{Stdin, IsTerminal, Write}, process::{exit, Command, Stdio}, fs::File}; +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, tmpdir: String, - uuid: String + uuid: String, } #[derive(Default, Clone)] @@ -21,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()) + } } } @@ -48,13 +57,11 @@ fn get_value(stdin: &mut Stdin) -> Option<String> { fn get_uuid(stdin: &mut Stdin) -> Option<String> { println!("GETUUID"); get_value(stdin) - .map(|s| s.strip_suffix("\n").unwrap().to_string()) } fn get_config_value(stdin: &mut Stdin, key: &str) -> Option<String> { println!("GETCONFIG {}", key); get_value(stdin) - .map(|s| s.strip_suffix("\n").unwrap().to_string()) } fn get_config(stdin: &mut Stdin, extensions: &Extensions) -> Config { @@ -66,7 +73,7 @@ fn get_config(stdin: &mut Stdin, extensions: &Extensions) -> Config { tmpdir: get_config_value(stdin, "tmpdir") .unwrap_or_else(|| "/tmp/xochitl".to_owned()), extensions: extensions.to_owned(), - uuid: get_uuid(stdin).unwrap() + uuid: get_uuid(stdin).unwrap(), } } @@ -100,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()) @@ -115,13 +122,17 @@ 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); } @@ -144,6 +155,10 @@ fn key_to_uuid(config: &Config, key: &str) -> String { } } +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) @@ -167,22 +182,36 @@ fn get_exifdata(filepath: &str, key: &str) -> Option<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); +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 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(); + + match check { + Ok(code) if code.success() => + println!("CHECKPRESENT-SUCCESS {}", key), + Ok(_) => + println!("CHECKPRESENT-FAILURE {}", key), + _ => + println!("CHECKPRESENT-UNKNOWN {} ssh failed", key) } } @@ -209,19 +238,22 @@ fn test_json() { assert_eq!(json_string_escape("ab\nd"), "ab\\u000ad"); } -fn setup_filestructure(config: &Config, path: &str, uuid: &str, file: &str) -> Option<()> { - let title = 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? +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 { @@ -241,13 +273,13 @@ fn setup_filestructure(config: &Config, path: &str, uuid: &str, file: &str) -> O metadata.write_all(format!(r#"{{ "createdTime": "{time}", "lastModified": "{time}", - "lastOpened": "0", + "lastOpened": "{time}", "lastOpenedPage": 0, "parent": "", "pinned": false, "type": "DocumentType", "visibleName": "{title}" - }}"#, time=10, title=json_string_escape(&title)).as_bytes()).ok()?; + }}"#, 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, @@ -265,34 +297,47 @@ fn setup_filestructure(config: &Config, path: &str, uuid: &str, file: &str) -> O } -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(config, key); +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).is_none() { + 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!("{}:{}", config.ssh_destination, config.xochitl_path)) + .arg(format!("{0}:{1}", config.ssh_destination, config.xochitl_path)) .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); + + 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") @@ -300,67 +345,81 @@ fn store_key<'a>(config: &Config, mut words: impl Iterator<Item=&'a str>) { .arg(&path) .status(); let maybe_info = if config.extensions.info { "INFO" } else { "DEBUG" }; - if let Ok(code) = cleanup { - if !code.success() { - println!("{} could not clean up tmp path {}: `rm' returned code {}", maybe_info, path, code); - } - } else { - println!("{} could not clean up tmp path {}: could not run `rm'", maybe_info, path); + + 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, mut words: impl Iterator<Item=&'a str>) { - let key = words.next().unwrap(); - let file = words.next().unwrap(); - assert_no_args_left(words); +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); +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); +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") } } @@ -375,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") } diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..13cd22b --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,38 @@ + +pub struct Parser<'a> { + buf: &'a str, + offset: usize +} + +impl<'a> Parser<'a> { + pub fn new(buf: &'a str) -> Self { + Parser{buf, offset: 0} + } + + pub fn next_word(&mut self) -> &'a str { + match self.buf[self.offset..].split_once(' ') { + Some((a,_)) => { + self.offset += a.len() + 1; + a + }, + None => { + let res = &self.buf[self.offset..]; + self.offset += self.buf.len() - self.offset; + res + } + } + } + + pub fn remainder(&mut self) -> String { + let res = self.buf[self.offset..].to_owned(); + self.offset = self.buf.len(); + res + } + + pub fn assert_end(&self) { + if self.buf.len() != self.offset { + println!("ERROR: failed parsing"); + panic!() + } + } +} |