diff --git a/Cargo.lock b/Cargo.lock index 2d33e71..8d6732a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "adler" version = "0.2.3" @@ -118,6 +120,7 @@ dependencies = [ "libc", "num-integer", "num-traits", + "serde", "time", "winapi 0.3.9", ] @@ -149,7 +152,7 @@ dependencies = [ [[package]] name = "ensync" -version = "1.0.0" +version = "1.0.1" dependencies = [ "chrono", "clap", @@ -167,6 +170,8 @@ dependencies = [ "regex", "rpassword", "rust-crypto", + "serde", + "serde_json", "sqlite", "structopt", "tempfile", @@ -316,6 +321,12 @@ dependencies = [ "libc", ] +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -705,6 +716,12 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + [[package]] name = "same-file" version = "1.0.1" @@ -716,9 +733,34 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.27" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db99f3919e20faa51bb2996057f5031d8685019b5a06139b1ce761da671b8526" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +dependencies = [ + "itoa", + "ryu", + "serde", +] [[package]] name = "slab" @@ -764,9 +806,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "structopt" -version = "0.3.21" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" dependencies = [ "clap", "lazy_static 1.4.0", @@ -775,9 +817,9 @@ dependencies = [ [[package]] name = "structopt-derive" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" dependencies = [ "heck", "proc-macro-error", diff --git a/Cargo.toml b/Cargo.toml index 0fd7d5c..697060b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,7 @@ location is not fully trusted. """ [dependencies] -# Can't upgrade to 0.3 because of rust-crypto dependency -chrono = { version = "0.4.19" } +chrono = { version = "0.4.19", features = ["serde"] } flate2 = "1.0.20" fourleaf = "0.1.1" error-chain = "0.12.4" @@ -32,10 +31,12 @@ rpassword = { version = "5.0.1", optional = true } # Scrypt. rust-crypto = "0.2.36" sqlite = "0.23.9" -structopt = "0.3.21" +structopt = "0.3.26" tempfile = "3.2.0" tiny-keccak = "1.4.0" toml = { version = "0.4.5", default-features = false } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" [dependencies.clap] version = "2.33.3" diff --git a/src/cli/cmd_sync.rs b/src/cli/cmd_sync.rs index 0fd8931..5d95ef5 100644 --- a/src/cli/cmd_sync.rs +++ b/src/cli/cmd_sync.rs @@ -22,7 +22,7 @@ use std::collections::HashSet; use std::ffi::OsStr; use std::fmt; use std::fs; -use std::io::{self, stderr, stdout, Read, Write}; +use std::io::{self, stderr, stdout, BufWriter, Read, Write}; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::sync::atomic::Ordering::SeqCst; @@ -30,7 +30,9 @@ use std::sync::{Arc, Mutex, RwLock}; use std::thread; use std::time::Duration; +use chrono::Utc; use libc::isatty; +use serde::Serialize; use crate::ancestor::*; use crate::cli::config::Config; @@ -82,6 +84,7 @@ impl<'a> fmt::Display for PathDisplay<'a, &'a OsStr> { ) } } + impl<'a> fmt::Display for PathDisplay<'a, (&'a OsStr, &'a OsStr)> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let prefix = (self.1).0.as_path().strip_prefix(self.0).unwrap(); @@ -93,6 +96,16 @@ impl<'a> fmt::Display for PathDisplay<'a, (&'a OsStr, &'a OsStr)> { } } +impl<'a> fmt::Display for PathDisplay<'a, (&'a OsStr, Option<&'a OsStr>)> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(name) = (self.1).1 { + PathDisplay(self.0, ((self.1).0, name)).fmt(f) + } else { + PathDisplay(self.0, (self.1).0).fmt(f) + } + } +} + #[derive(Debug)] struct LoggerImpl { client_root: PathBuf, @@ -104,6 +117,7 @@ struct LoggerImpl { created_directories: RwLock>, recdel_directories: RwLock>, spin: Option>, + json_status_out: Option>>, } #[derive(Debug, Default)] @@ -121,6 +135,72 @@ struct ReplicaSpinState { transfer: u64, } +#[derive(Serialize)] +struct LogJson<'a> { + status_version: usize, + time: chrono::DateTime, + #[serde(flatten)] + contents: LogJsonContents<'a>, + #[serde(flatten)] + location: Option>, +} + +#[derive(Serialize)] +#[serde(tag = "type", rename_all = "snake_case")] +enum LogJsonContents<'a> { + SyncStarted, + SyncFinished, + SyncConflict { + #[serde(rename = "conflict_type")] + type_: &'a str, + #[serde(rename = "conflict_reconciliation")] + reconciliation: &'a str, + }, + SyncCreate, + SyncUpdate { + #[serde(rename = "update_old_info")] + old_info: LogJsonPathInfo, + #[serde(rename = "update_new_info")] + new_info: LogJsonPathInfo, + }, + SyncRename { + #[serde(rename = "rename_new_name")] + new_name: String, + }, + SyncRemove, + SyncRemoveRecursively, + SyncRemoveDirectory, + SyncError { + error_message: String, + }, +} + +#[derive(Serialize)] +struct LogJsonLocation<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + side: Option<&'a str>, + path: String, + #[serde(skip_serializing_if = "Option::is_none")] + info: Option, +} + +#[derive(Serialize)] +#[serde(tag = "type", rename_all = "snake_case")] +enum LogJsonPathInfo { + Directory { + mode: u32, + }, + File { + mode: u32, + size: u64, + modified_time: chrono::DateTime, + }, + Symlink { + target: String, + }, + Special, +} + impl Logger for LoggerImpl { fn log(&self, level: LogLevel, what: &Log) { if level <= self.verbose_level { @@ -129,6 +209,78 @@ impl Logger for LoggerImpl { if level <= self.itemise_level { self.write_itemised(what); } + + if let Some(json_status_out_mutex) = &self.json_status_out { + let mut json_status_out = json_status_out_mutex.lock().unwrap(); + + self.write_json_status(&mut json_status_out, what); + } + } +} + +/// Gets the basename of the path of the given `ErrorOperation`, if any. +fn get_error_basename<'a>(op: &'a ErrorOperation) -> Option<&'a OsStr> { + match *op { + ErrorOperation::List => None, + ErrorOperation::MarkClean => None, + ErrorOperation::Chdir(name) => Some(name), + ErrorOperation::Create(name) => Some(name), + ErrorOperation::Update(name) => Some(name), + ErrorOperation::Rename(name) => Some(name), + ErrorOperation::Remove(name) => Some(name), + ErrorOperation::Rmdir => None, + ErrorOperation::Access(name) => Some(name), + } +} + +/// Format the given `ErrorOperation` into a human-readable error message. +fn format_error_log<'a>(op: &'a ErrorOperation, err: &'a Error) -> String { + let mut errs = err.to_string(); + + for e in err.iter().skip(1) { + errs.push_str(&format!("\ncaused by: {}", e)); + } + + if let Some(bt) = err.backtrace() { + errs.push_str(&format!("\n{:?}\n", bt)); + } + + match *op { + ErrorOperation::List => { + format!("Failed to list directory: {}", errs) + } + + ErrorOperation::MarkClean => { + format!("Failed to mark directory clean: {}", errs) + } + + ErrorOperation::Chdir(..) => { + format!("Failed to enter directory: {}", errs) + } + + ErrorOperation::Create(..) => { + format!("Failed to create: {}", errs) + } + + ErrorOperation::Update(..) => { + format!("Failed to update: {}", errs) + } + + ErrorOperation::Rename(..) => { + format!("Failed to rename: {}", errs) + } + + ErrorOperation::Remove(..) => { + format!("Failed to remove: {}", errs) + } + + ErrorOperation::Rmdir => { + format!("Failed to remove: {}", errs) + } + + ErrorOperation::Access(..) => { + format!("Failed to access: {}", errs) + } } } @@ -264,6 +416,7 @@ impl LoggerImpl { } match *what { + Log::SyncStarted | Log::SyncFinished => {} Log::Inspect(dir, name, reconciliation, conflict) => { let recon_str = match reconciliation { Reconciliation::InSync => Cow::Borrowed("in sync"), @@ -439,59 +592,10 @@ impl LoggerImpl { } Log::Error(side, path, ref op, err) => { - let mut errs = err.to_string(); - - for e in err.iter().skip(1) { - errs.push_str(&format!("\ncaused by: {}", e)); - } - - if let Some(bt) = err.backtrace() { - errs.push_str(&format!("\n{:?}\n", bt)); - } - - match *op { - ErrorOperation::List => { - say!(path, side, "Failed to list directory: {}", errs) - } - - ErrorOperation::MarkClean => say!( - path, - side, - "Failed to mark directory clean: {}", - errs - ), - - ErrorOperation::Chdir(name) => say!( - (path, name), - side, - "Failed to enter directory: {}", - errs - ), + let name = get_error_basename(op); + let message = format_error_log(op, err); - ErrorOperation::Create(name) => { - say!((path, name), side, "Failed to create: {}", errs) - } - - ErrorOperation::Update(name) => { - say!((path, name), side, "Failed to update: {}", errs) - } - - ErrorOperation::Rename(name) => { - say!((path, name), side, "Failed to rename: {}", errs) - } - - ErrorOperation::Remove(name) => { - say!((path, name), side, "Failed to remove: {}", errs) - } - - ErrorOperation::Rmdir => { - say!(path, side, "Failed to remove: {}", errs) - } - - ErrorOperation::Access(name) => { - say!((path, name), side, "Failed to access: {}", errs) - } - } + say!((path, name), side, "{}", message); } } @@ -643,6 +747,7 @@ impl LoggerImpl { } match *what { + Log::SyncStarted | Log::SyncFinished => {} Log::Error(..) => {} Log::RecursiveDelete(..) => {} @@ -755,6 +860,159 @@ impl LoggerImpl { } } } + + fn write_json_status(&self, mut out: &mut BufWriter, what: &Log) { + let now = chrono::Utc::now(); + + let display_path = |dir: &OsStr, name: &OsStr| -> String { + format!("{}", PathDisplay(&self.client_root, (dir, name))) + }; + + let display_maybe_path = + |dir: &OsStr, name: Option<&OsStr>| -> String { + format!("{}", PathDisplay(&self.client_root, (dir, name))) + }; + + let display_dir_path = |dir: &OsStr| -> String { + format!("{}", PathDisplay(&self.client_root, dir)) + }; + + fn name_side>(side: S) -> &'static str { + match side.into() { + ReplicaSide::Client => "local", + ReplicaSide::Server => "remote", + ReplicaSide::Ancestor => "ancestor", + } + } + + fn convert_state_to_info(state: &FileData) -> LogJsonPathInfo { + match *state { + FileData::Directory(mode) => { + LogJsonPathInfo::Directory { mode } + } + + FileData::Regular(mode, size, modified_time, ..) => { + LogJsonPathInfo::File { + mode, + size, + modified_time: chrono::DateTime::::from_utc( + chrono::NaiveDateTime::from_timestamp( + modified_time, + 0, + ), + Utc, + ), + } + } + FileData::Symlink(ref target) => LogJsonPathInfo::Symlink { + target: target.to_string_lossy().into_owned(), + }, + FileData::Special => LogJsonPathInfo::Special, + } + } + + let contents = match *what { + Log::SyncStarted => LogJsonContents::SyncStarted, + Log::SyncFinished => LogJsonContents::SyncFinished, + Log::Inspect(.., reconciliation, conflict) => { + LogJsonContents::SyncConflict { + type_: match conflict { + Conflict::NoConflict => return, + Conflict::EditEdit(..) => "edit_edit", + Conflict::EditDelete(deleted_side) => { + match deleted_side { + ReconciliationSide::Client => { + "edit_remote_delete_local" + } + ReconciliationSide::Server => { + "edit_local_delete_remote" + } + } + } + }, + reconciliation: match reconciliation { + Reconciliation::InSync => return, + Reconciliation::Unsync => "unsynced", + Reconciliation::Irreconcilable => "irreconcilable", + Reconciliation::Use(side) => match side { + ReconciliationSide::Client => "use_local", + ReconciliationSide::Server => "use_remote", + }, + Reconciliation::Split(side, ..) => match side { + ReconciliationSide::Client => "split_local", + ReconciliationSide::Server => "split_remote", + }, + }, + } + } + + Log::Create(..) => LogJsonContents::SyncCreate, + Log::Update(.., old, new) => LogJsonContents::SyncUpdate { + old_info: convert_state_to_info(old), + new_info: convert_state_to_info(new), + }, + Log::Rename(_, dir, _, new) => LogJsonContents::SyncRename { + new_name: display_path(dir, new), + }, + Log::Remove(..) => LogJsonContents::SyncRemove, + Log::RecursiveDelete(..) => LogJsonContents::SyncRemoveRecursively, + Log::Rmdir(..) => LogJsonContents::SyncRemoveDirectory, + Log::Error(.., ref op, err) => { + let error_message = format_error_log(op, err); + + LogJsonContents::SyncError { error_message } + } + }; + + let location = match *what { + Log::Inspect(dir, name, ..) => Some(LogJsonLocation { + side: None, + path: display_path(dir, name), + info: None, + }), + Log::Create(side, dir, name, state) + | Log::Remove(side, dir, name, state) => Some(LogJsonLocation { + side: Some(name_side(side)), + path: display_path(dir, name), + info: Some(convert_state_to_info(state)), + }), + Log::Update(side, dir, name, ..) + | Log::Rename(side, dir, name, ..) => Some(LogJsonLocation { + side: Some(name_side(side)), + path: display_path(dir, name), + info: None, + }), + Log::Rmdir(side, dir) | Log::RecursiveDelete(side, dir) => { + Some(LogJsonLocation { + side: Some(name_side(side)), + path: display_dir_path(dir), + info: None, + }) + } + Log::Error(side, dir, ref op, ..) => { + let name = get_error_basename(op); + + Some(LogJsonLocation { + side: Some(name_side(side)), + path: display_maybe_path(dir, name), + info: None, + }) + } + _ => None, + }; + + serde_json::to_writer( + &mut out, + &LogJson { + status_version: 1, + time: now, + contents, + location, + }, + ) + .expect("Failed to write to JSON status output"); + write!(&mut out, "\n").expect("Failed to write to JSON status output"); + } } pub fn run( @@ -764,6 +1022,7 @@ pub fn run( quietness: i32, itemise: bool, itemise_unchanged: bool, + json_status_out: Option, colour: &str, spin: &str, include_ancestors: bool, @@ -882,6 +1141,7 @@ pub fn run( } else { None }, + json_status_out: json_status_out.map(|j| Mutex::new(BufWriter::new(j))), }; interrupt::install_signal_handler(); @@ -1012,6 +1272,8 @@ fn run_sync< }}; } + context.log.log(INFO, &Log::SyncStarted); + let last_config_path = config.private_root.join("last-config.dat"); let min_prepare_type = match fs::File::open(&last_config_path).and_then(|mut file| { @@ -1115,6 +1377,8 @@ fn run_sync< } } + context.log.log(INFO, &Log::SyncFinished); + Ok(()) } diff --git a/src/log.rs b/src/log.rs index 2fc5da7..46a6066 100644 --- a/src/log.rs +++ b/src/log.rs @@ -65,6 +65,8 @@ pub enum ErrorOperation<'a> { #[derive(Clone, Copy, Debug)] pub enum Log<'a> { + SyncStarted, + SyncFinished, Inspect(&'a OsStr, &'a OsStr, Reconciliation, Conflict), Create(ReplicaSide, &'a OsStr, &'a OsStr, &'a FileData), Update( diff --git a/src/main.rs b/src/main.rs index 51789ad..ffc0171 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,6 +52,8 @@ mod replica; mod rules; mod server; +use std::fs::File; +use std::os::unix::io::{FromRawFd, RawFd}; use std::path::PathBuf; use clap::AppSettings; @@ -291,6 +293,11 @@ struct SyncSubcommand { /// `auto`, implies `--strategy=clean`. #[structopt(long)] override_mode: Option, + + /// Output line-delimited status messages to the given file descriptor. The output is + /// unaffected by the `-v` or `-q` flags. + #[structopt(long)] + json_status_fd: Option, } /// Initialise the key store. @@ -1097,9 +1104,13 @@ fn main_impl() -> Result<()> { return Err("Thread count must be at least 1".into()); } + let json_status_out = + sc.json_status_fd.map(|fd| unsafe { File::from_raw_fd(fd) }); + fn do_run( sc: &SyncSubcommand, config: &cli::config::Config, + json_status_out: Option, num_threads: u32, key_chain: &mut Option>, ) -> errors::Result<()> { @@ -1113,6 +1124,7 @@ fn main_impl() -> Result<()> { sc.verbosity.quiet, sc.itemise, sc.itemise_unchanged, + json_status_out, &sc.colour, &sc.spin, sc.include_ancestors, @@ -1130,7 +1142,13 @@ fn main_impl() -> Result<()> { loop { use std::io::{stderr, Write}; - match do_run(&sc, &config, num_threads, &mut key_chain) { + match do_run( + &sc, + &config, + json_status_out.as_ref().map(|j| j.try_clone().expect("failed to clone FD")), + num_threads, + &mut key_chain, + ) { Ok(()) => return Ok(()), Err(e) => { if let Some(seconds) = sc.reconnect { diff --git a/test/compare-current-output.sh b/test/compare-current-output.sh new file mode 100755 index 0000000..caa9d91 --- /dev/null +++ b/test/compare-current-output.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "$1" ]]; then + echo "Usage: $0 OLDFILE" >&2 + exit 1 +fi + +OUTPUT_DIR="$(mktemp --directory --tmpdir ensync-compare.XXXXXXXXX)" +trap "rm -rf '${OUTPUT_DIR}'" exit + +"$(dirname $0)"/generate-json-output.sh > "${OUTPUT_DIR}/current-output.json" + +diff ${DIFFOPTS:--u} "$1" "${OUTPUT_DIR}/current-output.json" diff --git a/test/generate-json-output.sh b/test/generate-json-output.sh new file mode 100755 index 0000000..da5dc35 --- /dev/null +++ b/test/generate-json-output.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +set -euo pipefail + +for prog in cargo jq openssl; do + if ! [[ -x "$(which $prog)" ]]; then + echo "Dependency cannot be found: $prog" >&2 + exit 1 + fi +done + +cargo build --quiet --release +ENSYNC="$(cargo metadata --format-version 1 | jq -r '.target_directory')/release/ensync" + +TEST_DIR="$(mktemp --directory --tmpdir ensync-test.XXXXXXXXX)" +cd "${TEST_DIR}" +trap "chmod -R 777 '${TEST_DIR}' && rm -rf '${TEST_DIR}'" exit + +LOCAL="${PWD}/local" +CONFIG="${PWD}/config" +REMOTE="${PWD}/remote" +TEST_FILES="${PWD}/test-files" + +mkdir "${LOCAL}" "${CONFIG}" "${TEST_FILES}" + +cat > "${CONFIG}/config.toml" < "${out}" + echo "${out}" +} + +function remove_times_and_sort() { + local sort_filter='sort_by([.path, .side, .type])' + + if [[ -n "${JSON_NO_SORT:-}" ]]; then + sort_filter='.' + fi + + jq -sS ' + def blur_if_exists(path): if path? then path = "..." else . end; + map( + blur_if_exists(.time) | + blur_if_exists(.update_old_info.modified_time) | + blur_if_exists(.update_new_info.modified_time) | + blur_if_exists(.info.modified_time) + ) | + '"${sort_filter}"' | + .[] + ' +} + +${ENSYNC} key init "${CONFIG}" +${ENSYNC} mkdir "${CONFIG}" /root +${ENSYNC} mkdir "${CONFIG}" remote-new-dir +${ENSYNC} mkdir "${CONFIG}" remote-dir-to-remove +${ENSYNC} put "${CONFIG}" "$(randfile 1)" remote-file-to-update +${ENSYNC} put "${CONFIG}" "$(randfile 2)" remote-file-to-remove +${ENSYNC} put "${CONFIG}" "$(randfile 3)" edit-edit-file +${ENSYNC} mkdir "${CONFIG}" dir-for-read-errors +${ENSYNC} mkdir "${CONFIG}" dir-for-write-errors +${ENSYNC} put "${CONFIG}" "$(randfile 10)" dir-for-write-errors/file-to-attempt-removal +mkdir "${LOCAL}/local-new-dir" +cp "$(randfile 4)" "${LOCAL}/local-file-to-update" +cp "$(randfile 5)" "${LOCAL}/local-file-to-remove" +cp "$(randfile 6)" "${LOCAL}/edit-edit-file" +ln -s target "${LOCAL}/local-symlink" +mkdir "${LOCAL}/local-dir-to-remove" +mkdir "${LOCAL}/dir-to-remove-recursively" +cp "$(randfile 9)" "${LOCAL}/dir-to-remove-recursively/testfile" + +JSON_STATUS_OUT="${PWD}/json-status-out" + +exec 3>"${JSON_STATUS_OUT}-1" +${ENSYNC} sync --json-status-fd=3 "${CONFIG}" >/dev/null 2>&1 + +sleep 1.5 + +cp "$(randfile 7)" "${LOCAL}/local-file-to-update" +rm "${LOCAL}/local-file-to-remove" +rmdir "${LOCAL}/local-dir-to-remove" +rm -r "${LOCAL}/dir-to-remove-recursively" +chmod 000 "${LOCAL}/dir-for-read-errors" +chmod 500 "${LOCAL}/dir-for-write-errors" +${ENSYNC} put --force "${CONFIG}" "$(randfile 8)" remote-file-to-update +${ENSYNC} rm "${CONFIG}" remote-file-to-remove +${ENSYNC} rmdir "${CONFIG}" remote-dir-to-remove +${ENSYNC} rm -r "${CONFIG}" dir-for-write-errors + +exec 3>"${JSON_STATUS_OUT}-2" +${ENSYNC} sync --json-status-fd=3 "${CONFIG}" #>/dev/null 2>&1 + +( + remove_times_and_sort < "${JSON_STATUS_OUT}-1" + echo + remove_times_and_sort < "${JSON_STATUS_OUT}-2" +) diff --git a/test/previous-outputs/1.0.1.json b/test/previous-outputs/1.0.1.json new file mode 100644 index 0000000..f8b5b48 --- /dev/null +++ b/test/previous-outputs/1.0.1.json @@ -0,0 +1,468 @@ +{ + "status_version": 1, + "time": "...", + "type": "sync_finished" +} +{ + "status_version": 1, + "time": "...", + "type": "sync_started" +} +{ + "info": { + "mode": 448, + "type": "directory" + }, + "path": "dir-for-read-errors", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "info": { + "mode": 448, + "type": "directory" + }, + "path": "dir-for-write-errors", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "info": { + "mode": 33152, + "modified_time": "...", + "size": 10, + "type": "file" + }, + "path": "dir-for-write-errors/file-to-attempt-removal", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "info": { + "mode": 493, + "type": "directory" + }, + "path": "dir-to-remove-recursively", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "info": { + "mode": 384, + "modified_time": "...", + "size": 9, + "type": "file" + }, + "path": "dir-to-remove-recursively/testfile", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "conflict_reconciliation": "split_remote", + "conflict_type": "edit_edit", + "path": "edit-edit-file", + "status_version": 1, + "time": "...", + "type": "sync_conflict" +} +{ + "info": { + "mode": 384, + "modified_time": "...", + "size": 6, + "type": "file" + }, + "path": "edit-edit-file", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "path": "edit-edit-file", + "rename_new_name": "edit-edit-file~1", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_rename" +} +{ + "info": { + "mode": 33152, + "modified_time": "...", + "size": 3, + "type": "file" + }, + "path": "edit-edit-file~1", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "info": { + "mode": 493, + "type": "directory" + }, + "path": "local-dir-to-remove", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "info": { + "mode": 384, + "modified_time": "...", + "size": 5, + "type": "file" + }, + "path": "local-file-to-remove", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "info": { + "mode": 384, + "modified_time": "...", + "size": 4, + "type": "file" + }, + "path": "local-file-to-update", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "info": { + "mode": 493, + "type": "directory" + }, + "path": "local-new-dir", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "info": { + "target": "target", + "type": "symlink" + }, + "path": "local-symlink", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "info": { + "mode": 448, + "type": "directory" + }, + "path": "remote-dir-to-remove", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "info": { + "mode": 33152, + "modified_time": "...", + "size": 2, + "type": "file" + }, + "path": "remote-file-to-remove", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "info": { + "mode": 33152, + "modified_time": "...", + "size": 1, + "type": "file" + }, + "path": "remote-file-to-update", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_create" +} +{ + "info": { + "mode": 448, + "type": "directory" + }, + "path": "remote-new-dir", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_create" +} + +{ + "status_version": 1, + "time": "...", + "type": "sync_finished" +} +{ + "status_version": 1, + "time": "...", + "type": "sync_started" +} +{ + "error_message": "Failed to list directory: Error listing directory '/tmp/ensync-test.N6FRoQ0bQ/local/dir-for-read-errors'\ncaused by: Permission denied (os error 13)", + "path": "dir-for-read-errors", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_error" +} +{ + "path": "dir-for-read-errors", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_update", + "update_new_info": { + "mode": 0, + "type": "directory" + }, + "update_old_info": { + "mode": 448, + "type": "directory" + } +} +{ + "path": "dir-for-write-errors", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_remove_recursively" +} +{ + "error_message": "Failed to remove: Permission denied (os error 13)", + "path": "dir-for-write-errors/file-to-attempt-removal", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_error" +} +{ + "info": { + "mode": 384, + "modified_time": "...", + "size": 10, + "type": "file" + }, + "path": "dir-for-write-errors/file-to-attempt-removal", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_remove" +} +{ + "path": "dir-to-remove-recursively", + "side": "ancestor", + "status_version": 1, + "time": "...", + "type": "sync_remove_directory" +} +{ + "path": "dir-to-remove-recursively", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_remove_directory" +} +{ + "path": "dir-to-remove-recursively", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_remove_directory" +} +{ + "path": "dir-to-remove-recursively", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_remove_recursively" +} +{ + "info": { + "mode": 384, + "modified_time": "...", + "size": 9, + "type": "file" + }, + "path": "dir-to-remove-recursively/testfile", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_remove" +} +{ + "path": "edit-edit-file~1", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_update", + "update_new_info": { + "mode": 384, + "modified_time": "...", + "size": 3, + "type": "file" + }, + "update_old_info": { + "mode": 33152, + "modified_time": "...", + "size": 3, + "type": "file" + } +} +{ + "path": "local-dir-to-remove", + "side": "ancestor", + "status_version": 1, + "time": "...", + "type": "sync_remove_directory" +} +{ + "path": "local-dir-to-remove", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_remove_directory" +} +{ + "path": "local-dir-to-remove", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_remove_directory" +} +{ + "path": "local-dir-to-remove", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_remove_recursively" +} +{ + "info": { + "mode": 384, + "modified_time": "...", + "size": 5, + "type": "file" + }, + "path": "local-file-to-remove", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_remove" +} +{ + "path": "local-file-to-update", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_update", + "update_new_info": { + "mode": 384, + "modified_time": "...", + "size": 7, + "type": "file" + }, + "update_old_info": { + "mode": 384, + "modified_time": "...", + "size": 4, + "type": "file" + } +} +{ + "path": "remote-dir-to-remove", + "side": "ancestor", + "status_version": 1, + "time": "...", + "type": "sync_remove_directory" +} +{ + "path": "remote-dir-to-remove", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_remove_directory" +} +{ + "path": "remote-dir-to-remove", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_remove_recursively" +} +{ + "path": "remote-dir-to-remove", + "side": "remote", + "status_version": 1, + "time": "...", + "type": "sync_remove_directory" +} +{ + "info": { + "mode": 384, + "modified_time": "...", + "size": 2, + "type": "file" + }, + "path": "remote-file-to-remove", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_remove" +} +{ + "conflict_reconciliation": "use_remote", + "conflict_type": "edit_edit", + "path": "remote-file-to-update", + "status_version": 1, + "time": "...", + "type": "sync_conflict" +} +{ + "path": "remote-file-to-update", + "side": "local", + "status_version": 1, + "time": "...", + "type": "sync_update", + "update_new_info": { + "mode": 33152, + "modified_time": "...", + "size": 8, + "type": "file" + }, + "update_old_info": { + "mode": 384, + "modified_time": "...", + "size": 1, + "type": "file" + } +}