Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 58 additions & 17 deletions aw-sync/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ enum Commands {
List {},
/// Doctor: classify every entry in the sync folder and say why pull is empty.
///
/// 3-level peers come from the same `RemoteDb` walker `pull_all` uses;
/// 2-level leftovers and unrecognised entries sit on top of that list.
/// Peers come from the same `RemoteDb` walker `pull_all` uses (2-level
/// leftovers and 3-level hosts). Unrecognised entries sit on top.
/// Does not create staging files.
Status {},
}
Expand Down Expand Up @@ -304,6 +304,20 @@ fn main() -> Result<(), Box<dyn Error>> {
Ok(())
}

/// One daemon/sync cycle using the host-layout that Android and
/// `aw-sync sync` (no advanced flags) already write and scan.
fn run_host_layout_sync(client: &AwClient, mode: sync::SyncMode) -> Result<(), Box<dyn Error>> {
if mode == sync::SyncMode::Pull || mode == sync::SyncMode::Both {
info!("Pulling from all hosts");
sync_wrapper::pull_all(client)?;
}
if mode == sync::SyncMode::Push || mode == sync::SyncMode::Both {
info!("Pushing local data");
sync_wrapper::push(client)?;
}
Ok(())
}

fn daemon(
client: &AwClient,
start_date: Option<DateTime<Utc>>,
Expand All @@ -317,27 +331,54 @@ fn daemon(
let _ = tx.send(());
})?;

