diff --git a/common/rpc-service.c b/common/rpc-service.c index 6c0b20b4..32a7503c 100644 --- a/common/rpc-service.c +++ b/common/rpc-service.c @@ -988,3 +988,9 @@ seafile_sync_error_id_to_str (int error_id, GError **error) { return g_strdup(sync_error_id_to_str (error_id)); } + +int +seafile_add_del_confirmation (const char *confirmation_id, int resync, GError **error) +{ + return seaf_sync_manager_add_del_confirmation (seaf->sync_mgr, confirmation_id, resync); +} diff --git a/daemon/seaf-daemon.c b/daemon/seaf-daemon.c index 07794cd0..591c0bac 100644 --- a/daemon/seaf-daemon.c +++ b/daemon/seaf-daemon.c @@ -92,6 +92,11 @@ register_rpc_service () "seafile_sync_error_id_to_str", searpc_signature_string__int()); + searpc_server_register_function ("seafile-rpcserver", + seafile_add_del_confirmation, + "seafile_add_del_confirmation", + searpc_signature_int__string_int()); + searpc_server_register_function ("seafile-rpcserver", seafile_get_config, "seafile_get_config", diff --git a/daemon/seafile-config.c b/daemon/seafile-config.c index 5d721fa3..518b1037 100644 --- a/daemon/seafile-config.c +++ b/daemon/seafile-config.c @@ -168,6 +168,9 @@ seafile_session_config_set_int (SeafileSession *session, if (g_strcmp0(key, KEY_PROXY_PORT) == 0) { session->http_proxy_port = value; } + if (g_strcmp0(key, KEY_DELETE_CONFIRM_THRESHOLD) == 0) { + session->delete_confirm_threshold = value; + } return 0; } diff --git a/daemon/seafile-config.h b/daemon/seafile-config.h index e81881b5..772e171d 100644 --- a/daemon/seafile-config.h +++ b/daemon/seafile-config.h @@ -37,6 +37,7 @@ #define KEY_PROXY_PASSWORD "proxy_password" #define PROXY_TYPE_HTTP "http" #define PROXY_TYPE_SOCKS "socks" +#define KEY_DELETE_CONFIRM_THRESHOLD "delete_confirm_threshold" gboolean seafile_session_config_exists (SeafileSession *session, const char *key); diff --git a/daemon/seafile-error.c b/daemon/seafile-error.c index 0c43f1f1..b63448e7 100644 --- a/daemon/seafile-error.c +++ b/daemon/seafile-error.c @@ -174,6 +174,11 @@ static SyncErrorInfo sync_error_info_tbl[] = { SYNC_ERROR_LEVEL_REPO, "Library is too large to sync" }, + { + SYNC_ERROR_ID_DEL_CONFIRMATION_PENDING, + SYNC_ERROR_LEVEL_REPO, + "Waiting for confirmation to delete files" + }, }; const char * diff --git a/daemon/seafile-session.c b/daemon/seafile-session.c index a2f28b59..2461b5f4 100644 --- a/daemon/seafile-session.c +++ b/daemon/seafile-session.c @@ -348,6 +348,8 @@ out: g_key_file_free (key_file); } +#define MAX_DELETED_FILES_NUM 500 + void seafile_session_prepare (SeafileSession *session) { @@ -415,6 +417,10 @@ seafile_session_prepare (SeafileSession *session) seafile_session_config_get_string(session, KEY_PROXY_PASSWORD); } + session->delete_confirm_threshold = seafile_session_config_get_int (session, KEY_DELETE_CONFIRM_THRESHOLD, NULL); + if (session->delete_confirm_threshold <= 0) + session->delete_confirm_threshold = MAX_DELETED_FILES_NUM; + int block_size = seafile_session_config_get_int(session, KEY_CDC_AVERAGE_BLOCK_SIZE, NULL); if (block_size >= 1024) { session->cdc_average_block_size = block_size; diff --git a/daemon/seafile-session.h b/daemon/seafile-session.h index 30ad841e..89a4d370 100644 --- a/daemon/seafile-session.h +++ b/daemon/seafile-session.h @@ -88,6 +88,7 @@ struct _SeafileSession { int http_proxy_port; char *http_proxy_username; char *http_proxy_password; + int delete_confirm_threshold; }; struct _SeafileSessionClass diff --git a/daemon/sync-mgr.c b/daemon/sync-mgr.c index e54818c4..6489b91c 100644 --- a/daemon/sync-mgr.c +++ b/daemon/sync-mgr.c @@ -15,6 +15,7 @@ #include "vc-utils.h" #include "sync-status-tree.h" +#include "diff-simple.h" #ifdef WIN32 #include @@ -62,6 +63,10 @@ struct _HttpServerState { }; typedef struct _HttpServerState HttpServerState; +typedef struct DelConfirmationResult { + gboolean resync; +} DelConfirmationResult; + struct _SeafSyncManagerPriv { struct SeafTimer *check_sync_timer; struct SeafTimer *update_tx_state_timer; @@ -77,6 +82,9 @@ struct _SeafSyncManagerPriv { GAsyncQueue *refresh_paths; struct SeafTimer *refresh_windows_timer; #endif + + pthread_mutex_t del_confirmation_lock; + GHashTable *del_confirmation_tasks; }; struct _ActivePathsInfo { @@ -165,6 +173,11 @@ seaf_sync_manager_new (SeafileSession *seaf) mgr->priv->refresh_paths = g_async_queue_new (); #endif + mgr->priv->del_confirmation_tasks = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, + g_free); + pthread_mutex_init (&mgr->priv->del_confirmation_lock, NULL); + return mgr; } @@ -970,6 +983,11 @@ commit_job (void *vtask) return res; } +static char * +exceed_max_deleted_files (SeafRepo *repo); +static void +notify_delete_confirmation (const char *repo_name, const char *desc, const char *confirmation_id); + static void commit_job_done (void *vres) { @@ -998,8 +1016,20 @@ commit_job_done (void *vres) return; } - if (res->changed) + if (res->changed) { + char *desc = NULL; + desc = exceed_max_deleted_files (repo); + if (desc) { + notify_delete_confirmation (repo->name, desc, repo->head->commit_id); + seaf_warning ("Delete more than %d files, add delete confirmation.\n", seaf->delete_confirm_threshold); + task->info->del_confirmation_pending = TRUE; + set_task_error (res->task, SYNC_ERROR_ID_DEL_CONFIRMATION_PENDING); + g_free (desc); + g_free (res); + return; + } start_upload_if_necessary (res->task); + } else if (task->is_manual_sync || task->is_initial_commit) check_head_commit_http (task); else @@ -1186,6 +1216,194 @@ out: return ret; } +#define MAX_DELETED_FILES_NUM 100 + +inline static char * +get_basename (char *path) +{ + char *slash; + slash = strrchr (path, '/'); + if (!slash) + return path; + return (slash + 1); +} + +static char * +exceed_max_deleted_files (SeafRepo *repo) +{ + SeafBranch *master = NULL, *local = NULL; + SeafCommit *local_head = NULL, *master_head = NULL; + GList *diff_results = NULL; + char *deleted_file = NULL; + GString *desc = NULL; + char *ret = NULL; + + local = seaf_branch_manager_get_branch (seaf->branch_mgr, repo->id, "local"); + if (!local) { + seaf_warning ("No local branch found for repo %s(%.8s).\n", + repo->name, repo->id); + goto out; + } + + master = seaf_branch_manager_get_branch (seaf->branch_mgr, repo->id, "master"); + if (!master) { + seaf_warning ("No master branch found for repo %s(%.8s).\n", + repo->name, repo->id); + goto out; + } + + local_head = seaf_commit_manager_get_commit (seaf->commit_mgr, repo->id, repo->version, + local->commit_id); + if (!local_head) { + seaf_warning ("Failed to get head of local branch for repo %s.\n", repo->id); + goto out; + } + + master_head = seaf_commit_manager_get_commit (seaf->commit_mgr, repo->id, repo->version, + master->commit_id); + if (!master_head) { + seaf_warning ("Failed to get head of master branch for repo %s.\n", repo->id); + goto out; + } + + diff_commit_roots (repo->id, repo->version, master_head->root_id, local_head->root_id, &diff_results, TRUE); + if (!diff_results) { + goto out; + } + GList *p; + DiffEntry *de; + int n_deleted = 0; + + for (p = diff_results; p != NULL; p = p->next) { + de = p->data; + switch (de->status) { + case DIFF_STATUS_DELETED: + if (n_deleted == 0) + deleted_file = get_basename(de->name); + n_deleted++; + break; + } + } + + if (n_deleted >= seaf->delete_confirm_threshold) { + desc = g_string_new (""); + g_string_append_printf (desc, "Deleted \"%s\" and %d more files.\n", + deleted_file, n_deleted - 1); + ret = g_string_free (desc, FALSE); + } + +out: + seaf_branch_unref (local); + seaf_branch_unref (master); + seaf_commit_unref (local_head); + seaf_commit_unref (master_head); + g_list_free_full (diff_results, (GDestroyNotify)diff_entry_free); + + return ret; +} + +static void +notify_delete_confirmation (const char *repo_name, const char *desc, const char *confirmation_id) +{ + json_t *obj = json_object (); + json_object_set_string_member (obj, "repo_name", repo_name); + json_object_set_string_member (obj, "delete_files", desc); + json_object_set_string_member (obj, "confirmation_id", confirmation_id); + + char *msg = json_dumps (obj, JSON_COMPACT); + + seaf_mq_manager_publish_notification (seaf->mq_mgr, "sync.del_confirmation", msg); + + json_decref (obj); + g_free (msg); +} + +int +seaf_sync_manager_add_del_confirmation (SeafSyncManager *mgr, + const char *confirmation_id, + gboolean resync) +{ + SeafSyncManagerPriv *priv = seaf->sync_mgr->priv; + DelConfirmationResult *result = NULL; + + result = g_new0 (DelConfirmationResult, 1); + result->resync = resync; + + pthread_mutex_lock (&priv->del_confirmation_lock); + g_hash_table_insert (priv->del_confirmation_tasks, g_strdup (confirmation_id), result); + pthread_mutex_unlock (&priv->del_confirmation_lock); + + return 0; +} + +static DelConfirmationResult * +get_del_confirmation_result (const char *confirmation_id) +{ + SeafSyncManagerPriv *priv = seaf->sync_mgr->priv; + DelConfirmationResult *result, *copy = NULL; + + + pthread_mutex_lock (&priv->del_confirmation_lock); + result = g_hash_table_lookup (priv->del_confirmation_tasks, confirmation_id); + if (result) { + copy = g_new0 (DelConfirmationResult, 1); + copy->resync = result->resync; + g_hash_table_remove (priv->del_confirmation_tasks, confirmation_id); + } + pthread_mutex_unlock (&priv->del_confirmation_lock); + + return copy; +} + +static void +resync_repo (SeafRepo *repo) +{ + GError *error = NULL; + char *repo_id = g_strdup (repo->id); + int repo_version = repo->version; + char *repo_name = g_strdup (repo->name); + char *token = g_strdup (repo->token); + char *magic = g_strdup (repo->magic); + int enc_version = repo->enc_version; + char *random_key = g_strdup (repo->random_key); + char *worktree = g_strdup (repo->worktree); + char *email = g_strdup (repo->email); + char *more_info = NULL; + json_t *obj = json_object (); + + json_object_set_int_member (obj, "is_readonly", repo->is_readonly); + json_object_set_string_member (obj, "repo_salt", repo->salt); + json_object_set_string_member (obj, "server_url", repo->server_url); + + more_info = json_dumps (obj, 0); + + if (repo->auto_sync && (repo->sync_interval == 0)) + seaf_wt_monitor_unwatch_repo (seaf->wt_monitor, repo->id); + + seaf_repo_manager_del_repo (seaf->repo_mgr, repo); + + char *ret = seaf_clone_manager_add_task (seaf->clone_mgr, repo_id, + repo_version, repo_name, + token, NULL, + magic, enc_version, + random_key, worktree, + email, more_info, &error); + if (error) { + seaf_warning ("Failed to clone repo %s: %s\n", repo_id, error->message); + g_clear_error (&error); + } + g_free (ret); + g_free (repo_id); + g_free (repo_name); + g_free (token); + g_free (magic); + g_free (random_key); + g_free (worktree); + g_free (email); + json_decref (obj); + g_free (more_info); +} + static int sync_repo_v2 (SeafSyncManager *manager, SeafRepo *repo, gboolean is_manual_sync) { @@ -1224,6 +1442,36 @@ sync_repo_v2 (SeafSyncManager *manager, SeafRepo *repo, gboolean is_manual_sync) if (strcmp (master->commit_id, local->commit_id) != 0) { if (is_manual_sync || can_schedule_repo (manager, repo)) { task = create_sync_task_v2 (manager, repo, is_manual_sync, FALSE); + if (!task->info->del_confirmation_pending) { + char *desc = NULL; + desc = exceed_max_deleted_files (repo); + if (desc) { + notify_delete_confirmation (repo->name, desc, local->commit_id); + seaf_warning ("Delete more than %d files, add delete confirmation.\n", seaf->delete_confirm_threshold); + task->info->del_confirmation_pending = TRUE; + set_task_error (task, SYNC_ERROR_ID_DEL_CONFIRMATION_PENDING); + g_free (desc); + goto out; + } + } else { + DelConfirmationResult *result = get_del_confirmation_result (local->commit_id); + if (!result) { + // User has not confirmed whether to continue syncing. + set_task_error (task, SYNC_ERROR_ID_DEL_CONFIRMATION_PENDING); + goto out; + } else if (result->resync) { + // User chooses to resync. + g_free (result); + task->info->del_confirmation_pending = FALSE; + set_task_error (task, SYNC_ERROR_ID_DEL_CONFIRMATION_PENDING); + // Delete this repo and resync this repo by adding clone task. + resync_repo (repo); + goto out; + } + // User chooes to continue syncing. + g_free (result); + task->info->del_confirmation_pending = FALSE; + } start_upload_if_necessary (task); } /* Do nothing if the client still has something to upload diff --git a/daemon/sync-mgr.h b/daemon/sync-mgr.h index dbc564d3..c5a4786a 100644 --- a/daemon/sync-mgr.h +++ b/daemon/sync-mgr.h @@ -35,6 +35,7 @@ struct _SyncInfo { gboolean end_multipart_upload; gint sync_perm_err_cnt; + gboolean del_confirmation_pending; }; enum { @@ -129,6 +130,11 @@ SyncInfo * seaf_sync_manager_get_sync_info (SeafSyncManager *mgr, const char *repo_id); +int +seaf_sync_manager_add_del_confirmation (SeafSyncManager *mgr, + const char *confirmation_id, + gboolean resync); + int seaf_sync_manager_disable_auto_sync (SeafSyncManager *mgr); diff --git a/include/seafile-error.h b/include/seafile-error.h index b115eca7..b794f0d4 100644 --- a/include/seafile-error.h +++ b/include/seafile-error.h @@ -62,6 +62,7 @@ #define SYNC_ERROR_ID_REMOVE_UNCOMMITTED_FOLDER 30 #define SYNC_ERROR_ID_INVALID_PATH_ON_WINDOWS 31 #define SYNC_ERROR_ID_LIBRARY_TOO_LARGE 32 -#define N_SYNC_ERROR_ID 33 +#define SYNC_ERROR_ID_DEL_CONFIRMATION_PENDING 33 +#define N_SYNC_ERROR_ID 34 #endif diff --git a/include/seafile-rpc.h b/include/seafile-rpc.h index 36d22f68..518fe3aa 100644 --- a/include/seafile-rpc.h +++ b/include/seafile-rpc.h @@ -216,4 +216,7 @@ seafile_shutdown (GError **error); char* seafile_sync_error_id_to_str (int error_id, GError **error); + +int +seafile_add_del_confirmation (const char *confirmation_id, int resync, GError **error); #endif diff --git a/python/seafile/rpcclient.py b/python/seafile/rpcclient.py index 34d34abc..ae5b5abb 100644 --- a/python/seafile/rpcclient.py +++ b/python/seafile/rpcclient.py @@ -211,3 +211,8 @@ class SeafileRpcClient(NamedPipeClient): def seafile_shutdown(): pass shutdown = seafile_shutdown + + @searpc_func("int", ["string", "int"]) + def seafile_add_del_confirmation(key, value): + pass + add_del_confirmation = seafile_add_del_confirmation