From b34aa75dfe7e0bd885e32c012314704d525b8f4c Mon Sep 17 00:00:00 2001 From: Eric Warmenhoven Date: Tue, 16 Dec 2025 23:47:46 -0500 Subject: [PATCH] cloud sync: conflict resolution It resolves all conflicts the same way, not asking per file, which is probably fine. Most of the time you probably want to accept all of the server changes anyway. --- command.h | 4 +++ intl/msg_hash_lbl.h | 8 +++++ intl/msg_hash_us.h | 16 +++++++++ menu/cbs/menu_cbs_sublabel.c | 8 +++++ menu/menu_displaylist.c | 17 ++++++++++ menu/menu_setting.c | 21 ++++++++++++ msg_hash.h | 2 ++ retroarch.c | 6 ++++ tasks/task_cloudsync.c | 63 +++++++++++++++++++++++++++--------- tasks/tasks_internal.h | 2 ++ 10 files changed, 132 insertions(+), 15 deletions(-) diff --git a/command.h b/command.h index 6fd256541b9f..5c036c4dd2d7 100644 --- a/command.h +++ b/command.h @@ -152,6 +152,10 @@ enum event_command #ifdef HAVE_CLOUDSYNC /* Trigger cloud sync */ CMD_EVENT_CLOUD_SYNC, + /* Resolve cloud sync conflicts by keeping local files */ + CMD_EVENT_CLOUD_SYNC_RESOLVE_KEEP_LOCAL, + /* Resolve cloud sync conflicts by keeping server files */ + CMD_EVENT_CLOUD_SYNC_RESOLVE_KEEP_SERVER, #endif /* Shutdown the OS */ CMD_EVENT_SHUTDOWN, diff --git a/intl/msg_hash_lbl.h b/intl/msg_hash_lbl.h index bc9096010e7d..c73e6221b9fa 100644 --- a/intl/msg_hash_lbl.h +++ b/intl/msg_hash_lbl.h @@ -3022,6 +3022,14 @@ MSG_HASH( MENU_ENUM_LABEL_CLOUD_SYNC_SYNC_NOW, "cloud_sync_sync_now" ) +MSG_HASH( + MENU_ENUM_LABEL_CLOUD_SYNC_RESOLVE_KEEP_LOCAL, + "cloud_sync_resolve_keep_local" + ) +MSG_HASH( + MENU_ENUM_LABEL_CLOUD_SYNC_RESOLVE_KEEP_SERVER, + "cloud_sync_resolve_keep_server" + ) MSG_HASH( MENU_ENUM_LABEL_RDB_ENTRY, "rdb_entry" diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index e27b0b521e15..07416aa1e6e5 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -239,6 +239,22 @@ MSG_HASH( MENU_ENUM_SUBLABEL_CLOUD_SYNC_SYNC_NOW, "Manually trigger cloud synchronization." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_RESOLVE_KEEP_LOCAL, + "Resolve Conflicts: Keep Local" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CLOUD_SYNC_RESOLVE_KEEP_LOCAL, + "Resolve all conflicts by uploading local files to the server." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_RESOLVE_KEEP_SERVER, + "Resolve Conflicts: Keep Server" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CLOUD_SYNC_RESOLVE_KEEP_SERVER, + "Resolve all conflicts by downloading server files, replacing local copies." + ) MSG_HASH( MENU_ENUM_SUBLABEL_QUIT_RETROARCH_NOSAVE, "Quit RetroArch application. Configuration save on exit is disabled." diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index d48c4e4cd09c..59564c220970 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -636,6 +636,8 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_core_list_unload, MENU_ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_download_core, MENU_ENUM_SUBLABEL_DOWNLOAD_CORE) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_update_installed_cores, MENU_ENUM_SUBLABEL_UPDATE_INSTALLED_CORES) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_sync_now, MENU_ENUM_SUBLABEL_CLOUD_SYNC_SYNC_NOW) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_resolve_keep_local, MENU_ENUM_SUBLABEL_CLOUD_SYNC_RESOLVE_KEEP_LOCAL) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_resolve_keep_server, MENU_ENUM_SUBLABEL_CLOUD_SYNC_RESOLVE_KEEP_SERVER) #if defined(ANDROID) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_switch_installed_cores_pfd, MENU_ENUM_SUBLABEL_SWITCH_INSTALLED_CORES_PFD) #endif @@ -4632,6 +4634,12 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_CLOUD_SYNC_SYNC_NOW: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_sync_now); break; + case MENU_ENUM_LABEL_CLOUD_SYNC_RESOLVE_KEEP_LOCAL: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_resolve_keep_local); + break; + case MENU_ENUM_LABEL_CLOUD_SYNC_RESOLVE_KEEP_SERVER: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_resolve_keep_server); + break; #if defined(ANDROID) case MENU_ENUM_LABEL_SWITCH_INSTALLED_CORES_PFD: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_switch_installed_cores_pfd); diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index 6ec6ea2e3081..6920acd42f6e 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -11116,6 +11116,23 @@ unsigned menu_displaylist_build_list( false) == 0) count++; } + + /* Add action items when cloud sync is enabled */ + if (settings->bools.cloud_sync_enable) + { + if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, + MENU_ENUM_LABEL_CLOUD_SYNC_SYNC_NOW, + PARSE_ACTION, false) == 0) + count++; + if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, + MENU_ENUM_LABEL_CLOUD_SYNC_RESOLVE_KEEP_LOCAL, + PARSE_ACTION, false) == 0) + count++; + if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, + MENU_ENUM_LABEL_CLOUD_SYNC_RESOLVE_KEEP_SERVER, + PARSE_ACTION, false) == 0) + count++; + } } break; #ifdef HAVE_MIST diff --git a/menu/menu_setting.c b/menu/menu_setting.c index cc6d115642b9..832a4c11d9ed 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -10222,6 +10222,24 @@ static bool setting_append_list( &subgroup_info, parent_group); MENU_SETTINGS_LIST_CURRENT_ADD_CMD(list, list_info, CMD_EVENT_CLOUD_SYNC); + + CONFIG_ACTION( + list, list_info, + MENU_ENUM_LABEL_CLOUD_SYNC_RESOLVE_KEEP_LOCAL, + MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_RESOLVE_KEEP_LOCAL, + &group_info, + &subgroup_info, + parent_group); + MENU_SETTINGS_LIST_CURRENT_ADD_CMD(list, list_info, CMD_EVENT_CLOUD_SYNC_RESOLVE_KEEP_LOCAL); + + CONFIG_ACTION( + list, list_info, + MENU_ENUM_LABEL_CLOUD_SYNC_RESOLVE_KEEP_SERVER, + MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_RESOLVE_KEEP_SERVER, + &group_info, + &subgroup_info, + parent_group); + MENU_SETTINGS_LIST_CURRENT_ADD_CMD(list, list_info, CMD_EVENT_CLOUD_SYNC_RESOLVE_KEEP_SERVER); #endif CONFIG_ACTION( @@ -11795,6 +11813,9 @@ static bool setting_append_list( general_write_handler, general_read_handler, SD_FLAG_NONE); + (*list)[list_info->index - 1].action_ok = &setting_bool_action_left_with_refresh; + (*list)[list_info->index - 1].action_left = &setting_bool_action_left_with_refresh; + (*list)[list_info->index - 1].action_right = &setting_bool_action_right_with_refresh; CONFIG_BOOL( list, list_info, diff --git a/msg_hash.h b/msg_hash.h index 42012d692493..57ef945b24c2 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -3302,6 +3302,8 @@ enum msg_hash_enums MENU_LABEL(CLOUD_SYNC_USERNAME), MENU_LABEL(CLOUD_SYNC_PASSWORD), MENU_LABEL(CLOUD_SYNC_SYNC_NOW), + MENU_LABEL(CLOUD_SYNC_RESOLVE_KEEP_LOCAL), + MENU_LABEL(CLOUD_SYNC_RESOLVE_KEEP_SERVER), MENU_LABEL(RECORDING_SETTINGS), MENU_LABEL(OVERLAY_SETTINGS), MENU_LABEL(REWIND_SETTINGS), diff --git a/retroarch.c b/retroarch.c index d4678031c920..8ba4f0323aa6 100644 --- a/retroarch.c +++ b/retroarch.c @@ -4641,6 +4641,12 @@ bool command_event(enum event_command cmd, void *data) case CMD_EVENT_CLOUD_SYNC: task_push_cloud_sync(); break; + case CMD_EVENT_CLOUD_SYNC_RESOLVE_KEEP_LOCAL: + task_push_cloud_sync_resolve_keep_local(); + break; + case CMD_EVENT_CLOUD_SYNC_RESOLVE_KEEP_SERVER: + task_push_cloud_sync_resolve_keep_server(); + break; #endif case CMD_EVENT_MENU_RESET_TO_DEFAULT_CONFIG: config_set_defaults(global_get_ptr()); diff --git a/tasks/task_cloudsync.c b/tasks/task_cloudsync.c index 5a818f4b26d0..75fd4e70dddb 100644 --- a/tasks/task_cloudsync.c +++ b/tasks/task_cloudsync.c @@ -69,6 +69,8 @@ typedef struct bool need_manifest_uploaded; bool failures; bool conflicts; + /* Conflict resolution mode: 0=none, 1=keep_local, 2=keep_server */ + int conflict_resolution; uint32_t uploads; uint32_t downloads; retro_time_t start_time; @@ -76,6 +78,10 @@ typedef struct static slock_t *tcs_running_lock = NULL; +/* Forward declarations for conflict resolution */ +static void task_cloud_sync_upload_current_file(task_cloud_sync_state_t *sync_state); +static void task_cloud_sync_fetch_server_file(task_cloud_sync_state_t *sync_state); + static void task_cloud_sync_begin_handler(void *user_data, const char *path, bool success, RFILE *file) { retro_task_t *task = (retro_task_t *)user_data; @@ -680,19 +686,28 @@ static void task_cloud_sync_fetch_server_file(task_cloud_sync_state_t *sync_stat static void task_cloud_sync_resolve_conflict(task_cloud_sync_state_t *sync_state) { - /* - * rather than pop up some UI let's just resolve it ourselves! - * three options: - * 1. rename the server file and replace it - * 2. rename the local file and replace it - * 3. ignore it - * If we ignore it then we need to keep it out of the new local manifest - */ struct item_file *server_file = &sync_state->server_manifest->list[sync_state->server_idx]; - RARCH_WARN(CSPFX "Conflicting change of %s.\n", CS_FILE_KEY(server_file)); - task_cloud_sync_add_to_updated_manifest(sync_state, CS_FILE_KEY(server_file), CS_FILE_HASH(server_file), true); - /* no need to mark need_manifest_uploaded, nothing changed */ - sync_state->conflicts = true; + + if (sync_state->conflict_resolution == 1) + { + /* Keep local: upload local file to server */ + RARCH_LOG(CSPFX "Conflict on %s, keeping local.\n", CS_FILE_KEY(server_file)); + task_cloud_sync_upload_current_file(sync_state); + } + else if (sync_state->conflict_resolution == 2) + { + /* Keep server: download server file */ + RARCH_LOG(CSPFX "Conflict on %s, keeping server.\n", CS_FILE_KEY(server_file)); + task_cloud_sync_fetch_server_file(sync_state); + } + else + { + /* Default: ignore the conflict, keep file out of local manifest */ + RARCH_WARN(CSPFX "Conflicting change of %s.\n", CS_FILE_KEY(server_file)); + task_cloud_sync_add_to_updated_manifest(sync_state, CS_FILE_KEY(server_file), CS_FILE_HASH(server_file), true); + /* no need to mark need_manifest_uploaded, nothing changed */ + sync_state->conflicts = true; + } } static void task_cloud_sync_upload_cb(void *user_data, const char *path, bool success, RFILE *file) @@ -1333,7 +1348,7 @@ static bool task_cloud_sync_task_finder(retro_task_t *task, void *user_data) return task->handler == task_cloud_sync_task_handler; } -void task_push_cloud_sync(void) +static void task_push_cloud_sync_with_mode(int conflict_resolution) { char task_title[128]; task_finder_data_t find_data; @@ -1364,8 +1379,9 @@ void task_push_cloud_sync(void) return; } - sync_state->phase = CLOUD_SYNC_PHASE_BEGIN; - sync_state->start_time = cpu_features_get_time_usec(); + sync_state->phase = CLOUD_SYNC_PHASE_BEGIN; + sync_state->start_time = cpu_features_get_time_usec(); + sync_state->conflict_resolution = conflict_resolution; strlcpy(task_title, "Cloud Sync in progress", sizeof(task_title)); @@ -1377,6 +1393,11 @@ void task_push_cloud_sync(void) task_queue_push(task); } +void task_push_cloud_sync(void) +{ + task_push_cloud_sync_with_mode(0); +} + void task_push_cloud_sync_update_driver(void) { char manifest_path[PATH_MAX_LENGTH]; @@ -1392,3 +1413,15 @@ void task_push_cloud_sync_update_driver(void) task_cloud_sync_manifest_filename(manifest_path, sizeof(manifest_path), false); filestream_delete(manifest_path); } + +void task_push_cloud_sync_resolve_keep_local(void) +{ + RARCH_LOG(CSPFX "Starting sync with conflict resolution: keep local.\n"); + task_push_cloud_sync_with_mode(1); +} + +void task_push_cloud_sync_resolve_keep_server(void) +{ + RARCH_LOG(CSPFX "Starting sync with conflict resolution: keep server.\n"); + task_push_cloud_sync_with_mode(2); +} diff --git a/tasks/tasks_internal.h b/tasks/tasks_internal.h index 055281132ca4..54f853780613 100644 --- a/tasks/tasks_internal.h +++ b/tasks/tasks_internal.h @@ -275,6 +275,8 @@ extern const char* const input_builtin_autoconfs[]; /* cloud sync tasks */ void task_push_cloud_sync_update_driver(void); void task_push_cloud_sync(void); +void task_push_cloud_sync_resolve_keep_local(void); +void task_push_cloud_sync_resolve_keep_server(void); RETRO_END_DECLS