From 3ab8291faa359dacb3d499c7a213bef7b60f4641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= <erik@bjareho.lt> Date: Mon, 30 Oct 2023 11:19:18 +0100 Subject: [PATCH] feat(sync): moved logic in sync-test scripts into rust, enabling sync with a single command (#430) * feat(sync): moved logic in `test-sync-pull/push.sh` scripts into rust code, enabling easy sync with a single command * fix: fixes to sync dirs * fix: moved --sync-dir arg into sync-advanced subcommand, fixed warnings * fix: pass around client instead of port/testing * fix: fixed log dir for aw-sync, misc sync fixes and refactor --- Cargo.lock | 15 ++++- aw-server/src/dirs.rs | 8 +-- aw-server/src/logging.rs | 22 +++---- aw-server/src/main.rs | 3 +- aw-sync/Cargo.toml | 4 ++ aw-sync/src/dirs.rs | 27 ++++++++ aw-sync/src/lib.rs | 7 ++ aw-sync/src/main.rs | 116 +++++++++++++++++++++------------ aw-sync/src/sync.rs | 57 +++-------------- aw-sync/src/sync_wrapper.rs | 102 +++++++++++++++++++++++++++++ aw-sync/src/util.rs | 124 ++++++++++++++++++++++++++++++++++++ aw-sync/test-sync-pull.sh | 5 +- aw-sync/test-sync-push.sh | 5 +- 13 files changed, 385 insertions(+), 110 deletions(-) create mode 100644 aw-sync/src/dirs.rs create mode 100644 aw-sync/src/sync_wrapper.rs create mode 100644 aw-sync/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 385170d7..25e17223 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -290,16 +290,20 @@ dependencies = [ name = "aw-sync" version = "0.1.0" dependencies = [ + "appdirs", "aw-client-rust", "aw-datastore", "aw-models", "aw-server", "chrono", "clap", + "dirs 3.0.2", + "gethostname", "log", "reqwest", "serde", "serde_json", + "toml", ] [[package]] @@ -752,6 +756,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs" version = "4.0.0" @@ -2338,7 +2351,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" dependencies = [ - "dirs", + "dirs 4.0.0", ] [[package]] diff --git a/aw-server/src/dirs.rs b/aw-server/src/dirs.rs index b94456e9..755d25b7 100644 --- a/aw-server/src/dirs.rs +++ b/aw-server/src/dirs.rs @@ -53,15 +53,15 @@ pub fn get_cache_dir() -> Result<PathBuf, ()> { } #[cfg(not(target_os = "android"))] -pub fn get_log_dir() -> Result<PathBuf, ()> { +pub fn get_log_dir(module: &str) -> Result<PathBuf, ()> { let mut dir = appdirs::user_log_dir(Some("activitywatch"), None)?; - dir.push("aw-server-rust"); + dir.push(module); fs::create_dir_all(dir.clone()).expect("Unable to create log dir"); Ok(dir) } #[cfg(target_os = "android")] -pub fn get_log_dir() -> Result<PathBuf, ()> { +pub fn get_log_dir(module: &str) -> Result<PathBuf, ()> { panic!("not implemented on Android"); } @@ -87,7 +87,7 @@ fn test_get_dirs() { set_android_data_dir("/test"); get_cache_dir().unwrap(); - get_log_dir().unwrap(); + get_log_dir("aw-server-rust").unwrap(); db_path(true).unwrap(); db_path(false).unwrap(); } diff --git a/aw-server/src/logging.rs b/aw-server/src/logging.rs index 6187d2db..8ec4568f 100644 --- a/aw-server/src/logging.rs +++ b/aw-server/src/logging.rs @@ -5,19 +5,17 @@ use fern::colors::{Color, ColoredLevelConfig}; use crate::dirs; -pub fn setup_logger(testing: bool, verbose: bool) -> Result<(), fern::InitError> { +pub fn setup_logger(module: &str, testing: bool, verbose: bool) -> Result<(), fern::InitError> { let mut logfile_path: PathBuf = - dirs::get_log_dir().expect("Unable to get log dir to store logs in"); + dirs::get_log_dir(module).expect("Unable to get log dir to store logs in"); fs::create_dir_all(logfile_path.clone()).expect("Unable to create folder for logs"); - logfile_path.push( - chrono::Local::now() - .format(if !testing { - "aw-server_%Y-%m-%dT%H-%M-%S%z.log" - } else { - "aw-server-testing_%Y-%m-%dT%H-%M-%S%z.log" - }) - .to_string(), - ); + let filename = if !testing { + format!("{}_%Y-%m-%dT%H-%M-%S%z.log", module) + } else { + format!("{}-testing_%Y-%m-%dT%H-%M-%S%z.log", module) + }; + + logfile_path.push(chrono::Local::now().format(&filename).to_string()); log_panics::init(); @@ -93,6 +91,6 @@ mod tests { #[ignore] #[test] fn test_setup_logger() { - setup_logger(true, true).unwrap(); + setup_logger("aw-server-rust", true, true).unwrap(); } } diff --git a/aw-server/src/main.rs b/aw-server/src/main.rs index 73185a64..933366e6 100644 --- a/aw-server/src/main.rs +++ b/aw-server/src/main.rs @@ -70,7 +70,8 @@ async fn main() -> Result<(), rocket::Error> { testing = true; } - logging::setup_logger(testing, opts.verbose).expect("Failed to setup logging"); + logging::setup_logger("aw-server-rust", testing, opts.verbose) + .expect("Failed to setup logging"); if testing { info!("Running server in Testing mode"); diff --git a/aw-sync/Cargo.toml b/aw-sync/Cargo.toml index 2380a005..6ff9dd69 100644 --- a/aw-sync/Cargo.toml +++ b/aw-sync/Cargo.toml @@ -14,12 +14,16 @@ path = "src/main.rs" [dependencies] log = "0.4" +toml = "0.7" chrono = { version = "0.4", features = ["serde"] } serde = "1.0" serde_json = "1.0" reqwest = { version = "0.11", features = ["json", "blocking"] } clap = { version = "4.1", features = ["derive"] } +appdirs = "0.2.0" +dirs = "3.0.2" aw-server = { path = "../aw-server" } aw-models = { path = "../aw-models" } aw-datastore = { path = "../aw-datastore" } aw-client-rust = { path = "../aw-client-rust" } +gethostname = "0.4.3" diff --git a/aw-sync/src/dirs.rs b/aw-sync/src/dirs.rs new file mode 100644 index 00000000..5e3faffd --- /dev/null +++ b/aw-sync/src/dirs.rs @@ -0,0 +1,27 @@ +use dirs::home_dir; +use std::fs; +use std::path::PathBuf; + +// TODO: This could be refactored to share logic with aw-server/src/dirs.rs +// TODO: add proper config support +#[allow(dead_code)] +pub fn get_config_dir() -> Result<PathBuf, ()> { + let mut dir = appdirs::user_config_dir(Some("activitywatch"), None, false)?; + dir.push("aw-sync"); + fs::create_dir_all(dir.clone()).expect("Unable to create config dir"); + Ok(dir) +} + +pub fn get_server_config_path(testing: bool) -> Result<PathBuf, ()> { + let dir = aw_server::dirs::get_config_dir()?; + Ok(dir.join(if testing { + "config-testing.toml" + } else { + "config.toml" + })) +} + +pub fn get_sync_dir() -> Result<PathBuf, ()> { + // TODO: make this configurable + home_dir().map(|p| p.join("ActivityWatchSync")).ok_or(()) +} diff --git a/aw-sync/src/lib.rs b/aw-sync/src/lib.rs index b107c9bd..8ba7bbd4 100644 --- a/aw-sync/src/lib.rs +++ b/aw-sync/src/lib.rs @@ -10,5 +10,12 @@ pub use sync::sync_datastores; pub use sync::sync_run; pub use sync::SyncSpec; +mod sync_wrapper; +pub use sync_wrapper::push; +pub use sync_wrapper::{pull, pull_all}; + mod accessmethod; pub use accessmethod::AccessMethod; + +mod dirs; +mod util; diff --git a/aw-sync/src/main.rs b/aw-sync/src/main.rs index 518e705d..83dacef5 100644 --- a/aw-sync/src/main.rs +++ b/aw-sync/src/main.rs @@ -23,9 +23,10 @@ use clap::{Parser, Subcommand}; use aw_client_rust::blocking::AwClient; mod accessmethod; +mod dirs; mod sync; - -const DEFAULT_PORT: &str = "5600"; +mod sync_wrapper; +mod util; #[derive(Parser)] #[clap(version = "0.1", author = "Erik Bjäreholt")] @@ -38,8 +39,8 @@ struct Opts { host: String, /// Port of instance to connect to. - #[clap(long, default_value = DEFAULT_PORT)] - port: String, + #[clap(long)] + port: Option<String>, /// Convenience option for using the default testing host and port. #[clap(long)] @@ -48,42 +49,53 @@ struct Opts { /// Enable debug logging. #[clap(long)] verbose: bool, - - /// Full path to sync directory. - /// If not specified, exit. - #[clap(long)] - sync_dir: String, - - /// Full path to sync db file - /// Useful for syncing buckets from a specific db file in the sync directory. - /// Must be a valid absolute path to a file in the sync directory. - #[clap(long)] - sync_db: Option<String>, } #[derive(Subcommand)] enum Commands { - /// Sync subcommand. + /// Sync subcommand (basic) + /// + /// Pulls remote buckets then pushes local buckets. + Sync { + /// Host(s) to pull from, comma separated. Will pull from all hosts if not specified. + #[clap(long)] + host: Option<String>, + }, + + /// Sync subcommand (advanced) /// /// Pulls remote buckets then pushes local buckets. /// First pulls remote buckets in the sync directory to the local aw-server. /// Then pushes local buckets from the aw-server to the local sync directory. #[clap(arg_required_else_help = true)] - Sync { + SyncAdvanced { /// Date to start syncing from. /// If not specified, start from beginning. /// NOTE: might be unstable, as count cannot be used to verify integrity of sync. /// Format: YYYY-MM-DD #[clap(long)] start_date: Option<String>, + /// Specify buckets to sync using a comma-separated list. /// If not specified, all buckets will be synced. #[clap(long)] buckets: Option<String>, + /// Mode to sync in. Can be "push", "pull", or "both". /// Defaults to "both". #[clap(long, default_value = "both")] mode: String, + + /// Full path to sync directory. + /// If not specified, exit. + #[clap(long)] + sync_dir: String, + + /// Full path to sync db file + /// Useful for syncing buckets from a specific db file in the sync directory. + /// Must be a valid absolute path to a file in the sync directory. + #[clap(long)] + sync_db: Option<String>, }, /// List buckets and their sync status. List {}, @@ -95,35 +107,59 @@ fn main() -> Result<(), Box<dyn Error>> { info!("Started aw-sync..."); - aw_server::logging::setup_logger(true, verbose).expect("Failed to setup logging"); - - let sync_directory = if opts.sync_dir.is_empty() { - println!("No sync directory specified, exiting..."); - std::process::exit(1); - } else { - Path::new(&opts.sync_dir) - }; - info!("Using sync dir: {}", sync_directory.display()); - - if let Some(sync_db) = &opts.sync_db { - info!("Using sync db: {}", sync_db); - } + aw_server::logging::setup_logger("aw-sync", opts.testing, verbose) + .expect("Failed to setup logging"); - let port = if opts.testing && opts.port == DEFAULT_PORT { - "5666" - } else { - &opts.port - }; + let port = opts + .port + .or_else(|| Some(crate::util::get_server_port(opts.testing).ok()?.to_string())) + .unwrap(); - let client = AwClient::new(opts.host.as_str(), port, "aw-sync"); + let client = AwClient::new(opts.host.as_str(), port.as_str(), "aw-sync"); match &opts.command { + // Perform basic sync + Commands::Sync { host } => { + // Pull + match host { + Some(host) => { + let hosts: Vec<&str> = host.split(',').collect(); + for host in hosts.iter() { + info!("Pulling from host: {}", host); + sync_wrapper::pull(host, &client)?; + } + } + None => { + info!("Pulling from all hosts"); + sync_wrapper::pull_all(&client)?; + } + } + + // Push + info!("Pushing local data"); + sync_wrapper::push(&client)?; + Ok(()) + } // Perform two-way sync - Commands::Sync { + Commands::SyncAdvanced { start_date, buckets, mode, + sync_dir, + sync_db, } => { + let sync_directory = if sync_dir.is_empty() { + error!("No sync directory specified, exiting..."); + std::process::exit(1); + } else { + Path::new(&sync_dir) + }; + info!("Using sync dir: {}", sync_directory.display()); + + if let Some(sync_db) = &sync_db { + info!("Using sync db: {}", sync_db); + } + let start: Option<DateTime<Utc>> = start_date.as_ref().map(|date| { println!("{}", date.clone()); chrono::NaiveDate::parse_from_str(&date.clone(), "%Y-%m-%d") @@ -140,7 +176,7 @@ fn main() -> Result<(), Box<dyn Error>> { .as_ref() .map(|b| b.split(',').map(|s| s.to_string()).collect()); - let sync_db: Option<PathBuf> = opts.sync_db.as_ref().map(|db| { + let sync_db: Option<PathBuf> = sync_db.as_ref().map(|db| { let db_path = Path::new(db); if !db_path.is_absolute() { panic!("Sync db path must be absolute"); @@ -165,11 +201,11 @@ fn main() -> Result<(), Box<dyn Error>> { _ => panic!("Invalid mode"), }; - sync::sync_run(client, &sync_spec, mode_enum) + sync::sync_run(&client, &sync_spec, mode_enum) } // List all buckets - Commands::List {} => sync::list_buckets(&client, sync_directory), + Commands::List {} => sync::list_buckets(&client), }?; // Needed to give the datastores some time to commit before program is shut down. diff --git a/aw-sync/src/sync.rs b/aw-sync/src/sync.rs index 8c4cfbb6..c842f5a6 100644 --- a/aw-sync/src/sync.rs +++ b/aw-sync/src/sync.rs @@ -9,7 +9,6 @@ extern crate chrono; extern crate reqwest; extern crate serde_json; -use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; @@ -28,6 +27,7 @@ pub enum SyncMode { Both, } +#[derive(Debug)] pub struct SyncSpec { /// Path of sync folder pub path: PathBuf, @@ -54,7 +54,7 @@ impl Default for SyncSpec { } /// Performs a single sync pass -pub fn sync_run(client: AwClient, sync_spec: &SyncSpec, mode: SyncMode) -> Result<(), String> { +pub fn sync_run(client: &AwClient, sync_spec: &SyncSpec, mode: SyncMode) -> Result<(), String> { let info = client.get_info().map_err(|e| e.to_string())?; // FIXME: Here it is assumed that the device_id for the local server is the one used by @@ -64,7 +64,7 @@ pub fn sync_run(client: AwClient, sync_spec: &SyncSpec, mode: SyncMode) -> Resul // FIXME: Bad device_id assumption? let ds_localremote = setup_local_remote(sync_spec.path.as_path(), device_id)?; - let remote_dbfiles = find_remotes_nonlocal( + let remote_dbfiles = crate::util::find_remotes_nonlocal( sync_spec.path.as_path(), device_id, sync_spec.path_db.as_ref(), @@ -99,14 +99,14 @@ pub fn sync_run(client: AwClient, sync_spec: &SyncSpec, mode: SyncMode) -> Resul if mode == SyncMode::Pull || mode == SyncMode::Both { info!("Pulling..."); for ds_from in &ds_remotes { - sync_datastores(ds_from, &client, false, None, sync_spec); + sync_datastores(ds_from, client, false, None, sync_spec); } } // Push local server buckets to sync folder if mode == SyncMode::Push || mode == SyncMode::Both { info!("Pushing..."); - sync_datastores(&client, &ds_localremote, true, Some(device_id), sync_spec); + sync_datastores(client, &ds_localremote, true, Some(device_id), sync_spec); } // Close open database connections @@ -128,14 +128,16 @@ pub fn sync_run(client: AwClient, sync_spec: &SyncSpec, mode: SyncMode) -> Resul } #[allow(dead_code)] -pub fn list_buckets(client: &AwClient, sync_directory: &Path) -> Result<(), String> { +pub fn list_buckets(client: &AwClient) -> Result<(), String> { + let sync_directory = crate::dirs::get_sync_dir().map_err(|_| "Could not get sync dir")?; + let sync_directory = sync_directory.as_path(); let info = client.get_info().map_err(|e| e.to_string())?; // FIXME: Incorrect device_id assumption? let device_id = info.device_id.as_str(); let ds_localremote = setup_local_remote(sync_directory, device_id)?; - let remote_dbfiles = find_remotes_nonlocal(sync_directory, device_id, None); + let remote_dbfiles = crate::util::find_remotes_nonlocal(sync_directory, device_id, None); info!("Found remotes: {:?}", remote_dbfiles); // TODO: Check for compatible remote db version before opening @@ -172,47 +174,6 @@ fn setup_local_remote(path: &Path, device_id: &str) -> Result<Datastore, String> Ok(ds_localremote) } -/// Returns a list of all remote dbs -fn find_remotes(sync_directory: &Path) -> std::io::Result<Vec<PathBuf>> { - let dbs = fs::read_dir(sync_directory)? - .map(|res| res.ok().unwrap().path()) - .filter(|p| p.is_dir()) - .flat_map(|d| fs::read_dir(d).unwrap()) - .map(|res| res.ok().unwrap().path()) - .filter(|path| path.extension().unwrap_or_else(|| OsStr::new("")) == "db") - .collect(); - Ok(dbs) -} - -/// Returns a list of all remotes, excluding local ones -fn find_remotes_nonlocal( - sync_directory: &Path, - device_id: &str, - sync_db: Option<&PathBuf>, -) -> Vec<PathBuf> { - let remotes_all = find_remotes(sync_directory).unwrap(); - remotes_all - .into_iter() - // Filter out own remote - .filter(|path| { - !(path - .clone() - .into_os_string() - .into_string() - .unwrap() - .contains(device_id)) - }) - // If sync_db is Some, return only remotes in that path - .filter(|path| { - if let Some(sync_db) = sync_db { - path.starts_with(sync_db) - } else { - true - } - }) - .collect() -} - pub fn create_datastore(path: &Path) -> Datastore { let pathstr = path.as_os_str().to_str().unwrap(); Datastore::new(pathstr.to_string(), false) diff --git a/aw-sync/src/sync_wrapper.rs b/aw-sync/src/sync_wrapper.rs new file mode 100644 index 00000000..11daae50 --- /dev/null +++ b/aw-sync/src/sync_wrapper.rs @@ -0,0 +1,102 @@ +use std::boxed::Box; +use std::error::Error; +use std::fs; +use std::net::TcpStream; + +use crate::sync::{sync_run, SyncMode, SyncSpec}; +use aw_client_rust::blocking::AwClient; + +pub fn pull_all(client: &AwClient) -> Result<(), Box<dyn Error>> { + let hostnames = crate::util::get_remotes()?; + for host in hostnames { + pull(&host, client)? + } + Ok(()) +} + +pub fn pull(host: &str, client: &AwClient) -> Result<(), Box<dyn Error>> { + // Check if server is running + let parts: Vec<&str> = client.baseurl.split("://").collect(); + let host_parts: Vec<&str> = parts[1].split(':').collect(); + let addr = host_parts[0]; + let port = host_parts[1].parse::<u16>().unwrap(); + + if TcpStream::connect((addr, port)).is_err() { + return Err(format!("Local server {} not running", &client.baseurl).into()); + } + + // Path to the sync folder + // Sync folder is structured ./{hostname}/{device_id}/test.db + let sync_root_dir = crate::dirs::get_sync_dir().map_err(|_| "Could not get sync dir")?; + let sync_dir = sync_root_dir.join(host); + let dbs = fs::read_dir(&sync_dir)? + .filter_map(Result::ok) + .filter(|entry| entry.path().is_dir()) + .map(|entry| fs::read_dir(entry.path())) + .filter_map(Result::ok) + .flatten() + .filter_map(Result::ok) + .filter(|entry| { + entry.path().is_file() + && entry.path().extension().and_then(|os_str| os_str.to_str()) == Some("db") + }) + .collect::<Vec<_>>(); + + // filter out dbs that are smaller than 50kB (workaround for trying to sync empty database + // files that are spuriously created somewhere) + let mut dbs = dbs + .into_iter() + .filter(|entry| entry.metadata().map(|m| m.len() > 50_000).unwrap_or(false)) + .collect::<Vec<_>>(); + + // if more than one db, warn and use the largest one + if dbs.len() > 1 { + warn!( + "More than one db found in sync folder for host, choosing largest db {:?}", + dbs + ); + dbs = vec![dbs + .into_iter() + .max_by_key(|entry| entry.metadata().map(|m| m.len()).unwrap_or(0)) + .unwrap()]; + } + // if no db, error + if dbs.is_empty() { + return Err(format!("No db found in sync folder {:?}", sync_dir).into()); + } + + for db in dbs { + let sync_spec = SyncSpec { + path: sync_dir.clone(), + path_db: Some(db.path().clone()), + buckets: Some(vec![ + format!("aw-watcher-window_{}", host), + format!("aw-watcher-afk_{}", host), + ]), + start: None, + }; + sync_run(client, &sync_spec, SyncMode::Pull)?; + } + + Ok(()) +} + +pub fn push(client: &AwClient) -> Result<(), Box<dyn Error>> { + let hostname = crate::util::get_hostname()?; + let sync_dir = crate::dirs::get_sync_dir() + .map_err(|_| "Could not get sync dir")? + .join(&hostname); + + let sync_spec = SyncSpec { + path: sync_dir, + path_db: None, + buckets: Some(vec![ + format!("aw-watcher-window_{}", hostname), + format!("aw-watcher-afk_{}", hostname), + ]), + start: None, + }; + sync_run(client, &sync_spec, SyncMode::Push)?; + + Ok(()) +} diff --git a/aw-sync/src/util.rs b/aw-sync/src/util.rs new file mode 100644 index 00000000..f2a38f0e --- /dev/null +++ b/aw-sync/src/util.rs @@ -0,0 +1,124 @@ +use std::boxed::Box; +use std::error::Error; +use std::ffi::OsStr; +use std::fs; +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; + +pub fn get_hostname() -> Result<String, Box<dyn Error>> { + let hostname = gethostname::gethostname() + .into_string() + .map_err(|_| "Failed to convert hostname to string")?; + Ok(hostname) +} + +/// Returns the port of the local aw-server instance +pub fn get_server_port(testing: bool) -> Result<u16, Box<dyn Error>> { + // TODO: get aw-server config more reliably + let aw_server_conf = crate::dirs::get_server_config_path(testing) + .map_err(|_| "Could not get aw-server config path")?; + let fallback: u16 = if testing { 5666 } else { 5600 }; + let port = if aw_server_conf.exists() { + let mut file = File::open(&aw_server_conf)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + let value: toml::Value = toml::from_str(&contents)?; + value + .get("port") + .and_then(|v| v.as_integer()) + .map(|v| v as u16) + .unwrap_or(fallback) + } else { + fallback + }; + Ok(port) +} + +/// Check if a directory contains a .db file +fn contains_db_file(dir: &std::path::Path) -> bool { + fs::read_dir(dir) + .ok() + .map(|entries| { + entries.filter_map(Result::ok).any(|entry| { + entry + .path() + .extension() + .map(|ext| ext == "db") + .unwrap_or(false) + }) + }) + .unwrap_or(false) +} + +/// Check if a directory contains a subdirectory that contains a .db file +fn contains_subdir_with_db_file(dir: &std::path::Path) -> bool { + fs::read_dir(dir) + .ok() + .map(|entries| { + entries + .filter_map(Result::ok) + .any(|entry| entry.path().is_dir() && contains_db_file(&entry.path())) + }) + .unwrap_or(false) +} + +/// Return all remotes in the sync folder +/// Only returns folders that match ./{host}/{device_id}/*.db +// TODO: share logic with find_remotes and find_remotes_nonlocal +pub fn get_remotes() -> Result<Vec<String>, Box<dyn Error>> { + let sync_root_dir = crate::dirs::get_sync_dir().map_err(|_| "Could not get sync dir")?; + let hostnames = fs::read_dir(sync_root_dir)? + .filter_map(Result::ok) + .filter(|entry| entry.path().is_dir() && contains_subdir_with_db_file(&entry.path())) + .filter_map(|entry| { + entry + .path() + .file_name() + .and_then(|os_str| os_str.to_str().map(String::from)) + }) + .collect(); + info!("Found remotes: {:?}", hostnames); + Ok(hostnames) +} + +/// Returns a list of all remote dbs +fn find_remotes(sync_directory: &Path) -> std::io::Result<Vec<PathBuf>> { + let dbs = fs::read_dir(sync_directory)? + .map(|res| res.ok().unwrap().path()) + .filter(|p| p.is_dir()) + .flat_map(|d| fs::read_dir(d).unwrap()) + .map(|res| res.ok().unwrap().path()) + .filter(|path| path.extension().unwrap_or_else(|| OsStr::new("")) == "db") + .collect(); + Ok(dbs) +} + +/// Returns a list of all remotes, excluding local ones +pub fn find_remotes_nonlocal( + sync_directory: &Path, + device_id: &str, + sync_db: Option<&PathBuf>, +) -> Vec<PathBuf> { + let remotes_all = find_remotes(sync_directory).unwrap(); + remotes_all + .into_iter() + // Filter out own remote + .filter(|path| { + !(path + .clone() + .into_os_string() + .into_string() + .unwrap() + .contains(device_id)) + }) + // If sync_db is Some, return only remotes in that path + .filter(|path| { + if let Some(sync_db) = sync_db { + path.starts_with(sync_db) + } else { + true + } + }) + .collect() +} diff --git a/aw-sync/test-sync-pull.sh b/aw-sync/test-sync-pull.sh index 89f4235a..851f8216 100755 --- a/aw-sync/test-sync-pull.sh +++ b/aw-sync/test-sync-pull.sh @@ -34,11 +34,12 @@ function sync_host() { continue fi - AWSYNCPARAMS="--port $PORT --sync-dir $SYNCDIR --sync-db $db" + AWSYNC_ARGS="--port $PORT" + AWSYNC_ARGS_ADV="--sync-dir $SYNCDIR --sync-db $db" BUCKETS="aw-watcher-window_$host,aw-watcher-afk_$host" echo "Syncing $db to $host" - cargo run --bin aw-sync -- $AWSYNCPARAMS sync --mode pull --buckets $BUCKETS + cargo run --bin aw-sync -- $AWSYNC_ARGS sync-advanced $AWSYNC_ARGS_ADV --mode pull --buckets $BUCKETS # TODO: If there are no buckets from the expected host, emit a warning at the end. # (push-script should not have created them to begin with) done diff --git a/aw-sync/test-sync-push.sh b/aw-sync/test-sync-push.sh index 58492669..3ba4b179 100755 --- a/aw-sync/test-sync-push.sh +++ b/aw-sync/test-sync-push.sh @@ -25,7 +25,8 @@ else fi SYNCDIR="$HOME/ActivityWatchSync/$HOSTNAME" -AWSYNCPARAMS="--port $PORT --sync-dir $SYNCDIR" +AWSYNC_ARGS="--port $PORT" +AWSYNC_ARGS_ADV="--sync-dir $SYNCDIR" # NOTE: Only sync window and AFK buckets, for now -cargo run --bin aw-sync --release -- $AWSYNCPARAMS sync --mode push --buckets aw-watcher-window_$HOSTNAME,aw-watcher-afk_$HOSTNAME +cargo run --bin aw-sync --release -- $AWSYNC_ARGS sync-advanced $AWSYNC_ARGS_ADV --mode push --buckets aw-watcher-window_$HOSTNAME,aw-watcher-afk_$HOSTNAME