{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreihs4mrlwbobzwy4lq6nylaz7bcehtmgx2wahuv5ngdy4v7gddddd4",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mom7jvsoqwq2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreign5bq2jfrhi2ks3txfajtcc3uob7dq3i33uvu3mqj77etylu2d2m"
},
"mimeType": "image/webp",
"size": 451672
},
"path": "/hiyoyok/conflict-resolution-in-a-bidirectional-sync-app-how-i-handle-the-hard-cases-5e24",
"publishedAt": "2026-06-19T01:07:18.000Z",
"site": "https://dev.to",
"tags": [
"tauri",
"rust",
"android",
"programming",
"HiyokoAutoSync",
"@hiyoyok"
],
"textContent": "All tests run on an 8-year-old MacBook Air. All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.\n\nHiyokoAutoSync 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.\n\n## The conflict cases\n\n 1. File modified on both sides since last sync — which version wins?\n 2. File deleted on one side, modified on the other — delete or keep?\n 3. File moved on one side — move on the other side or treat as delete + create?\n\n\n\nMost sync apps punt on cases 1 and 3. Here's my approach.\n\n## Detecting conflicts\n\nTrack last-synced state in SQLite:\n\n\n\n CREATE TABLE sync_state (\n file_path TEXT PRIMARY KEY,\n mac_hash TEXT,\n android_hash TEXT,\n mac_modified INTEGER,\n android_modified INTEGER,\n last_synced INTEGER\n );\n\n\nOn sync check:\n\n\n\n fn classify_file(record: &SyncRecord, mac_stat: &FileStat, android_stat: &FileStat) -> SyncAction {\n let mac_changed = mac_stat.hash != record.mac_hash;\n let android_changed = android_stat.hash != record.android_hash;\n\n match (mac_changed, android_changed) {\n (true, false) => SyncAction::CopyToAndroid,\n (false, true) => SyncAction::CopyToMac,\n (false, false) => SyncAction::NoOp,\n (true, true) => SyncAction::Conflict,\n }\n }\n\n\n## Conflict resolution strategies\n\nI offer three strategies, user-configurable:\n\n**Newer wins** : compare modification timestamps, keep the more recent file.\n\n\n\n SyncAction::Conflict => {\n if mac_stat.modified > android_stat.modified {\n SyncAction::CopyToAndroid\n } else {\n SyncAction::CopyToMac\n }\n }\n\n\n**Mac always wins** : for users who treat Mac as source of truth.\n\n**Keep both** : rename one file with a conflict suffix, keep both versions.\n\n\n\n // Rename Android version to \"file.conflict-2026-05-01.ext\"\n let conflict_name = add_conflict_suffix(&file_path);\n copy_to_mac_as(&android_file, &conflict_name)?;\n copy_to_android(&mac_file)?;\n\n\n## Delete vs modify conflict\n\nFile deleted on Mac, modified on Android:\n\n\n\n (Deleted, Modified) => {\n // Default: keep the modified file, restore it on Mac\n // Alternative: delete from both sides\n // User configurable\n SyncAction::RestoreToMac\n }\n\n\nThe safe default is to keep data. Deleting across both sides on a conflict can cause data loss users didn't intend.\n\n## The verdict\n\nBidirectional 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.\n\n**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.\n\nIf this was useful, a ❤️ helps more than you'd think — thanks!\n\nHiyokoAutoSync | X → @hiyoyok",
"title": "Conflict Resolution in a Bidirectional Sync App — How I Handle the Hard Cases"
}