/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ #include "common.h" #define DEBUG_FLAG SEAFILE_DEBUG_SYNC #include "log.h" #include "seafile-error-impl.h" #include "seafile-session.h" #include "vc-utils.h" #include "utils.h" #include "seafile-config.h" #include "timer.h" #define CLONE_DB "clone.db" #define CHECK_CONNECT_INTERVAL 5 static void on_repo_http_fetched (SeafileSession *seaf, HttpTxTask *tx_task, SeafCloneManager *mgr); static void transition_state (CloneTask *task, int new_state); static void transition_to_error (CloneTask *task, int error); static int add_transfer_task (CloneTask *task, GError **error); static const char *state_str[] = { "init", "check server", "fetch", "done", "error", "canceling", "canceled", /* States only used by old protocol. */ "connect", "connect", /* Use "connect" for CHECK_PROTOCOL */ "index", "checkout", "merge", }; static void mark_clone_done_v2 (SeafRepo *repo, CloneTask *task) { SeafBranch *local = NULL; seaf_repo_manager_set_repo_worktree (repo->manager, repo, task->worktree); local = seaf_branch_manager_get_branch (seaf->branch_mgr, repo->id, "local"); if (!local) { seaf_warning ("Cannot get branch local for repo %s(%.10s).\n", repo->name, repo->id); transition_to_error (task, SYNC_ERROR_ID_LOCAL_DATA_CORRUPT); return; } /* Set repo head to mark checkout done. */ seaf_repo_set_head (repo, local); seaf_branch_unref (local); if (repo->encrypted) { if (!task->resync_enc_repo) { if (seaf_repo_manager_set_repo_passwd (seaf->repo_mgr, repo, task->passwd) < 0) { seaf_warning ("[Clone mgr] failed to set passwd for %s.\n", repo->id); transition_to_error (task, SYNC_ERROR_ID_GENERAL_ERROR); return; } } else { if (seaf_repo_manager_load_repo_enc_info (seaf->repo_mgr, repo) < 0) { seaf_warning ("[Clone mgr] failed to set enc info for %s.\n", repo->id); transition_to_error (task, SYNC_ERROR_ID_GENERAL_ERROR); return; } } } if (task->is_readonly) { seaf_repo_set_readonly (repo); } if (task->sync_wt_name) { seaf_repo_manager_set_repo_property (seaf->repo_mgr, repo->id, REPO_SYNC_WORKTREE_NAME, "true"); } if (task->server_url) repo->server_url = g_strdup(task->server_url); if (repo->auto_sync && (repo->sync_interval == 0)) { if (seaf_wt_monitor_watch_repo (seaf->wt_monitor, repo->id, repo->worktree) < 0) { seaf_warning ("failed to watch repo %s(%.10s).\n", repo->name, repo->id); transition_to_error (task, SYNC_ERROR_ID_GENERAL_ERROR); return; } } /* For compatibility, still set these two properties. * So that if we downgrade to an old version, the syncing can still work. */ seaf_repo_manager_set_repo_property (seaf->repo_mgr, repo->id, REPO_REMOTE_HEAD, repo->head->commit_id); seaf_repo_manager_set_repo_property (seaf->repo_mgr, repo->id, REPO_LOCAL_HEAD, repo->head->commit_id); transition_state (task, CLONE_STATE_DONE); } static void start_clone_v2 (CloneTask *task) { GError *error = NULL; if (g_access (task->worktree, F_OK) != 0 && checkdir_with_mkdir (task->worktree) < 0) { seaf_warning ("[clone mgr] Failed to create worktree %s.\n", task->worktree); transition_to_error (task, SYNC_ERROR_ID_WRITE_LOCAL_DATA); return; } SeafRepo *repo = seaf_repo_manager_get_repo (seaf->repo_mgr, task->repo_id); if (repo != NULL) { seaf_repo_manager_set_repo_token (seaf->repo_mgr, repo, task->token); seaf_repo_manager_set_repo_email (seaf->repo_mgr, repo, task->email); seaf_repo_manager_set_repo_relay_info (seaf->repo_mgr, repo->id, task->peer_addr, task->peer_port); if (task->server_url) { seaf_repo_manager_set_repo_property (seaf->repo_mgr, repo->id, REPO_PROP_SERVER_URL, task->server_url); } if (task->username) { seaf_repo_manager_set_repo_property (seaf->repo_mgr, repo->id, REPO_PROP_USERNAME, task->username); } mark_clone_done_v2 (repo, task); return; } if (add_transfer_task (task, &error) == 0) transition_state (task, CLONE_STATE_FETCH); else transition_to_error (task, SYNC_ERROR_ID_NOT_ENOUGH_MEMORY); } static void check_head_commit_done (HttpHeadCommit *result, void *user_data) { CloneTask *task = user_data; if (task->state == CLONE_STATE_CANCEL_PENDING) { transition_state (task, CLONE_STATE_CANCELED); return; } if (result->check_success && !result->is_corrupt && !result->is_deleted) { memcpy (task->server_head_id, result->head_commit, 40); start_clone_v2 (task); } else { transition_to_error (task, result->error_code); } } static void http_check_head_commit (CloneTask *task) { int ret = http_tx_manager_check_head_commit (seaf->http_tx_mgr, task->repo_id, task->repo_version, task->effective_url, task->token, task->use_fileserver_port, check_head_commit_done, task); if (ret < 0) transition_to_error (task, SYNC_ERROR_ID_NOT_ENOUGH_MEMORY); } static char * http_fileserver_url (const char *url) { const char *host; char *colon; char *url_no_port; char *ret = NULL; /* Just return the url itself if it's invalid. */ if (strlen(url) <= strlen("http://")) return g_strdup(url); /* Skip protocol schem. */ host = url + strlen("http://"); colon = strrchr (host, ':'); if (colon) { url_no_port = g_strndup(url, colon - url); ret = g_strconcat(url_no_port, ":8082", NULL); g_free (url_no_port); } else { ret = g_strconcat(url, ":8082", NULL); } return ret; } static void check_http_fileserver_protocol_done (HttpProtocolVersion *result, void *user_data) { CloneTask *task = user_data; if (task->state == CLONE_STATE_CANCEL_PENDING) { transition_state (task, CLONE_STATE_CANCELED); return; } if (result->check_success && !result->not_supported) { task->http_protocol_version = result->version; task->effective_url = http_fileserver_url (task->server_url); task->use_fileserver_port = TRUE; http_check_head_commit (task); } else { /* Wait for periodic retry. */ transition_to_error (task, result->error_code); } } static void check_http_protocol_done (HttpProtocolVersion *result, void *user_data) { CloneTask *task = user_data; if (task->state == CLONE_STATE_CANCEL_PENDING) { transition_state (task, CLONE_STATE_CANCELED); return; } if (result->check_success && !result->not_supported) { task->http_protocol_version = result->version; task->effective_url = g_strdup(task->server_url); http_check_head_commit (task); } else if (strncmp(task->server_url, "https", 5) != 0) { char *host_fileserver = http_fileserver_url(task->server_url); if (http_tx_manager_check_protocol_version (seaf->http_tx_mgr, host_fileserver, TRUE, check_http_fileserver_protocol_done, task) < 0) transition_to_error (task, SYNC_ERROR_ID_NOT_ENOUGH_MEMORY); g_free (host_fileserver); } else { /* Wait for periodic retry. */ transition_to_error (task, result->error_code); } } static void check_http_protocol (CloneTask *task) { if (http_tx_manager_check_protocol_version (seaf->http_tx_mgr, task->server_url, FALSE, check_http_protocol_done, task) < 0) { transition_to_error (task, SYNC_ERROR_ID_NOT_ENOUGH_MEMORY); return; } transition_state (task, CLONE_STATE_CHECK_SERVER); } static CloneTask * clone_task_new (const char *repo_id, const char *repo_name, const char *token, const char *worktree, const char *passwd, const char *email) { CloneTask *task = g_new0 (CloneTask, 1); memcpy (task->repo_id, repo_id, 37); task->token = g_strdup (token); task->worktree = g_strdup(worktree); task->email = g_strdup(email); if (repo_name) task->repo_name = g_strdup(repo_name); if (passwd) task->passwd = g_strdup (passwd); task->error = SYNC_ERROR_ID_NO_ERROR; return task; } static void clone_task_free (CloneTask *task) { g_free (task->tx_id); g_free (task->worktree); g_free (task->passwd); g_free (task->token); g_free (task->repo_name); g_free (task->peer_addr); g_free (task->peer_port); g_free (task->email); g_free (task->username); g_free (task->random_key); g_free (task->server_url); g_free (task->effective_url); g_free (task); } const char * clone_task_state_to_str (int state) { if (state < 0 || state >= N_CLONE_STATES) return NULL; return state_str[state]; } SeafCloneManager * seaf_clone_manager_new (SeafileSession *session) { SeafCloneManager *mgr = g_new0 (SeafCloneManager, 1); char *db_path = g_build_path ("/", session->seaf_dir, CLONE_DB, NULL); if (sqlite_open_db (db_path, &mgr->db) < 0) { g_critical ("[Clone mgr] Failed to open db\n"); g_free (db_path); g_free (mgr); return NULL; } mgr->seaf = session; mgr->tasks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)clone_task_free); return mgr; } static gboolean load_enc_info_cb (sqlite3_stmt *stmt, void *data) { CloneTask *task = data; int enc_version; const char *random_key; enc_version = sqlite3_column_int (stmt, 0); random_key = (const char *)sqlite3_column_text (stmt, 1); task->enc_version = enc_version; task->random_key = g_strdup (random_key); return FALSE; } static int load_clone_enc_info (CloneTask *task) { char sql[256]; snprintf (sql, sizeof(sql), "SELECT enc_version, random_key FROM CloneEncInfo WHERE repo_id='%s'", task->repo_id); if (sqlite_foreach_selected_row (task->manager->db, sql, load_enc_info_cb, task) < 0) return -1; return 0; } static gboolean load_version_info_cb (sqlite3_stmt *stmt, void *data) { CloneTask *task = data; int repo_version; repo_version = sqlite3_column_int (stmt, 0); task->repo_version = repo_version; return FALSE; } static void load_clone_repo_version_info (CloneTask *task) { char sql[256]; snprintf (sql, sizeof(sql), "SELECT repo_version FROM CloneVersionInfo WHERE repo_id='%s'", task->repo_id); sqlite_foreach_selected_row (task->manager->db, sql, load_version_info_cb, task); } static gboolean load_more_info_cb (sqlite3_stmt *stmt, void *data) { CloneTask *task = data; json_error_t jerror; json_t *object = NULL; const char *more_info; more_info = (const char *)sqlite3_column_text (stmt, 0); object = json_loads (more_info, 0, &jerror); if (!object) { seaf_warning ("Failed to load more sync info from json: %s.\n", jerror.text); return FALSE; } json_t *integer = json_object_get (object, "is_readonly"); task->is_readonly = json_integer_value (integer); json_t *string = json_object_get (object, "server_url"); if (string) task->server_url = g_strdup (json_string_value (string)); json_t *repo_salt = json_object_get (object, "repo_salt"); if (repo_salt) task->repo_salt = g_strdup (json_string_value (repo_salt)); json_t *username = json_object_get (object, "username"); if (username) task->username = g_strdup (json_string_value (username)); json_decref (object); return FALSE; } static void load_clone_more_info (CloneTask *task) { char sql[256]; snprintf (sql, sizeof(sql), "SELECT more_info FROM CloneTasksMoreInfo WHERE repo_id='%s'", task->repo_id); sqlite_foreach_selected_row (task->manager->db, sql, load_more_info_cb, task); } static gboolean restart_task (sqlite3_stmt *stmt, void *data) { SeafCloneManager *mgr = data; const char *repo_id, *repo_name, *token, *worktree, *passwd; const char *email; CloneTask *task; SeafRepo *repo; repo_id = (const char *)sqlite3_column_text (stmt, 0); repo_name = (const char *)sqlite3_column_text (stmt, 1); token = (const char *)sqlite3_column_text (stmt, 2); worktree = (const char *)sqlite3_column_text (stmt, 4); passwd = (const char *)sqlite3_column_text (stmt, 5); email = (const char *)sqlite3_column_text (stmt, 8); task = clone_task_new (repo_id, repo_name, token, worktree, passwd, email); task->manager = mgr; /* Default to 1. */ task->enc_version = 1; if (passwd && load_clone_enc_info (task) < 0) { clone_task_free (task); return TRUE; } task->repo_version = 0; load_clone_repo_version_info (task); load_clone_more_info (task); repo = seaf_repo_manager_get_repo (seaf->repo_mgr, repo_id); if (repo != NULL && repo->head != NULL) { transition_state (task, CLONE_STATE_DONE); return TRUE; } if (task->repo_version > 0) { if (task->server_url) { check_http_protocol (task); } else { transition_to_error (task, SYNC_ERROR_ID_GENERAL_ERROR); return TRUE; } } g_hash_table_insert (mgr->tasks, g_strdup(task->repo_id), task); return TRUE; } int seaf_clone_manager_init (SeafCloneManager *mgr) { const char *sql; sql = "CREATE TABLE IF NOT EXISTS CloneTasks " "(repo_id TEXT PRIMARY KEY, repo_name TEXT, " "token TEXT, dest_id TEXT," "worktree_parent TEXT, passwd TEXT, " "server_addr TEXT, server_port TEXT, email TEXT);"; if (sqlite_query_exec (mgr->db, sql) < 0) return -1; sql = "CREATE TABLE IF NOT EXISTS CloneTasksMoreInfo " "(repo_id TEXT PRIMARY KEY, more_info TEXT);"; if (sqlite_query_exec (mgr->db, sql) < 0) return -1; sql = "CREATE TABLE IF NOT EXISTS CloneEncInfo " "(repo_id TEXT PRIMARY KEY, enc_version INTEGER, random_key TEXT);"; if (sqlite_query_exec (mgr->db, sql) < 0) return -1; sql = "CREATE TABLE IF NOT EXISTS CloneVersionInfo " "(repo_id TEXT PRIMARY KEY, repo_version INTEGER);"; if (sqlite_query_exec (mgr->db, sql) < 0) return -1; sql = "CREATE TABLE IF NOT EXISTS CloneServerURL " "(repo_id TEXT PRIMARY KEY, server_url TEXT);"; if (sqlite_query_exec (mgr->db, sql) < 0) return -1; return 0; } static int check_connect_pulse (void *vmanager) { SeafCloneManager *mgr = vmanager; CloneTask *task; GHashTableIter iter; gpointer key, value; g_hash_table_iter_init (&iter, mgr->tasks); while (g_hash_table_iter_next (&iter, &key, &value)) { task = value; if (task->state == CLONE_STATE_ERROR && task->repo_version > 0 && sync_error_level (task->error) == SYNC_ERROR_LEVEL_NETWORK) { task->error = SYNC_ERROR_ID_NO_ERROR; check_http_protocol (task); } } return TRUE; } int seaf_clone_manager_start (SeafCloneManager *mgr) { mgr->check_timer = seaf_timer_new (check_connect_pulse, mgr, CHECK_CONNECT_INTERVAL * 1000); char *sql = "SELECT * FROM CloneTasks"; if (sqlite_foreach_selected_row (mgr->db, sql, restart_task, mgr) < 0) return -1; g_signal_connect (seaf, "repo-http-fetched", (GCallback)on_repo_http_fetched, mgr); return 0; } static int save_task_to_db (SeafCloneManager *mgr, CloneTask *task) { char *sql; if (task->passwd) sql = sqlite3_mprintf ("REPLACE INTO CloneTasks VALUES " "('%q', '%q', '%q', NULL, '%q', '%q', NULL, NULL, '%q')", task->repo_id, task->repo_name, task->token, task->worktree, task->passwd, task->email); else sql = sqlite3_mprintf ("REPLACE INTO CloneTasks VALUES " "('%q', '%q', '%q', NULL, '%q', NULL, NULL, NULL, '%q')", task->repo_id, task->repo_name, task->token, task->worktree, task->email); if (sqlite_query_exec (mgr->db, sql) < 0) { sqlite3_free (sql); return -1; } sqlite3_free (sql); if (task->passwd && task->enc_version >= 2 && task->random_key) { sql = sqlite3_mprintf ("REPLACE INTO CloneEncInfo VALUES " "('%q', %d, '%q')", task->repo_id, task->enc_version, task->random_key); if (sqlite_query_exec (mgr->db, sql) < 0) { sqlite3_free (sql); return -1; } sqlite3_free (sql); } sql = sqlite3_mprintf ("REPLACE INTO CloneVersionInfo VALUES " "('%q', %d)", task->repo_id, task->repo_version); if (sqlite_query_exec (mgr->db, sql) < 0) { sqlite3_free (sql); return -1; } sqlite3_free (sql); if (task->is_readonly || task->server_url || task->repo_salt || task->username) { /* need to store more info */ json_t *object = NULL; gchar *info = NULL; object = json_object (); json_object_set_new (object, "is_readonly", json_integer (task->is_readonly)); if (task->server_url) json_object_set_new (object, "server_url", json_string(task->server_url)); if (task->repo_salt) json_object_set_new (object, "repo_salt", json_string(task->repo_salt)); if (task->username) json_object_set_new (object, "username", json_string(task->username)); info = json_dumps (object, 0); json_decref (object); sql = sqlite3_mprintf ("REPLACE INTO CloneTasksMoreInfo VALUES " "('%q', '%q')", task->repo_id, info); if (sqlite_query_exec (mgr->db, sql) < 0) { sqlite3_free (sql); g_free (info); return -1; } sqlite3_free (sql); g_free (info); } return 0; } static int remove_task_from_db (SeafCloneManager *mgr, const char *repo_id) { char sql[256]; snprintf (sql, sizeof(sql), "DELETE FROM CloneTasks WHERE repo_id='%s'", repo_id); if (sqlite_query_exec (mgr->db, sql) < 0) return -1; snprintf (sql, sizeof(sql), "DELETE FROM CloneEncInfo WHERE repo_id='%s'", repo_id); if (sqlite_query_exec (mgr->db, sql) < 0) return -1; snprintf (sql, sizeof(sql), "DELETE FROM CloneVersionInfo WHERE repo_id='%s'", repo_id); if (sqlite_query_exec (mgr->db, sql) < 0) return -1; snprintf (sql, sizeof(sql), "DELETE FROM CloneTasksMoreInfo WHERE repo_id='%s'", repo_id); if (sqlite_query_exec (mgr->db, sql) < 0) return -1; return 0; } static void transition_state (CloneTask *task, int new_state) { seaf_message ("Transition clone state for %.8s from [%s] to [%s].\n", task->repo_id, state_str[task->state], state_str[new_state]); if (new_state == CLONE_STATE_DONE || new_state == CLONE_STATE_CANCELED) { /* Remove from db but leave in memory. */ remove_task_from_db (task->manager, task->repo_id); } task->state = new_state; } static void transition_to_error (CloneTask *task, int error) { seaf_message ("Transition clone state for %.8s from [%s] to [error]: %s.\n", task->repo_id, state_str[task->state], sync_error_id_to_str(error)); task->state = CLONE_STATE_ERROR; task->error = error; } static int add_transfer_task (CloneTask *task, GError **error) { int ret = http_tx_manager_add_download (seaf->http_tx_mgr, task->repo_id, task->repo_version, task->effective_url, task->token, task->server_head_id, TRUE, task->passwd, task->worktree, task->http_protocol_version, task->email, task->username, task->use_fileserver_port, task->repo_name, error); if (ret < 0) return -1; task->tx_id = g_strdup(task->repo_id); return 0; } static gboolean is_duplicate_task (SeafCloneManager *mgr, const char *repo_id) { CloneTask *task = g_hash_table_lookup (mgr->tasks, repo_id); if (task != NULL && task->state != CLONE_STATE_DONE && task->state != CLONE_STATE_CANCELED) return TRUE; return FALSE; } static gboolean is_worktree_of_repo (SeafCloneManager *mgr, const char *path) { GList *repos, *ptr; SeafRepo *repo; GHashTableIter iter; gpointer key, value; CloneTask *task; repos = seaf_repo_manager_get_repo_list (seaf->repo_mgr, -1, -1); for (ptr = repos; ptr != NULL; ptr = ptr->next) { repo = ptr->data; if (g_strcmp0 (path, repo->worktree) == 0) { g_list_free (repos); return TRUE; } } g_list_free (repos); g_hash_table_iter_init (&iter, mgr->tasks); while (g_hash_table_iter_next (&iter, &key, &value)) { task = value; if (task->state == CLONE_STATE_DONE || task->state == CLONE_STATE_CANCELED) continue; if (g_strcmp0 (path, task->worktree) == 0) return TRUE; } return FALSE; } static char * try_worktree (const char *worktree) { char *tmp; unsigned int cnt; /* There is a repo name conflict, so we try to add a postfix */ cnt = 1; while (1) { tmp = g_strdup_printf("%s-%d", worktree, cnt++); if (g_access(tmp, F_OK) < 0) { return tmp; } if (cnt == -1U) { /* we have tried too much times, so give up */ g_free(tmp); return NULL; } g_free(tmp); } /* XXX: never reach here */ } static inline void remove_trail_slash (char *path) { int tail = strlen (path) - 1; while (tail >= 0 && (path[tail] == '/' || path[tail] == '\\')) path[tail--] = '\0'; } static char * make_worktree (SeafCloneManager *mgr, const char *worktree, gboolean dry_run, GError **error) { char *wt = g_strdup (worktree); SeafStat st; int rc; char *ret; remove_trail_slash (wt); rc = seaf_stat (wt, &st); if (rc < 0) { ret = wt; return ret; } else if (!S_ISDIR(st.st_mode)) { if (!dry_run) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Invalid local directory"); g_free (wt); return NULL; } ret = try_worktree (wt); g_free (wt); return ret; } /* OK, wt is an existing dir. Let's see if it's the worktree for * another repo. */ if (is_worktree_of_repo (mgr, wt)) { if (!dry_run) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Already in sync"); g_free (wt); return NULL; } ret = try_worktree (wt); g_free (wt); } else { return wt; } return ret; } /* * Generate a conflict-free path to be used as worktree. * This worktree path can be used as the @worktree parameter * for seaf_clone_manager_add_task(). */ char * seaf_clone_manager_gen_default_worktree (SeafCloneManager *mgr, const char *worktree_parent, const char *repo_name) { char *wt = g_build_filename (worktree_parent, repo_name, NULL); char *worktree; worktree = make_worktree (mgr, wt, TRUE, NULL); if (!worktree) return wt; g_free (wt); return worktree; } inline static gboolean is_separator (char c) { return (c == '/' || c == '\\'); } /* * Returns < 0 if dira includes dirb or dira == dirb; * Returns 0 if no inclusive relationship; * Returns > 0 if dirb includes dira. */ static int check_dir_inclusiveness (const char *dira, const char *dirb) { char *a, *b; char *p1, *p2; int ret = 0; a = g_strdup(dira); b = g_strdup(dirb); remove_trail_slash (a); remove_trail_slash (b); p1 = a; p2 = b; while (*p1 != 0 && *p2 != 0) { /* Go to the last one in a path separator sequence. */ while (is_separator(*p1) && is_separator(p1[1])) ++p1; while (is_separator(*p2) && is_separator(p2[1])) ++p2; if (!(is_separator(*p1) && is_separator(*p2)) && *p1 != *p2) goto out; ++p1; ++p2; } /* Example: * p1 * a: /abc/def/ghi * p2 * b: /abc/def */ if (*p1 == 0 && *p2 == 0) ret = -1; else if (*p1 != 0 && is_separator(*p1)) ret = 1; else if (*p2 != 0 && is_separator(*p2)) ret = -1; out: g_free (a); g_free (b); return ret; } gboolean seaf_clone_manager_check_worktree_path (SeafCloneManager *mgr, const char *path, GError **error) { GList *repos, *ptr; SeafRepo *repo; GHashTableIter iter; gpointer key, value; CloneTask *task; if (check_dir_inclusiveness (path, seaf->seaf_dir) != 0 || /* It's OK if path is included by the default worktree parent. */ check_dir_inclusiveness (path, seaf->worktree_dir) < 0 || check_dir_inclusiveness (path, seaf->ccnet_dir) != 0) { seaf_warning ("Worktree path conflicts with seafile system path.\n"); g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Worktree conflicts system path"); return FALSE; } repos = seaf_repo_manager_get_repo_list (seaf->repo_mgr, -1, -1); for (ptr = repos; ptr != NULL; ptr = ptr->next) { repo = ptr->data; if (repo->worktree != NULL && check_dir_inclusiveness (path, repo->worktree) != 0) { seaf_warning ("Worktree path conflict with repo %s.\n", repo->name); g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Worktree conflicts existing repo"); g_list_free (repos); return FALSE; } } g_list_free (repos); g_hash_table_iter_init (&iter, mgr->tasks); while (g_hash_table_iter_next (&iter, &key, &value)) { task = value; if (task->state == CLONE_STATE_DONE || task->state == CLONE_STATE_CANCELED) continue; if (check_dir_inclusiveness (path, task->worktree) != 0) { seaf_warning ("Worktree path conflict with clone %.8s.\n", task->repo_id); g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Worktree conflicts existing repo"); return FALSE; } } return TRUE; } static char * add_task_common (SeafCloneManager *mgr, const char *repo_id, int repo_version, const char *repo_name, const char *token, const char *passwd, int enc_version, const char *random_key, const char *worktree, const char *email, const char *more_info, gboolean sync_wt_name, GError **error) { CloneTask *task; task = clone_task_new (repo_id, repo_name, token, worktree, passwd, email); task->manager = mgr; task->enc_version = enc_version; task->random_key = g_strdup (random_key); task->repo_version = repo_version; task->sync_wt_name = sync_wt_name; if (more_info) { json_error_t jerror; json_t *object = NULL; object = json_loads (more_info, 0, &jerror); if (!object) { seaf_warning ("Failed to load more sync info from json: %s.\n", jerror.text); clone_task_free (task); return NULL; } json_t *integer = json_object_get (object, "is_readonly"); task->is_readonly = json_integer_value (integer); json_t *string = json_object_get (object, "server_url"); if (string) task->server_url = canonical_server_url (json_string_value (string)); json_t *repo_salt = json_object_get (object, "repo_salt"); if (repo_salt) task->repo_salt = g_strdup (json_string_value (repo_salt)); json_t *username = json_object_get (object, "username"); if (username) task->username = g_strdup (json_string_value (username)); integer = json_object_get (object, "resync_enc_repo"); task->resync_enc_repo = json_integer_value (integer); json_decref (object); } if (save_task_to_db (mgr, task) < 0) { seaf_warning ("[Clone mgr] failed to save task.\n"); clone_task_free (task); return NULL; } if (task->repo_version > 0) { if (task->server_url) { check_http_protocol (task); } else { clone_task_free (task); return NULL; } } /* The old task for this repo will be freed. */ g_hash_table_insert (mgr->tasks, g_strdup(task->repo_id), task); return g_strdup(repo_id); } static gboolean check_encryption_args (const char *magic, int enc_version, const char *random_key, const char *repo_salt, GError **error) { if (!magic) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Magic must be specified"); return FALSE; } if (enc_version != 1 && enc_version != 2 && enc_version != 3 && enc_version != 4) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Unsupported enc version"); return FALSE; } if (enc_version >= 2) { if (!random_key || strlen(random_key) != 96) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Random key not specified"); return FALSE; } if (enc_version >= 3 && (!(repo_salt) || strlen(repo_salt) != 64) ) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Repo salt not specified"); return FALSE; } } return TRUE; } static gboolean check_pwd_hash_encryption_args (const char *pwd_hash, int enc_version, const char *random_key, const char *repo_salt, GError **error) { if (!pwd_hash) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Password hash must be specified"); return FALSE; } if (enc_version != 1 && enc_version != 2 && enc_version != 3 && enc_version != 4) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Unsupported enc version"); return FALSE; } if (enc_version >= 2) { if (!random_key || strlen(random_key) != 96) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Random key not specified"); return FALSE; } if (enc_version >= 3 && (!(repo_salt) || strlen(repo_salt) != 64) ) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Repo salt not specified"); return FALSE; } } return TRUE; } static gboolean is_wt_repo_name_same (const char *worktree, const char *repo_name) { char *basename = g_path_get_basename (worktree); gboolean ret = FALSE; ret = (strcmp (basename, repo_name) == 0); g_free (basename); return ret; } char * seaf_clone_manager_add_task (SeafCloneManager *mgr, const char *repo_id, int repo_version, const char *repo_name, const char *token, const char *passwd, const char *magic, int enc_version, const char *random_key, const char *worktree_in, const char *email, const char *more_info, GError **error) { SeafRepo *repo = NULL; char *worktree = NULL; char *ret = NULL; gboolean sync_wt_name = FALSE; char *repo_salt = NULL; char *algo = NULL; char *params = NULL; char *pwd_hash = NULL; gboolean resync_enc_repo = FALSE; if (!seaf->started) { seaf_message ("System not started, skip adding clone task.\n"); goto out; } #ifdef USE_GPL_CRYPTO if (repo_version == 0 || (passwd && enc_version < 2)) { seaf_warning ("Don't support syncing old version libraries.\n"); g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Don't support syncing old version libraries"); goto out; } #endif if (more_info) { json_error_t jerror; json_t *object; object = json_loads (more_info, 0, &jerror); if (!object) { seaf_warning ("Failed to load more sync info from json: %s.\n", jerror.text); goto out; } json_t *string = json_object_get (object, "repo_salt"); if (string) repo_salt = g_strdup (json_string_value (string)); json_t *integer = json_object_get (object, "resync_enc_repo"); resync_enc_repo = json_integer_value (integer); string = json_object_get (object, "pwd_hash_algo"); if (string) algo = g_strdup (json_string_value (string)); string = json_object_get (object, "pwd_hash_params"); if (string) params = g_strdup (json_string_value (string)); string = json_object_get (object, "pwd_hash"); if (string) pwd_hash = g_strdup (json_string_value (string)); json_decref (object); } if (passwd && algo && !check_pwd_hash_encryption_args (pwd_hash, enc_version, random_key, repo_salt, error)) { goto out; } else if (passwd && !check_encryption_args (magic, enc_version, random_key, repo_salt, error)) { goto out; } /* After a repo was unsynced, the sync task may still be blocked in the * network, so the repo is not actually deleted yet. * In this case just return an error to the user. */ SyncInfo *sync_info = seaf_sync_manager_get_sync_info (seaf->sync_mgr, repo_id); if (sync_info && sync_info->in_sync) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Repo already exists"); goto out; } repo = seaf_repo_manager_get_repo (seaf->repo_mgr, repo_id); if (repo != NULL && repo->head != NULL) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Repo already exists"); goto out; } if (is_duplicate_task (mgr, repo_id)) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Task is already in progress"); goto out; } if (passwd && algo) { if (seafile_pwd_hash_verify_repo_passwd(enc_version, repo_id, passwd, repo_salt, pwd_hash, algo, params) < 0) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Incorrect password"); goto out; } } else if (passwd && seafile_verify_repo_passwd(repo_id, passwd, magic, enc_version, repo_salt) < 0) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Incorrect password"); goto out; } if (!seaf_clone_manager_check_worktree_path (mgr, worktree_in, error)) goto out; /* Return error if worktree_in conflicts with another repo or * is not a directory. */ worktree = make_worktree (mgr, worktree_in, FALSE, error); if (!worktree) { goto out; } /* Don't sync worktree folder name with library name later if they're not the same * at the beginning. */ sync_wt_name = is_wt_repo_name_same (worktree, repo_name); /* If a repo was unsynced and then downloaded again, there may be * a garbage record for this repo. We don't want the downloaded blocks * be removed by GC. */ if (repo_version > 0) seaf_repo_manager_remove_garbage_repo (seaf->repo_mgr, repo_id); /* Delete orphan information in the db in case the repo was corrupt. */ if (!repo && !resync_enc_repo) seaf_repo_manager_remove_repo_ondisk (seaf->repo_mgr, repo_id, FALSE); ret = add_task_common (mgr, repo_id, repo_version, repo_name, token, passwd, enc_version, random_key, worktree, email, more_info, sync_wt_name, error); out: g_free (worktree); g_free (repo_salt); g_free (algo); g_free (params); g_free (pwd_hash); return ret; } static char * make_worktree_for_download (SeafCloneManager *mgr, const char *wt_tmp, GError **error) { char *worktree; if (g_access (wt_tmp, F_OK) == 0) { worktree = try_worktree (wt_tmp); } else { worktree = g_strdup(wt_tmp); } if (!seaf_clone_manager_check_worktree_path (mgr, worktree, error)) { g_free (worktree); return NULL; } return worktree; } char * seaf_clone_manager_add_download_task (SeafCloneManager *mgr, const char *repo_id, int repo_version, const char *repo_name, const char *token, const char *passwd, const char *magic, int enc_version, const char *random_key, const char *wt_parent, const char *email, const char *more_info, GError **error) { SeafRepo *repo = NULL; char *wt_tmp = NULL; char *worktree = NULL; char *ret = NULL; char *repo_salt = NULL; char *algo = NULL; char *params = NULL; char *pwd_hash = NULL; if (!seaf->started) { seaf_message ("System not started, skip adding clone task.\n"); goto out; } #ifdef USE_GPL_CRYPTO if (repo_version == 0 || (passwd && enc_version < 2)) { seaf_warning ("Don't support syncing old version libraries.\n"); g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Don't support syncing old version libraries"); goto out; } #endif if (more_info) { json_error_t jerror; json_t *object; object = json_loads (more_info, 0, &jerror); if (!object) { seaf_warning ("Failed to load more sync info from json: %s.\n", jerror.text); goto out; } json_t *string = json_object_get (object, "repo_salt"); if (string) repo_salt = g_strdup (json_string_value (string)); string = json_object_get (object, "pwd_hash_algo"); if (string) algo = g_strdup (json_string_value (string)); string = json_object_get (object, "pwd_hash_params"); if (string) params = g_strdup (json_string_value (string)); string = json_object_get (object, "pwd_hash"); if (string) pwd_hash = g_strdup (json_string_value (string)); json_decref (object); } if (passwd && algo && !check_pwd_hash_encryption_args (pwd_hash, enc_version, random_key, repo_salt, error)) { goto out; } else if (passwd && !check_encryption_args (magic, enc_version, random_key, repo_salt, error)) { goto out; } /* After a repo was unsynced, the sync task may still be blocked in the * network, so the repo is not actually deleted yet. * In this case just return an error to the user. */ SyncInfo *sync_info = seaf_sync_manager_get_sync_info (seaf->sync_mgr, repo_id); if (sync_info && sync_info->in_sync) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Repo already exists"); goto out; } repo = seaf_repo_manager_get_repo (seaf->repo_mgr, repo_id); if (repo != NULL && repo->head != NULL) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Repo already exists"); goto out; } if (is_duplicate_task (mgr, repo_id)) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Task is already in progress"); goto out; } if (passwd && algo) { if (seafile_pwd_hash_verify_repo_passwd(enc_version, repo_id, passwd, repo_salt, pwd_hash, algo, params) < 0) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Incorrect password"); goto out; } } else if (passwd && seafile_verify_repo_passwd(repo_id, passwd, magic, enc_version, repo_salt) < 0) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Incorrect password"); goto out; } IgnoreReason reason; if (should_ignore_on_checkout (repo_name, &reason)) { if (reason == IGNORE_REASON_END_SPACE_PERIOD) g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Library name ends with space or period character"); else g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Library name contains invalid characters such as ':', '*', '|', '?'"); goto out; } wt_tmp = g_build_filename (wt_parent, repo_name, NULL); worktree = make_worktree_for_download (mgr, wt_tmp, error); if (!worktree) { goto out; } /* If a repo was unsynced and then downloaded again, there may be * a garbage record for this repo. We don't want the downloaded blocks * be removed by GC. */ if (repo_version > 0) seaf_repo_manager_remove_garbage_repo (seaf->repo_mgr, repo_id); /* Delete orphan information in the db in case the repo was corrupt. */ if (!repo) seaf_repo_manager_remove_repo_ondisk (seaf->repo_mgr, repo_id, FALSE); ret = add_task_common (mgr, repo_id, repo_version, repo_name, token, passwd, enc_version, random_key, worktree, email, more_info, TRUE, error); out: g_free (worktree); g_free (wt_tmp); g_free (repo_salt); g_free (algo); g_free (params); g_free (pwd_hash); return ret; } int seaf_clone_manager_cancel_task (SeafCloneManager *mgr, const char *repo_id) { CloneTask *task; if (!seaf->started) { seaf_message ("System not started, skip canceling clone task.\n"); return -1; } task = g_hash_table_lookup (mgr->tasks, repo_id); if (!task) return -1; switch (task->state) { case CLONE_STATE_INIT: case CLONE_STATE_CONNECT: case CLONE_STATE_ERROR: transition_state (task, CLONE_STATE_CANCELED); break; case CLONE_STATE_CHECK_SERVER: transition_state (task, CLONE_STATE_CANCEL_PENDING); case CLONE_STATE_FETCH: http_tx_manager_cancel_task (seaf->http_tx_mgr, task->repo_id, HTTP_TASK_TYPE_DOWNLOAD); transition_state (task, CLONE_STATE_CANCEL_PENDING); break; case CLONE_STATE_INDEX: case CLONE_STATE_CHECKOUT: case CLONE_STATE_MERGE: case CLONE_STATE_CHECK_PROTOCOL: /* We cannot cancel an in-progress checkout, just * wait until it finishes. */ transition_state (task, CLONE_STATE_CANCEL_PENDING); break; case CLONE_STATE_CANCEL_PENDING: break; default: seaf_warning ("[Clone mgr] cannot cancel a not-running task.\n"); return -1; } return 0; } CloneTask * seaf_clone_manager_get_task (SeafCloneManager *mgr, const char *repo_id) { return (CloneTask *) g_hash_table_lookup (mgr->tasks, repo_id); } GList * seaf_clone_manager_get_tasks (SeafCloneManager *mgr) { return g_hash_table_get_values (mgr->tasks); } static void check_folder_permissions (CloneTask *task); static void on_repo_http_fetched (SeafileSession *seaf, HttpTxTask *tx_task, SeafCloneManager *mgr) { CloneTask *task; /* Only handle clone task. */ if (!tx_task->is_clone) return; task = g_hash_table_lookup (mgr->tasks, tx_task->repo_id); g_return_if_fail (task != NULL); if (tx_task->state == HTTP_TASK_STATE_CANCELED) { /* g_assert (task->state == CLONE_STATE_CANCEL_PENDING); */ transition_state (task, CLONE_STATE_CANCELED); return; } else if (tx_task->state == HTTP_TASK_STATE_ERROR) { transition_to_error (task, tx_task->error); return; } SeafRepo *repo = seaf_repo_manager_get_repo (seaf->repo_mgr, tx_task->repo_id); if (repo == NULL) { seaf_warning ("[Clone mgr] cannot find repo %s after fetched.\n", tx_task->repo_id); transition_to_error (task, SYNC_ERROR_ID_LOCAL_DATA_CORRUPT); return; } seaf_repo_manager_set_repo_token (seaf->repo_mgr, repo, task->token); seaf_repo_manager_set_repo_email (seaf->repo_mgr, repo, task->email); seaf_repo_manager_set_repo_relay_info (seaf->repo_mgr, repo->id, task->peer_addr, task->peer_port); if (task->server_url) { seaf_repo_manager_set_repo_property (seaf->repo_mgr, repo->id, REPO_PROP_SERVER_URL, task->server_url); } if (task->username) { seaf_repo_manager_set_repo_property (seaf->repo_mgr, repo->id, REPO_PROP_USERNAME, task->username); } check_folder_permissions (task); } static void check_folder_perms_done (HttpFolderPerms *result, void *user_data) { CloneTask *task = user_data; GList *ptr; HttpFolderPermRes *res; SeafRepo *repo = seaf_repo_manager_get_repo (seaf->repo_mgr, task->repo_id); if (repo == NULL) { seaf_warning ("[Clone mgr] cannot find repo %s after fetched.\n", task->repo_id); transition_to_error (task, SYNC_ERROR_ID_LOCAL_DATA_CORRUPT); return; } if (!result->success) { goto out; } for (ptr = result->results; ptr; ptr = ptr->next) { res = ptr->data; seaf_repo_manager_update_folder_perms (seaf->repo_mgr, res->repo_id, FOLDER_PERM_TYPE_USER, res->user_perms); seaf_repo_manager_update_folder_perms (seaf->repo_mgr, res->repo_id, FOLDER_PERM_TYPE_GROUP, res->group_perms); seaf_repo_manager_update_folder_perm_timestamp (seaf->repo_mgr, res->repo_id, res->timestamp); } out: mark_clone_done_v2 (repo, task); } static void check_folder_permissions (CloneTask *task) { SeafRepo *repo = NULL; HttpFolderPermReq *req; GList *requests = NULL; repo = seaf_repo_manager_get_repo (seaf->repo_mgr, task->repo_id); if (repo == NULL) { seaf_warning ("[Clone mgr] cannot find repo %s after fetched.\n", task->repo_id); transition_to_error (task, SYNC_ERROR_ID_LOCAL_DATA_CORRUPT); return; } if (!seaf_repo_manager_server_is_pro (seaf->repo_mgr, task->server_url)) { mark_clone_done_v2 (repo, task); return; } req = g_new0 (HttpFolderPermReq, 1); memcpy (req->repo_id, task->repo_id, 36); req->token = g_strdup(task->token); req->timestamp = 0; requests = g_list_append (requests, req); /* The requests list will be freed in http tx manager. */ if (http_tx_manager_get_folder_perms (seaf->http_tx_mgr, task->effective_url, task->use_fileserver_port, requests, check_folder_perms_done, task) < 0) transition_to_error (task, SYNC_ERROR_ID_NOT_ENOUGH_MEMORY); }