summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.rs276
-rw-r--r--src/parser.rs38
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!()
+ }
+ }
+}