From 6befe9f334a3384e6a6bf494211edb4e549efa2f Mon Sep 17 00:00:00 2001 From: stuebinm Date: Thu, 7 Mar 2024 20:59:58 +0100 Subject: set up xochitl file structure, then scp this results in a semi-workable thing. the biggest problem is that special remotes aren't really supposed to know much (or anything) about the files they store, which is an assumption I break in the most direct way possible: it only makes sense to store pdfs on a reMarkable. So this now uses exiftool to get the pdf's title. Unfortunately, most of my pdfs turn out to not have any titles – and those that have often have useless titles set (e.g. "Microsoft Word Document" or some such). Will have to think about that one a bit .. --- src/main.rs | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 152 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index b500951..8085ec6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ -use std::{io::{Stdin, IsTerminal}, process::{exit, Command, Stdio}}; +use std::{io::{Stdin, IsTerminal, Write}, process::{exit, Command, Stdio}, fs::File}; struct Config { ssh_destination: String, xochitl_path: String, ssh_key_path: Option, - extensions: Extensions + extensions: Extensions, + tmpdir: String, + uuid: String } #[derive(Default, Clone)] @@ -39,24 +41,32 @@ fn get_value(stdin: &mut Stdin) -> Option { 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 { + println!("GETUUID"); + get_value(stdin) + .map(|s| s.strip_suffix("\n").unwrap().to_string()) +} + fn get_config_value(stdin: &mut Stdin, key: &str) -> Option { println!("GETCONFIG {}", key); get_value(stdin) + .map(|s| s.strip_suffix("\n").unwrap().to_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() } } @@ -116,11 +126,11 @@ fn startup(stdin: &mut Stdin) -> Config { } -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,10 +144,33 @@ fn key_to_uuid(key: &str) -> String { } } +fn get_exifdata(filepath: &str, key: &str) -> Option { + 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_present<'a>(config: &Config, mut words: impl Iterator) { let key = words.next().unwrap(); assert_no_args_left(words); - let uuid = key_to_uuid(key); + 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)) @@ -153,14 +186,104 @@ fn check_key_present<'a>(config: &Config, mut words: impl Iterator } } +// 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) + } + } + buf +} + +#[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) -> 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? + } + }; + 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": "0", + "lastOpenedPage": 0, + "parent": "", + "pinned": false, + "type": "DocumentType", + "visibleName": "{title}" + }}"#, time=10, 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, mut words: impl Iterator) { let key = words.next().unwrap(); let file = words.next().unwrap(); assert_no_args_left(words); - let uuid = key_to_uuid(key); + let uuid = key_to_uuid(config, key); + let path = format!("{}/{}", config.tmpdir, uuid); + + if setup_filestructure(config, &path, &uuid, file).is_none() { + println!("TRANSFER-FAILURE STORE {} could not set up xochitl file structure", key); + return + } + + // TODO: check if anything with that uuid is present already, otherwise destructive let check = Command::new("scp") - .arg(file) - .arg(format!("{}:{}/{}.pdf", config.ssh_destination, config.xochitl_path, uuid)) + .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)) .status(); if let Ok(code) = check { if code.success() { @@ -171,13 +294,26 @@ fn store_key<'a>(config: &Config, mut words: impl Iterator) { } else { 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" }; + 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); + } } fn retrieve_key<'a>(config: &Config, mut words: impl Iterator) { let key = words.next().unwrap(); let file = words.next().unwrap(); assert_no_args_left(words); - let uuid = key_to_uuid(key); + let uuid = key_to_uuid(config, key); let check = Command::new("scp") .arg(format!("{}:{}/{}.pdf", config.ssh_destination, config.xochitl_path, uuid)) .arg(file) @@ -196,7 +332,7 @@ fn retrieve_key<'a>(config: &Config, mut words: impl Iterator) { fn remove_key<'a>(config: &Config, mut words: impl Iterator) { let key = words.next().unwrap(); assert_no_args_left(words); - let uuid = key_to_uuid(key); + let uuid = key_to_uuid(config, key); let cmd = Command::new("ssh") .arg(&config.ssh_destination) @@ -216,7 +352,7 @@ fn remove_key<'a>(config: &Config, mut words: impl Iterator) { fn whereis<'a>(config: &Config, mut words: impl Iterator) { let key = words.next().unwrap(); assert_no_args_left(words); - let uuid = key_to_uuid(key); + let uuid = key_to_uuid(config, key); println!("WHEREIS-SUCCESS {ssh}:{dir}/{uuid}.pdf", ssh=config.ssh_destination, dir=config.xochitl_path, uuid=uuid); } -- cgit v1.2.3