All tests run on an 8-year-old MacBook Air. All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.
HiyokoAutoSync does bidirectional sync between Android and Mac. Bidirectional sync has a hard problem: what happens when the same file is modified on both sides? Here's how I handle it.
The conflict cases
- File modified on both sides since last sync — which version wins?
- File deleted on one side, modified on the other — delete or keep?
- File moved on one side — move on the other side or treat as delete + create?
Most sync apps punt on cases 1 and 3. Here's my approach.
Detecting conflicts
Track last-synced state in SQLite:
CREATE TABLE sync_state (
file_path TEXT PRIMARY KEY,
mac_hash TEXT,
android_hash TEXT,
mac_modified INTEGER,
android_modified INTEGER,
last_synced INTEGER
);
On sync check:
fn classify_file(record: &SyncRecord, mac_stat: &FileStat, android_stat: &FileStat) -> SyncAction {
let mac_changed = mac_stat.hash != record.mac_hash;
let android_changed = android_stat.hash != record.android_hash;
match (mac_changed, android_changed) {
(true, false) => SyncAction::CopyToAndroid,
(false, true) => SyncAction::CopyToMac,
(false, false) => SyncAction::NoOp,
(true, true) => SyncAction::Conflict,
}
}
Conflict resolution strategies
I offer three strategies, user-configurable:
Newer wins: compare modification timestamps, keep the more recent file.
SyncAction::Conflict => {
if mac_stat.modified > android_stat.modified {
SyncAction::CopyToAndroid
} else {
SyncAction::CopyToMac
}
}
Mac always wins: for users who treat Mac as source of truth.
Keep both: rename one file with a conflict suffix, keep both versions.
// Rename Android version to "file.conflict-2026-05-01.ext"
let conflict_name = add_conflict_suffix(&file_path);
copy_to_mac_as(&android_file, &conflict_name)?;
copy_to_android(&mac_file)?;
Delete vs modify conflict
File deleted on Mac, modified on Android:
(Deleted, Modified) => {
// Default: keep the modified file, restore it on Mac
// Alternative: delete from both sides
// User configurable
SyncAction::RestoreToMac
}
The safe default is to keep data. Deleting across both sides on a conflict can cause data loss users didn't intend.
The verdict
Bidirectional sync without conflict resolution is a bug waiting to happen. The "newer wins" strategy covers 90% of real-world cases. Keep both covers the rest. Make it configurable for power users.
TL;DR: Track sync state (hash + modified time) in SQLite per file. Classify each file into CopyToAndroid, CopyToMac, NoOp, or Conflict using a match on what changed. Offer three strategies: newer wins, Mac wins, or keep both. For delete vs modify conflicts, default to keeping data — accidental deletion is worse than a duplicate.
If this was useful, a ❤️ helps more than you'd think — thanks!
HiyokoAutoSync | X → @hiyoyok
Top comments (0)