let sync_dir = dirs::get_sync_dir()?;
if let Some(db_path) = &sync_db {
info!("Using sync db: {}", db_path.display());
// Default daemon (what aw-qt and the bundled binary run) must use the
// same `{sync_dir}/{hostname}/{device_id}/` layout as `aw-sync sync`
// and Android. Driving `sync_run` against the sync root writes a
// 2-level `{device_id}/test.db` that neither peers nor `find_remotes`
// on a 3-level tree can see — so the daemon silently never pulls
// (ActivityWatch/aw-server-rust#682).
//
// Host-layout pull goes through `list_remote_dbs`, which now sees both
// 3-level peers and leftover 2-level `{device_id}/*.db` (hostname
// unknown). Same-device duplicates pick newest data, not size — so a
// large leftover does not shadow a current 3-level file. On first
// push, an own leftover 2-level db is renamed into the 3-level path
// instead of starting empty. The advanced `--buckets` / `--start-date`
// / `--sync-db` path still uses `find_remotes` (2-level relative to
// the given directory).
let use_host_layout = start_date.is_none() && buckets.is_none() && sync_db.is_none();

let sync_spec = if use_host_layout {
info!("Daemon using host-layout sync (compatible with `aw-sync sync` and Android)");
None
} else {
let sync_dir = dirs::get_sync_dir()?;
if let Some(db_path) = &sync_db {
info!("Using sync db: {}", db_path.display());

if !db_path.is_absolute() {
Err("Sync db path must be absolute")?
}
if !db_path.starts_with(&sync_dir) {
Err("Sync db path must be in sync directory")?
if !db_path.is_absolute() {
Err("Sync db path must be absolute")?
}
if !db_path.starts_with(&sync_dir) {
Err("Sync db path must be in sync directory")?
}
}
}

let sync_spec = sync::SyncSpec {
path: sync_dir,
buckets,
path_db: sync_db,
start: start_date,
Some(sync::SyncSpec {
path: sync_dir,
buckets,
path_db: sync_db,
start: start_date,
})
};

loop {
if let Err(e) = sync::sync_run(client, &sync_spec, mode) {
let cycle_result = if let Some(spec) = &sync_spec {
sync::sync_run(client, spec, mode)
} else {
run_host_layout_sync(client, mode)
};
if let Err(e) = cycle_result {
error!("Error during sync cycle: {}", e);
return Err(e);
}
Expand Down
5 changes: 3 additions & 2 deletions aw-sync/src/status.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! `aw-sync status` — a doctor command for the sync folder.
//!
//! Read-only: never creates staging databases. 3-level peers are the
//! `RemoteDb` list `pull_all` uses; classification of leftovers sits on top.
//! Read-only: never creates staging databases. Peers are the `RemoteDb`
//! list `pull_all` uses (2-level leftovers and 3-level hosts); unrecognised
//! entries sit on top.

use std::collections::{BTreeMap, HashSet};
use std::error::Error;
Expand Down
29 changes: 25 additions & 4 deletions aw-sync/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,16 @@ pub fn sync_run(
let device_id = info.device_id.as_str();

// FIXME: Bad device_id assumption?
let ds_localremote = setup_local_remote(sync_spec.path.as_path(), device_id)?;
// Only stage a local db when this pass actually pushes. Pull-only
// `sync_run` is how `sync_wrapper::pull` walks a *peer's* host folder;
// creating `{peer_host}/{our_device_id}/test.db` there breaks the
// "each device only writes files it owns" invariant (see
// ActivityWatch/aw-server-rust#682).
let ds_localremote = if mode == SyncMode::Push || mode == SyncMode::Both {
Some(setup_local_remote(sync_spec.path.as_path(), device_id)?)
} else {
None
};
let remote_dbfiles = crate::util::find_remotes_nonlocal(
sync_spec.path.as_path(),
device_id,
Expand Down Expand Up @@ -130,16 +139,18 @@ pub fn sync_run(
}

// Push local server buckets to sync folder
if mode == SyncMode::Push || mode == SyncMode::Both {
if let Some(ds_localremote) = &ds_localremote {
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
for ds_from in &ds_remotes {
ds_from.close();
}
ds_localremote.close();
if let Some(ds_localremote) = &ds_localremote {
ds_localremote.close();
}

// Dropping also works to close the database connections, weirdly enough.
// Probably because once the database is dropped, the thread will stop,
Expand Down Expand Up @@ -186,6 +197,16 @@ fn setup_local_remote(path: &Path, device_id: &str) -> Result<Datastore, Box<dyn
// FIXME: Don't run twice if already exists
fs::create_dir_all(path)?;

// Host-layout push uses `path` = `{sync_root}/{hostname}`. If this device
// still has a leftover 2-level `{sync_root}/{device_id}/test.db` and no
// 3-level dest yet, rename it into place instead of creating an empty
// staging db (which would re-export the entire history).
if let (Some(sync_root), Some(hostname)) =
(path.parent(), path.file_name().and_then(|s| s.to_str()))
{
crate::util::promote_legacy_own_db(sync_root, hostname, device_id)?;
}

let remotedir = path.join(device_id);
fs::create_dir_all(&remotedir)?;

Expand Down
17 changes: 11 additions & 6 deletions aw-sync/src/sync_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ pub fn pull_all(client: &AwClient) -> Result<(), Box<dyn Error>> {
.collect::<Vec<_>>()
);
for remote in selected {
pull_db(client, &remote.hostname, &remote.path)?;
let walk_root = if remote.hostname.is_empty() {
// 2-level leftover: `{sync_root}/{device_id}/test.db`. Walk from
// the sync root so `find_remotes` can see it.
sync_root.clone()
} else {
sync_root.join(&remote.hostname)
};
pull_db(client, &walk_root, &remote.path)?;
}
Ok(())
}
Expand Down Expand Up @@ -58,15 +65,13 @@ pub fn pull(host: &str, client: &AwClient) -> Result<(), Box<dyn Error>> {
.max_by_key(|entry| entry.metadata().map(|m| m.len()).unwrap_or(0))
.ok_or_else(|| format!("No db found in sync folder {:?}", sync_dir))?;

pull_db(client, host, &db.path())
pull_db(client, &sync_dir, &db.path())
}

fn pull_db(client: &AwClient, host: &str, db_path: &Path) -> Result<(), Box<dyn Error>> {
fn pull_db(client: &AwClient, walk_root: &Path, db_path: &Path) -> Result<(), Box<dyn Error>> {
client.wait_for_start()?;
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 sync_spec = SyncSpec {
path: sync_dir,
path: walk_root.to_path_buf(),
path_db: Some(db_path.to_path_buf()),
buckets: None, // Sync all buckets by default
start: None,
Expand Down
Loading
Loading