/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ #include "common.h" #include #ifdef WIN32 #include #include #endif #include #include "utils.h" #define DEBUG_FLAG SEAFILE_DEBUG_SYNC #include "log.h" #include "vc-utils.h" #include "seafile-session.h" #include "seafile-config.h" #include "commit-mgr.h" #include "branch-mgr.h" #include "repo-mgr.h" #include "fs-mgr.h" #include "seafile-error.h" #include "seafile-crypt.h" #include "index/index.h" #include "index/cache-tree.h" #include "diff-simple.h" #include "change-set.h" #include "db.h" #include "seafile-object.h" #define INDEX_DIR "index" #define IGNORE_FILE "seafile-ignore.txt" #ifdef HAVE_KEYSTORAGE_GK #include "repokey/seafile-gnome-keyring.h" #endif // HAVE_KEYSTORAGE_GK #ifndef SEAFILE_CLIENT_VERSION #define SEAFILE_CLIENT_VERSION PACKAGE_VERSION #endif struct _SeafRepoManagerPriv { GHashTable *repo_hash; sqlite3 *db; pthread_mutex_t db_lock; GHashTable *checkout_tasks_hash; pthread_rwlock_t lock; GHashTable *user_perms; /* repo_id -> folder user perms */ GHashTable *group_perms; /* repo_id -> folder group perms */ pthread_mutex_t perm_lock; GAsyncQueue *lock_office_job_queue; // sync_errors is used to record sync errors for which notifications have been sent to avoid repeated notifications of the same error. GList *sync_errors; pthread_mutex_t errors_lock; GHashTable* block_map_cache_table; pthread_rwlock_t block_map_lock; }; static const char *ignore_table[] = { /* tmp files under Linux */ "*~", /* Seafile's backup file */ "*.sbak", /* Emacs tmp files */ "#*#", /* windows image cache */ "Thumbs.db", /* For Mac */ ".DS_Store", "._*", NULL, }; #define CONFLICT_PATTERN " \\(SFConflict .+\\)" #define OFFICE_LOCK_PATTERN "~\\$(.+)$" static GPatternSpec** ignore_patterns; static GPatternSpec* office_temp_ignore_patterns[4]; static GRegex *conflict_pattern = NULL; static GRegex *office_lock_pattern = NULL; static SeafRepo * load_repo (SeafRepoManager *manager, const char *repo_id); static void load_repos (SeafRepoManager *manager, const char *seaf_dir); static void seaf_repo_manager_del_repo_property (SeafRepoManager *manager, const char *repo_id); static int save_branch_repo_map (SeafRepoManager *manager, SeafBranch *branch); static void save_repo_property (SeafRepoManager *manager, const char *repo_id, const char *key, const char *value); static void locked_file_free (LockedFile *file) { if (!file) return; g_free (file->operation); g_free (file); } static gboolean load_locked_file (sqlite3_stmt *stmt, void *data) { GHashTable *ret = data; LockedFile *file; const char *path, *operation, *file_id; gint64 old_mtime; path = (const char *)sqlite3_column_text (stmt, 0); operation = (const char *)sqlite3_column_text (stmt, 1); old_mtime = sqlite3_column_int64 (stmt, 2); file_id = (const char *)sqlite3_column_text (stmt, 3); file = g_new0 (LockedFile, 1); file->operation = g_strdup(operation); file->old_mtime = old_mtime; if (file_id) memcpy (file->file_id, file_id, 40); g_hash_table_insert (ret, g_strdup(path), file); return TRUE; } LockedFileSet * seaf_repo_manager_get_locked_file_set (SeafRepoManager *mgr, const char *repo_id) { GHashTable *locked_files = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)locked_file_free); char sql[256]; sqlite3_snprintf (sizeof(sql), sql, "SELECT path, operation, old_mtime, file_id FROM LockedFiles " "WHERE repo_id = '%q'", repo_id); pthread_mutex_lock (&mgr->priv->db_lock); /* Ingore database error. We return an empty set on error. */ sqlite_foreach_selected_row (mgr->priv->db, sql, load_locked_file, locked_files); pthread_mutex_unlock (&mgr->priv->db_lock); LockedFileSet *ret = g_new0 (LockedFileSet, 1); ret->mgr = mgr; memcpy (ret->repo_id, repo_id, 36); ret->locked_files = locked_files; return ret; } void locked_file_set_free (LockedFileSet *fset) { if (!fset) return; g_hash_table_destroy (fset->locked_files); g_free (fset); } int locked_file_set_add_update (LockedFileSet *fset, const char *path, const char *operation, gint64 old_mtime, const char *file_id) { SeafRepoManager *mgr = fset->mgr; char *sql; sqlite3_stmt *stmt; LockedFile *file; gboolean exists; exists = (g_hash_table_lookup (fset->locked_files, path) != NULL); pthread_mutex_lock (&mgr->priv->db_lock); if (!exists) { seaf_debug ("New locked file record %.8s, %s, %s, %" G_GINT64_FORMAT".\n", fset->repo_id, path, operation, old_mtime); sql = "INSERT INTO LockedFiles VALUES (?, ?, ?, ?, ?, NULL)"; stmt = sqlite_query_prepare (mgr->priv->db, sql); sqlite3_bind_text (stmt, 1, fset->repo_id, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 2, path, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 3, operation, -1, SQLITE_TRANSIENT); sqlite3_bind_int64 (stmt, 4, old_mtime); sqlite3_bind_text (stmt, 5, file_id, -1, SQLITE_TRANSIENT); if (sqlite3_step (stmt) != SQLITE_DONE) { seaf_warning ("Failed to insert locked file %s to db: %s.\n", path, sqlite3_errmsg (mgr->priv->db)); sqlite3_finalize (stmt); pthread_mutex_unlock (&mgr->priv->db_lock); return -1; } sqlite3_finalize (stmt); file = g_new0 (LockedFile, 1); file->operation = g_strdup(operation); file->old_mtime = old_mtime; if (file_id) memcpy (file->file_id, file_id, 40); g_hash_table_insert (fset->locked_files, g_strdup(path), file); } else { seaf_debug ("Update locked file record %.8s, %s, %s.\n", fset->repo_id, path, operation); /* If a UPDATE record exists, don't update the old_mtime. * We need to keep the old mtime when the locked file was first detected. */ sql = "UPDATE LockedFiles SET operation = ?, file_id = ? " "WHERE repo_id = ? AND path = ?"; stmt = sqlite_query_prepare (mgr->priv->db, sql); sqlite3_bind_text (stmt, 1, operation, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 2, file_id, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 3, fset->repo_id, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 4, path, -1, SQLITE_TRANSIENT); if (sqlite3_step (stmt) != SQLITE_DONE) { seaf_warning ("Failed to update locked file %s to db: %s.\n", path, sqlite3_errmsg (mgr->priv->db)); sqlite3_finalize (stmt); pthread_mutex_unlock (&mgr->priv->db_lock); return -1; } sqlite3_finalize (stmt); file = g_hash_table_lookup (fset->locked_files, path); g_free (file->operation); file->operation = g_strdup(operation); if (file_id) memcpy (file->file_id, file_id, 40); } pthread_mutex_unlock (&mgr->priv->db_lock); return 0; } int locked_file_set_remove (LockedFileSet *fset, const char *path, gboolean db_only) { SeafRepoManager *mgr = fset->mgr; char *sql; sqlite3_stmt *stmt; if (g_hash_table_lookup (fset->locked_files, path) == NULL) return 0; seaf_debug ("Remove locked file record %.8s, %s.\n", fset->repo_id, path); pthread_mutex_lock (&mgr->priv->db_lock); sql = "DELETE FROM LockedFiles WHERE repo_id = ? AND path = ?"; stmt = sqlite_query_prepare (mgr->priv->db, sql); sqlite3_bind_text (stmt, 1, fset->repo_id, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 2, path, -1, SQLITE_TRANSIENT); if (sqlite3_step (stmt) != SQLITE_DONE) { seaf_warning ("Failed to remove locked file %s from db: %s.\n", path, sqlite3_errmsg (mgr->priv->db)); sqlite3_finalize (stmt); pthread_mutex_unlock (&mgr->priv->db_lock); return -1; } sqlite3_finalize (stmt); pthread_mutex_unlock (&mgr->priv->db_lock); if (!db_only) g_hash_table_remove (fset->locked_files, path); return 0; } LockedFile * locked_file_set_lookup (LockedFileSet *fset, const char *path) { return (LockedFile *) g_hash_table_lookup (fset->locked_files, path); } /* Folder permissions. */ FolderPerm * folder_perm_new (const char *path, const char *permission) { FolderPerm *perm = g_new0 (FolderPerm, 1); perm->path = g_strdup(path); perm->permission = g_strdup(permission); return perm; } void folder_perm_free (FolderPerm *perm) { if (!perm) return; g_free (perm->path); g_free (perm->permission); g_free (perm); } static GList * folder_perm_list_copy (GList *perms) { GList *ret = NULL, *ptr; FolderPerm *perm, *new_perm; for (ptr = perms; ptr; ptr = ptr->next) { perm = ptr->data; new_perm = folder_perm_new (perm->path, perm->permission); ret = g_list_append (ret, new_perm); } return ret; } static gint comp_folder_perms (gconstpointer a, gconstpointer b) { const FolderPerm *perm_a = a, *perm_b = b; return (strcmp (perm_b->path, perm_a->path)); } static void delete_folder_perm (SeafRepoManager *mgr, const char *repo_id, FolderPermType type, FolderPerm *perm) { GList *folder_perms = NULL; if (type == FOLDER_PERM_TYPE_USER) { folder_perms = g_hash_table_lookup (mgr->priv->user_perms, repo_id); if (!folder_perms) return; if (g_strcmp0 (perm->path, "") == 0) { g_list_free_full (folder_perms, (GDestroyNotify)folder_perm_free); g_hash_table_remove (mgr->priv->user_perms, repo_id); return; } GList *existing = g_list_find_custom (folder_perms, perm, comp_folder_perms); if (existing) { FolderPerm *old_perm = existing->data; folder_perms = g_list_remove (folder_perms, old_perm); folder_perm_free (old_perm); g_hash_table_insert (mgr->priv->user_perms, g_strdup(repo_id), folder_perms); } } else if (type == FOLDER_PERM_TYPE_GROUP) { folder_perms = g_hash_table_lookup (mgr->priv->group_perms, repo_id); if (!folder_perms) return; if (g_strcmp0 (perm->path, "") == 0) { g_list_free_full (folder_perms, (GDestroyNotify)folder_perm_free); g_hash_table_remove (mgr->priv->group_perms, repo_id); return; } GList *existing = g_list_find_custom (folder_perms, perm, comp_folder_perms); if (existing) { FolderPerm *old_perm = existing->data; folder_perms = g_list_remove (folder_perms, old_perm); folder_perm_free (old_perm); g_hash_table_insert (mgr->priv->group_perms, g_strdup(repo_id), folder_perms); } } } int seaf_repo_manager_delete_folder_perm (SeafRepoManager *mgr, const char *repo_id, FolderPermType type, FolderPerm *perm) { char *sql; sqlite3_stmt *stmt; g_return_val_if_fail ((type == FOLDER_PERM_TYPE_USER || type == FOLDER_PERM_TYPE_GROUP), -1); if (!perm) { return 0; } /* Update db. */ pthread_mutex_lock (&mgr->priv->db_lock); if (g_strcmp0 (perm->path, "") == 0) { if (type == FOLDER_PERM_TYPE_USER) sql = "DELETE FROM FolderUserPerms WHERE repo_id = ?"; else sql = "DELETE FROM FolderGroupPerms WHERE repo_id = ?"; } else { if (type == FOLDER_PERM_TYPE_USER) sql = "DELETE FROM FolderUserPerms WHERE repo_id = ? and path = ?"; else sql = "DELETE FROM FolderGroupPerms WHERE repo_id = ? and path = ?"; } stmt = sqlite_query_prepare (mgr->priv->db, sql); sqlite3_bind_text (stmt, 1, repo_id, -1, SQLITE_TRANSIENT); if (g_strcmp0 (perm->path, "") != 0) sqlite3_bind_text (stmt, 2, perm->path, -1, SQLITE_TRANSIENT); if (sqlite3_step (stmt) != SQLITE_DONE) { seaf_warning ("Failed to remove folder perm for %.8s: %s.\n", repo_id, sqlite3_errmsg (mgr->priv->db)); sqlite3_finalize (stmt); pthread_mutex_unlock (&mgr->priv->db_lock); return -1; } sqlite3_finalize (stmt); pthread_mutex_unlock (&mgr->priv->db_lock); /* Update in memory */ pthread_mutex_lock (&mgr->priv->perm_lock); delete_folder_perm (mgr, repo_id, type, perm); pthread_mutex_unlock (&mgr->priv->perm_lock); return 0; } int seaf_repo_manager_update_folder_perm (SeafRepoManager *mgr, const char *repo_id, FolderPermType type, FolderPerm *perm) { char *sql; sqlite3_stmt *stmt; g_return_val_if_fail ((type == FOLDER_PERM_TYPE_USER || type == FOLDER_PERM_TYPE_GROUP), -1); if (!perm) { return 0; } /* Update db. */ pthread_mutex_lock (&mgr->priv->db_lock); if (type == FOLDER_PERM_TYPE_USER) sql = "DELETE FROM FolderUserPerms WHERE repo_id = ? and path = ?"; else sql = "DELETE FROM FolderGroupPerms WHERE repo_id = ? and path = ?"; stmt = sqlite_query_prepare (mgr->priv->db, sql); sqlite3_bind_text (stmt, 1, repo_id, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 2, perm->path, -1, SQLITE_TRANSIENT); if (sqlite3_step (stmt) != SQLITE_DONE) { seaf_warning ("Failed to remove folder perm for %.8s(%s): %s.\n", repo_id, perm->path, sqlite3_errmsg (mgr->priv->db)); sqlite3_finalize (stmt); pthread_mutex_unlock (&mgr->priv->db_lock); return -1; } sqlite3_finalize (stmt); if (type == FOLDER_PERM_TYPE_USER) sql = "INSERT INTO FolderUserPerms VALUES (?, ?, ?)"; else sql = "INSERT INTO FolderGroupPerms VALUES (?, ?, ?)"; stmt = sqlite_query_prepare (mgr->priv->db, sql); sqlite3_bind_text (stmt, 1, repo_id, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 2, perm->path, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 3, perm->permission, -1, SQLITE_TRANSIENT); if (sqlite3_step (stmt) != SQLITE_DONE) { seaf_warning ("Failed to insert folder perm for %.8s(%s): %s.\n", repo_id, perm->path, sqlite3_errmsg (mgr->priv->db)); sqlite3_finalize (stmt); pthread_mutex_unlock (&mgr->priv->db_lock); return -1; } sqlite3_finalize (stmt); pthread_mutex_unlock (&mgr->priv->db_lock); /* Update in memory */ GList *folder_perms; pthread_mutex_lock (&mgr->priv->perm_lock); if (type == FOLDER_PERM_TYPE_USER) { folder_perms = g_hash_table_lookup (mgr->priv->user_perms, repo_id); if (folder_perms) { GList *existing = g_list_find_custom (folder_perms, perm, comp_folder_perms); if (existing) { FolderPerm *old_perm = existing->data; g_free (old_perm->permission); old_perm->permission = g_strdup (perm->permission); } else { FolderPerm *new_perm = folder_perm_new (perm->path, perm->permission); folder_perms = g_list_insert_sorted (folder_perms, new_perm, comp_folder_perms); } } else { FolderPerm *new_perm = folder_perm_new (perm->path, perm->permission); folder_perms = g_list_insert_sorted (folder_perms, new_perm, comp_folder_perms); } g_hash_table_insert (mgr->priv->user_perms, g_strdup(repo_id), folder_perms); } else if (type == FOLDER_PERM_TYPE_GROUP) { folder_perms = g_hash_table_lookup (mgr->priv->group_perms, repo_id); if (folder_perms) { GList *existing = g_list_find_custom (folder_perms, perm, comp_folder_perms); if (existing) { FolderPerm *old_perm = existing->data; g_free (old_perm->permission); old_perm->permission = g_strdup (perm->permission); } else { FolderPerm *new_perm = folder_perm_new (perm->path, perm->permission); folder_perms = g_list_insert_sorted (folder_perms, new_perm, comp_folder_perms); } } else { FolderPerm *new_perm = folder_perm_new (perm->path, perm->permission); folder_perms = g_list_insert_sorted (folder_perms, new_perm, comp_folder_perms); } g_hash_table_insert (mgr->priv->group_perms, g_strdup(repo_id), folder_perms); } pthread_mutex_unlock (&mgr->priv->perm_lock); return 0; } int seaf_repo_manager_update_folder_perms (SeafRepoManager *mgr, const char *repo_id, FolderPermType type, GList *folder_perms) { char *sql; sqlite3_stmt *stmt; GList *ptr; FolderPerm *perm; g_return_val_if_fail ((type == FOLDER_PERM_TYPE_USER || type == FOLDER_PERM_TYPE_GROUP), -1); /* Update db. */ pthread_mutex_lock (&mgr->priv->db_lock); if (type == FOLDER_PERM_TYPE_USER) sql = "DELETE FROM FolderUserPerms WHERE repo_id = ?"; else sql = "DELETE FROM FolderGroupPerms WHERE repo_id = ?"; stmt = sqlite_query_prepare (mgr->priv->db, sql); sqlite3_bind_text (stmt, 1, repo_id, -1, SQLITE_TRANSIENT); if (sqlite3_step (stmt) != SQLITE_DONE) { seaf_warning ("Failed to remove folder perms for %.8s: %s.\n", repo_id, sqlite3_errmsg (mgr->priv->db)); sqlite3_finalize (stmt); pthread_mutex_unlock (&mgr->priv->db_lock); return -1; } sqlite3_finalize (stmt); if (!folder_perms) { pthread_mutex_unlock (&mgr->priv->db_lock); return 0; } if (type == FOLDER_PERM_TYPE_USER) sql = "INSERT INTO FolderUserPerms VALUES (?, ?, ?)"; else sql = "INSERT INTO FolderGroupPerms VALUES (?, ?, ?)"; stmt = sqlite_query_prepare (mgr->priv->db, sql); for (ptr = folder_perms; ptr; ptr = ptr->next) { perm = ptr->data; sqlite3_bind_text (stmt, 1, repo_id, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 2, perm->path, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 3, perm->permission, -1, SQLITE_TRANSIENT); if (sqlite3_step (stmt) != SQLITE_DONE) { seaf_warning ("Failed to insert folder perms for %.8s: %s.\n", repo_id, sqlite3_errmsg (mgr->priv->db)); sqlite3_finalize (stmt); pthread_mutex_unlock (&mgr->priv->db_lock); return -1; } sqlite3_reset (stmt); sqlite3_clear_bindings (stmt); } sqlite3_finalize (stmt); pthread_mutex_unlock (&mgr->priv->db_lock); /* Update in memory */ GList *new, *old; new = folder_perm_list_copy (folder_perms); new = g_list_sort (new, comp_folder_perms); pthread_mutex_lock (&mgr->priv->perm_lock); if (type == FOLDER_PERM_TYPE_USER) { old = g_hash_table_lookup (mgr->priv->user_perms, repo_id); if (old) g_list_free_full (old, (GDestroyNotify)folder_perm_free); g_hash_table_insert (mgr->priv->user_perms, g_strdup(repo_id), new); } else if (type == FOLDER_PERM_TYPE_GROUP) { old = g_hash_table_lookup (mgr->priv->group_perms, repo_id); if (old) g_list_free_full (old, (GDestroyNotify)folder_perm_free); g_hash_table_insert (mgr->priv->group_perms, g_strdup(repo_id), new); } pthread_mutex_unlock (&mgr->priv->perm_lock); return 0; } static gboolean load_folder_perm (sqlite3_stmt *stmt, void *data) { GList **p_perms = data; const char *path, *permission; path = (const char *)sqlite3_column_text (stmt, 0); permission = (const char *)sqlite3_column_text (stmt, 1); FolderPerm *perm = folder_perm_new (path, permission); *p_perms = g_list_prepend (*p_perms, perm); return TRUE; } static GList * load_folder_perms_for_repo (SeafRepoManager *mgr, const char *repo_id, FolderPermType type) { GList *perms = NULL; char sql[256]; g_return_val_if_fail ((type == FOLDER_PERM_TYPE_USER || type == FOLDER_PERM_TYPE_GROUP), NULL); if (type == FOLDER_PERM_TYPE_USER) sqlite3_snprintf (sizeof(sql), sql, "SELECT path, permission FROM FolderUserPerms " "WHERE repo_id = '%q'", repo_id); else sqlite3_snprintf (sizeof(sql), sql, "SELECT path, permission FROM FolderGroupPerms " "WHERE repo_id = '%q'", repo_id); pthread_mutex_lock (&mgr->priv->db_lock); if (sqlite_foreach_selected_row (mgr->priv->db, sql, load_folder_perm, &perms) < 0) { pthread_mutex_unlock (&mgr->priv->db_lock); GList *ptr; for (ptr = perms; ptr; ptr = ptr->next) folder_perm_free ((FolderPerm *)ptr->data); g_list_free (perms); return NULL; } pthread_mutex_unlock (&mgr->priv->db_lock); /* Sort list in descending order by perm->path (longer path first). */ perms = g_list_sort (perms, comp_folder_perms); return perms; } static void init_folder_perms (SeafRepoManager *mgr) { SeafRepoManagerPriv *priv = mgr->priv; GList *repo_ids = g_hash_table_get_keys (priv->repo_hash); GList *ptr; GList *perms; char *repo_id; priv->user_perms = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); priv->group_perms = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); pthread_mutex_init (&priv->perm_lock, NULL); for (ptr = repo_ids; ptr; ptr = ptr->next) { repo_id = ptr->data; perms = load_folder_perms_for_repo (mgr, repo_id, FOLDER_PERM_TYPE_USER); if (perms) { pthread_mutex_lock (&priv->perm_lock); g_hash_table_insert (priv->user_perms, g_strdup(repo_id), perms); pthread_mutex_unlock (&priv->perm_lock); } perms = load_folder_perms_for_repo (mgr, repo_id, FOLDER_PERM_TYPE_GROUP); if (perms) { pthread_mutex_lock (&priv->perm_lock); g_hash_table_insert (priv->group_perms, g_strdup(repo_id), perms); pthread_mutex_unlock (&priv->perm_lock); } } g_list_free (repo_ids); } static void remove_folder_perms (SeafRepoManager *mgr, const char *repo_id) { GList *perms = NULL; pthread_mutex_lock (&mgr->priv->perm_lock); perms = g_hash_table_lookup (mgr->priv->user_perms, repo_id); if (perms) { g_list_free_full (perms, (GDestroyNotify)folder_perm_free); g_hash_table_remove (mgr->priv->user_perms, repo_id); } perms = g_hash_table_lookup (mgr->priv->group_perms, repo_id); if (perms) { g_list_free_full (perms, (GDestroyNotify)folder_perm_free); g_hash_table_remove (mgr->priv->group_perms, repo_id); } pthread_mutex_unlock (&mgr->priv->perm_lock); } int seaf_repo_manager_update_folder_perm_timestamp (SeafRepoManager *mgr, const char *repo_id, gint64 timestamp) { char sql[256]; int ret; snprintf (sql, sizeof(sql), "REPLACE INTO FolderPermTimestamp VALUES ('%s', %"G_GINT64_FORMAT")", repo_id, timestamp); pthread_mutex_lock (&mgr->priv->db_lock); ret = sqlite_query_exec (mgr->priv->db, sql); pthread_mutex_unlock (&mgr->priv->db_lock); return ret; } gint64 seaf_repo_manager_get_folder_perm_timestamp (SeafRepoManager *mgr, const char *repo_id) { char sql[256]; gint64 ret; sqlite3_snprintf (sizeof(sql), sql, "SELECT timestamp FROM FolderPermTimestamp WHERE repo_id = '%q'", repo_id); pthread_mutex_lock (&mgr->priv->db_lock); ret = sqlite_get_int64 (mgr->priv->db, sql); pthread_mutex_unlock (&mgr->priv->db_lock); return ret; } static char * lookup_folder_perm (GList *perms, const char *path) { GList *ptr; FolderPerm *perm; char *folder; int len; char *permission = NULL; for (ptr = perms; ptr; ptr = ptr->next) { perm = ptr->data; if (strcmp (perm->path, "/") != 0) folder = g_strconcat (perm->path, "/", NULL); else folder = g_strdup(perm->path); len = strlen(folder); if (strcmp (perm->path, path) == 0 || strncmp(folder, path, len) == 0) { permission = perm->permission; g_free (folder); break; } g_free (folder); } return permission; } static gboolean is_path_writable (const char *repo_id, gboolean is_repo_readonly, const char *path) { SeafRepoManager *mgr = seaf->repo_mgr; GList *user_perms = NULL, *group_perms = NULL; char *permission = NULL; char *abs_path = NULL; pthread_mutex_lock (&mgr->priv->perm_lock); user_perms = g_hash_table_lookup (mgr->priv->user_perms, repo_id); group_perms = g_hash_table_lookup (mgr->priv->group_perms, repo_id); if (user_perms || group_perms) abs_path = g_strconcat ("/", path, NULL); if (user_perms) permission = lookup_folder_perm (user_perms, abs_path); if (!permission && group_perms) permission = lookup_folder_perm (group_perms, abs_path); pthread_mutex_unlock (&mgr->priv->perm_lock); g_free (abs_path); if (!permission) return !is_repo_readonly; if (strcmp (permission, "rw") == 0) return TRUE; else return FALSE; } gboolean seaf_repo_manager_is_path_writable (SeafRepoManager *mgr, const char *repo_id, const char *path) { SeafRepo *repo = seaf_repo_manager_get_repo (mgr, repo_id); if (!repo) { seaf_warning ("Failed to get repo %s.\n", repo_id); return FALSE; } return is_path_writable (repo_id, repo->is_readonly, path); } gboolean is_repo_id_valid (const char *id) { if (!id) return FALSE; return is_uuid_valid (id); } /* * Sync error related. These functions should belong to the sync-mgr module. * But since we have to store the errors in repo database, we have to put the code here. */ int seaf_repo_manager_record_sync_error (const char *repo_id, const char *repo_name, const char *path, int error_id) { char *sql; int ret; pthread_mutex_lock (&seaf->repo_mgr->priv->db_lock); if (path != NULL) sql = sqlite3_mprintf ("DELETE FROM FileSyncError WHERE repo_id='%q' AND path='%q'", repo_id, path); else sql = sqlite3_mprintf ("DELETE FROM FileSyncError WHERE repo_id='%q' AND path IS NULL", repo_id); ret = sqlite_query_exec (seaf->repo_mgr->priv->db, sql); sqlite3_free (sql); if (ret < 0) goto out; /* REPLACE INTO will update the primary key id automatically. * So new errors are always on top. */ if (path != NULL) sql = sqlite3_mprintf ("INSERT INTO FileSyncError " "(repo_id, repo_name, path, err_id, timestamp) " "VALUES ('%q', '%q', '%q', %d, %lld)", repo_id, repo_name, path, error_id, (gint64)time(NULL)); else sql = sqlite3_mprintf ("INSERT INTO FileSyncError " "(repo_id, repo_name, err_id, timestamp) " "VALUES ('%q', '%q', %d, %lld)", repo_id, repo_name, error_id, (gint64)time(NULL)); ret = sqlite_query_exec (seaf->repo_mgr->priv->db, sql); sqlite3_free (sql); out: pthread_mutex_unlock (&seaf->repo_mgr->priv->db_lock); return ret; } static gboolean collect_file_sync_errors (sqlite3_stmt *stmt, void *data) { GList **pret = data; const char *repo_id, *repo_name, *path; int id, err_id; gint64 timestamp; SeafileFileSyncError *error; id = sqlite3_column_int (stmt, 0); repo_id = (const char *)sqlite3_column_text (stmt, 1); repo_name = (const char *)sqlite3_column_text (stmt, 2); path = (const char *)sqlite3_column_text (stmt, 3); err_id = sqlite3_column_int (stmt, 4); timestamp = sqlite3_column_int64 (stmt, 5); error = g_object_new (SEAFILE_TYPE_FILE_SYNC_ERROR, "id", id, "repo_id", repo_id, "repo_name", repo_name, "path", path, "err_id", err_id, "timestamp", timestamp, NULL); *pret = g_list_prepend (*pret, error); return TRUE; } int seaf_repo_manager_del_file_sync_error_by_id (SeafRepoManager *mgr, int id) { int ret = 0; char *sql = NULL; pthread_mutex_lock (&mgr->priv->db_lock); sql = sqlite3_mprintf ("DELETE FROM FileSyncError WHERE id=%d", id); ret = sqlite_query_exec (mgr->priv->db, sql); sqlite3_free (sql); pthread_mutex_unlock (&mgr->priv->db_lock); return ret; } GList * seaf_repo_manager_get_file_sync_errors (SeafRepoManager *mgr, int offset, int limit) { GList *ret = NULL; char *sql; pthread_mutex_lock (&mgr->priv->db_lock); sql = sqlite3_mprintf ("SELECT id, repo_id, repo_name, path, err_id, timestamp FROM " "FileSyncError ORDER BY id DESC LIMIT %d OFFSET %d", limit, offset); sqlite_foreach_selected_row (mgr->priv->db, sql, collect_file_sync_errors, &ret); sqlite3_free (sql); pthread_mutex_unlock (&mgr->priv->db_lock); ret = g_list_reverse (ret); return ret; } /* * Record file-level sync errors and send system notification. */ struct _SyncError { char *repo_id; char *repo_name; char *path; int err_id; gint64 timestamp; }; typedef struct _SyncError SyncError; void send_sync_error_notification (const char *repo_id, const char *repo_name, const char *path, int err_id) { json_t *object; char *str; object = json_object (); json_object_set_new (object, "repo_id", json_string(repo_id)); json_object_set_new (object, "repo_name", json_string(repo_name)); json_object_set_new (object, "path", json_string(path)); json_object_set_new (object, "err_id", json_integer(err_id)); str = json_dumps (object, 0); seaf_mq_manager_publish_notification (seaf->mq_mgr, "sync.error", str); free (str); json_decref (object); } static void check_and_send_notification (SeafRepoManager *mgr, const char *repo_id, const char *repo_name, const char *path, int err_id) { GList *errors = mgr->priv->sync_errors, *ptr; SyncError *err, *new_err; gboolean found = FALSE; pthread_mutex_lock (&mgr->priv->errors_lock); for (ptr = errors; ptr; ptr = ptr->next) { err = ptr->data; if (g_strcmp0 (err->repo_id, repo_id) == 0 && g_strcmp0 (err->path, path) == 0) { found = TRUE; if (err->err_id != err_id) { err->err_id = err_id; send_sync_error_notification (repo_id, repo_name, path, err_id); } err->timestamp = (gint64)time(NULL); break; } } if (!found) { new_err = g_new0 (SyncError, 1); new_err->repo_id = g_strdup(repo_id); new_err->repo_name = g_strdup(repo_name); new_err->path = g_strdup(path); new_err->err_id = err_id; new_err->timestamp = (gint64)time(NULL); mgr->priv->sync_errors = g_list_prepend (mgr->priv->sync_errors, new_err); send_sync_error_notification (repo_id, repo_name, path, err_id); } pthread_mutex_unlock (&mgr->priv->errors_lock); } void send_file_sync_error_notification (const char *repo_id, const char *repo_name, const char *path, int err_id) { if (!repo_name) { SeafRepo *repo = seaf_repo_manager_get_repo (seaf->repo_mgr, repo_id); if (!repo) return; repo_name = repo->name; } seaf_repo_manager_record_sync_error (repo_id, repo_name, path, err_id); seaf_sync_manager_set_task_error_code (seaf->sync_mgr, repo_id, err_id); check_and_send_notification (seaf->repo_mgr, repo_id, repo_name, path, err_id); } SeafRepo* seaf_repo_new (const char *id, const char *name, const char *desc) { SeafRepo* repo; /* valid check */ repo = g_new0 (SeafRepo, 1); memcpy (repo->id, id, 36); repo->id[36] = '\0'; repo->name = g_strdup(name); repo->desc = g_strdup(desc); repo->worktree_invalid = TRUE; repo->auto_sync = 1; pthread_mutex_init (&repo->lock, NULL); return repo; } int seaf_repo_check_worktree (SeafRepo *repo) { SeafStat st; if (repo->worktree == NULL) { seaf_warning ("Worktree for repo '%s'(%.8s) is not set.\n", repo->name, repo->id); return -1; } /* check repo worktree */ if (g_access(repo->worktree, F_OK) < 0) { if (!repo->worktree_invalid) { seaf_warning ("Failed to access worktree %s for repo '%s'(%.8s)\n", repo->worktree, repo->name, repo->id); } return -1; } if (seaf_stat(repo->worktree, &st) < 0) { seaf_warning ("Failed to stat worktree %s for repo '%s'(%.8s)\n", repo->worktree, repo->name, repo->id); return -1; } if (!S_ISDIR(st.st_mode)) { seaf_warning ("Worktree %s for repo '%s'(%.8s) is not a directory.\n", repo->worktree, repo->name, repo->id); return -1; } return 0; } static gboolean check_worktree_common (SeafRepo *repo) { if (!repo->head) { seaf_warning ("Head for repo '%s'(%.8s) is not set.\n", repo->name, repo->id); return FALSE; } if (seaf_repo_check_worktree (repo) < 0) { return FALSE; } return TRUE; } void seaf_repo_free (SeafRepo *repo) { if (repo->head) seaf_branch_unref (repo->head); g_free (repo->name); g_free (repo->desc); g_free (repo->category); g_free (repo->worktree); g_free (repo->relay_id); g_free (repo->email); g_free (repo->username); g_free (repo->token); g_free (repo->pwd_hash_algo); g_free (repo->pwd_hash_params); g_free (repo); } static void set_head_common (SeafRepo *repo, SeafBranch *branch) { if (repo->head) seaf_branch_unref (repo->head); repo->head = branch; seaf_branch_ref(branch); } int seaf_repo_set_head (SeafRepo *repo, SeafBranch *branch) { if (save_branch_repo_map (repo->manager, branch) < 0) return -1; set_head_common (repo, branch); return 0; } SeafCommit * seaf_repo_get_head_commit (const char *repo_id) { SeafRepo *repo; SeafCommit *head; repo = seaf_repo_manager_get_repo (seaf->repo_mgr, repo_id); if (!repo) { seaf_warning ("Failed to get repo %s.\n", repo_id); return NULL; } head = seaf_commit_manager_get_commit (seaf->commit_mgr, repo_id, repo->version, repo->head->commit_id); if (!head) { seaf_warning ("Failed to get head for repo %s.\n", repo_id); return NULL; } return head; } void seaf_repo_from_commit (SeafRepo *repo, SeafCommit *commit) { repo->name = g_strdup (commit->repo_name); repo->desc = g_strdup (commit->repo_desc); repo->encrypted = commit->encrypted; repo->last_modify = commit->ctime; memcpy (repo->root_id, commit->root_id, 40); if (repo->encrypted) { repo->enc_version = commit->enc_version; if (repo->enc_version == 1 && !commit->pwd_hash_algo) memcpy (repo->magic, commit->magic, 32); else if (repo->enc_version == 2) { memcpy (repo->random_key, commit->random_key, 96); } else if (repo->enc_version == 3) { memcpy (repo->random_key, commit->random_key, 96); memcpy (repo->salt, commit->salt, 64); } else if (repo->enc_version == 4) { memcpy (repo->random_key, commit->random_key, 96); memcpy (repo->salt, commit->salt, 64); } if (repo->enc_version >= 2 && !commit->pwd_hash_algo) { memcpy (repo->magic, commit->magic, 64); } if (commit->pwd_hash_algo) { memcpy (repo->pwd_hash, commit->pwd_hash, 64); repo->pwd_hash_algo = g_strdup (commit->pwd_hash_algo); repo->pwd_hash_params = g_strdup (commit->pwd_hash_params); } } repo->no_local_history = commit->no_local_history; repo->version = commit->version; } void seaf_repo_to_commit (SeafRepo *repo, SeafCommit *commit) { commit->repo_name = g_strdup (repo->name); commit->repo_desc = g_strdup (repo->desc); commit->encrypted = repo->encrypted; if (commit->encrypted) { commit->enc_version = repo->enc_version; if (commit->enc_version == 1 && !repo->pwd_hash_algo) commit->magic = g_strdup (repo->magic); else if (commit->enc_version == 2) { commit->random_key = g_strdup (repo->random_key); } else if (commit->enc_version == 3) { commit->random_key = g_strdup (repo->random_key); commit->salt = g_strdup (repo->salt); } else if (commit->enc_version == 4) { commit->random_key = g_strdup (repo->random_key); commit->salt = g_strdup (repo->salt); } if (commit->enc_version >= 2 && !repo->pwd_hash_algo) { commit->magic = g_strdup (repo->magic); } if (repo->pwd_hash_algo) { commit->pwd_hash = g_strdup (repo->pwd_hash); commit->pwd_hash_algo = g_strdup (repo->pwd_hash_algo); commit->pwd_hash_params = g_strdup (repo->pwd_hash_params); } } commit->no_local_history = repo->no_local_history; commit->version = repo->version; } static gboolean need_to_sync_worktree_name (const char *repo_id) { char *need_sync_wt_name = seaf_repo_manager_get_repo_property (seaf->repo_mgr, repo_id, REPO_SYNC_WORKTREE_NAME); gboolean ret = (g_strcmp0(need_sync_wt_name, "true") == 0); g_free (need_sync_wt_name); return ret; } static void update_repo_worktree_name (SeafRepo *repo, const char *new_name, gboolean rewatch) { char *dirname = NULL, *basename = NULL; char *new_worktree = NULL; seaf_message ("Update worktree folder name of repo %s to %s.\n", repo->id, new_name); dirname = g_path_get_dirname (repo->worktree); if (g_strcmp0 (dirname, ".") == 0) return; basename = g_path_get_basename (repo->worktree); new_worktree = g_build_filename (dirname, new_name, NULL); /* This can possibly fail on Windows if some files are opened under the worktree. * The rename operation will be retried on next restart. */ if (seaf_util_rename (repo->worktree, new_worktree) < 0) { seaf_warning ("Failed to rename worktree from %s to %s: %s.\n", repo->worktree, new_worktree, strerror(errno)); goto out; } if (seaf_repo_manager_set_repo_worktree (seaf->repo_mgr, repo, new_worktree) < 0) { goto out; } if (rewatch) { if (seaf_wt_monitor_unwatch_repo (seaf->wt_monitor, repo->id) < 0) { seaf_warning ("Failed to unwatch repo %s old worktree.\n", repo->id); goto out; } if (seaf_wt_monitor_watch_repo (seaf->wt_monitor, repo->id, repo->worktree) < 0) { seaf_warning ("Failed to watch repo %s new worktree.\n", repo->id); } } out: g_free (dirname); g_free (basename); g_free (new_worktree); } void seaf_repo_set_name (SeafRepo *repo, const char *new_name) { char *old_name = repo->name; repo->name = g_strdup(new_name); g_free (old_name); if (need_to_sync_worktree_name (repo->id)) update_repo_worktree_name (repo, new_name, TRUE); } static gboolean collect_commit (SeafCommit *commit, void *vlist, gboolean *stop) { GList **commits = vlist; /* The traverse function will unref the commit, so we need to ref it. */ seaf_commit_ref (commit); *commits = g_list_prepend (*commits, commit); return TRUE; } GList * seaf_repo_get_commits (SeafRepo *repo) { GList *branches; GList *ptr; SeafBranch *branch; GList *commits = NULL; branches = seaf_branch_manager_get_branch_list (seaf->branch_mgr, repo->id); if (branches == NULL) { seaf_warning ("Failed to get branch list of repo %s.\n", repo->id); return NULL; } for (ptr = branches; ptr != NULL; ptr = ptr->next) { branch = ptr->data; gboolean res = seaf_commit_manager_traverse_commit_tree (seaf->commit_mgr, repo->id, repo->version, branch->commit_id, collect_commit, &commits, FALSE); if (!res) { for (ptr = commits; ptr != NULL; ptr = ptr->next) seaf_commit_unref ((SeafCommit *)(ptr->data)); g_list_free (commits); goto out; } } commits = g_list_reverse (commits); out: for (ptr = branches; ptr != NULL; ptr = ptr->next) { seaf_branch_unref ((SeafBranch *)ptr->data); } return commits; } void seaf_repo_set_readonly (SeafRepo *repo) { repo->is_readonly = TRUE; save_repo_property (repo->manager, repo->id, REPO_PROP_IS_READONLY, "true"); } void seaf_repo_unset_readonly (SeafRepo *repo) { repo->is_readonly = FALSE; save_repo_property (repo->manager, repo->id, REPO_PROP_IS_READONLY, "false"); } gboolean seaf_repo_manager_is_ignored_hidden_file (const char *filename) { GPatternSpec **spec = ignore_patterns; while (*spec) { if (g_pattern_match_string(*spec, filename)) return TRUE; spec++; } return FALSE; } static gboolean should_ignore(const char *basepath, const char *filename, void *data) { GPatternSpec **spec = ignore_patterns; GList *ignore_list = (GList *)data; if (!g_utf8_validate (filename, -1, NULL)) { seaf_warning ("File name %s contains non-UTF8 characters, skip.\n", filename); return TRUE; } /* Ignore file/dir if its name is too long. */ if (strlen(filename) >= SEAF_DIR_NAME_LEN) { seaf_warning ("File name %s is too long, skip.\n", filename); return TRUE; } if (strchr (filename, '/')) return TRUE; while (*spec) { if (g_pattern_match_string(*spec, filename)) return TRUE; spec++; } if (!seaf->sync_extra_temp_file) { spec = office_temp_ignore_patterns; while (*spec) { if (g_pattern_match_string(*spec, filename)) return TRUE; spec++; } } if (basepath) { char *fullpath = g_build_path ("/", basepath, filename, NULL); if (seaf_repo_check_ignore_file (ignore_list, fullpath)) { g_free (fullpath); return TRUE; } g_free (fullpath); } return FALSE; } #ifndef WIN32 static inline gboolean has_trailing_space_or_period (const char *path) { int len = strlen(path); if (path[len - 1] == ' ' || path[len - 1] == '.') { return TRUE; } return FALSE; } static gboolean check_path_ignore_on_windows (const char *file_path) { gboolean ret = FALSE; static char illegals[] = {'\\', ':', '*', '?', '"', '<', '>', '|', '\b', '\t'}; char **components = g_strsplit (file_path, "/", -1); int n_comps = g_strv_length (components); int j = 0; char *file_name; int i; char c; for (; j < n_comps; ++j) { file_name = components[j]; if (has_trailing_space_or_period (file_name)) { /* Ignore files/dir whose path has trailing spaces. It would cause * problem on windows. */ /* g_debug ("ignore '%s' which contains trailing space in path\n", path); */ ret = TRUE; goto out; } for (i = 0; i < G_N_ELEMENTS(illegals); i++) { if (strchr (file_name, illegals[i])) { ret = TRUE; goto out; } } for (c = 1; c <= 31; c++) { if (strchr (file_name, c)) { ret = TRUE; goto out; } } } out: g_strfreev (components); return ret; } #endif static int index_cb (const char *repo_id, int version, const char *path, unsigned char sha1[], SeafileCrypt *crypt, gboolean write_data) { gint64 size; /* Check in blocks and get object ID. */ if (seaf_fs_manager_index_blocks (seaf->fs_mgr, repo_id, version, path, sha1, &size, crypt, write_data, !seaf->disable_block_hash) < 0) { seaf_warning ("Failed to index file %s.\n", path); return -1; } return 0; } static gboolean is_symlink (const char *full_path) { #ifndef WIN32 SeafStat st; if (lstat (full_path, &st) == 0 && S_ISLNK(st.st_mode)) { return TRUE; } return FALSE; #else return FALSE; #endif } #define MAX_COMMIT_SIZE 100 * (1 << 20) /* 100MB */ typedef struct _AddOptions { LockedFileSet *fset; ChangeSet *changeset; gboolean is_repo_ro; gboolean startup_scan; } AddOptions; static int add_file (const char *repo_id, int version, const char *modifier, struct index_state *istate, const char *path, const char *full_path, SeafStat *st, SeafileCrypt *crypt, gint64 *total_size, GQueue **remain_files, AddOptions *options) { gboolean added = FALSE; int ret = 0; gboolean is_writable = TRUE, is_locked = FALSE; struct cache_entry *ce; char *base_name = NULL; if (seaf->ignore_symlinks && is_symlink(full_path)) { return ret; } if (options) is_writable = is_path_writable(repo_id, options->is_repo_ro, path); is_locked = seaf_filelock_manager_is_file_locked (seaf->filelock_mgr, repo_id, path); if (is_locked && options && !(options->startup_scan)) { /* send_sync_error_notification (repo_id, NULL, path, */ /* SYNC_ERROR_ID_FILE_LOCKED); */ } if (options && options->startup_scan) { SyncStatus status; ce = index_name_exists (istate, path, strlen(path), 0); if (!ce || ie_match_stat(ce, st, 0) != 0) status = SYNC_STATUS_SYNCING; else status = SYNC_STATUS_SYNCED; /* Don't set "syncing" status for read-only path. */ if (status == SYNC_STATUS_SYNCED || (is_writable && !is_locked)) seaf_sync_manager_update_active_path (seaf->sync_mgr, repo_id, path, S_IFREG, status, FALSE); /* send an error notification for read-only repo when modifying a file. */ if (status == SYNC_STATUS_SYNCING && !is_writable) send_file_sync_error_notification (repo_id, NULL, path, SYNC_ERROR_ID_UPDATE_TO_READ_ONLY_REPO); } if (!is_writable || is_locked) return ret; #if defined WIN32 || defined __APPLE__ if (options && options->fset) { LockedFile *file = locked_file_set_lookup (options->fset, path); if (file) { if (strcmp (file->operation, LOCKED_OP_DELETE) == 0) { /* Only remove the lock record if the file is changed. */ if (st->st_mtime == file->old_mtime) { return ret; } locked_file_set_remove (options->fset, path, FALSE); } else if (strcmp (file->operation, LOCKED_OP_UPDATE) == 0) { return ret; } } } #endif #ifndef WIN32 base_name = g_path_get_basename(full_path); if (!seaf->hide_windows_incompatible_path_notification && check_path_ignore_on_windows (base_name)) { ce = index_name_exists (istate, path, strlen(path), 0); if (!ce) send_file_sync_error_notification (repo_id, NULL, path, SYNC_ERROR_ID_INVALID_PATH_ON_WINDOWS); } g_free (base_name); #endif if (!remain_files) { ret = add_to_index (repo_id, version, istate, path, full_path, st, 0, crypt, index_cb, modifier, &added); if (!added) { /* If the contents of the file doesn't change, move it to synced status. */ seaf_sync_manager_update_active_path (seaf->sync_mgr, repo_id, path, S_IFREG, SYNC_STATUS_SYNCED, FALSE); } else { if (total_size) *total_size += (gint64)(st->st_size); if (options && options->changeset) { /* ce may be updated. */ ce = index_name_exists (istate, path, strlen(path), 0); add_to_changeset (options->changeset, DIFF_STATUS_ADDED, ce->sha1, st, modifier, path, NULL); } } } else if (*remain_files == NULL) { ret = add_to_index (repo_id, version, istate, path, full_path, st, 0, crypt, index_cb, modifier, &added); if (added) { *total_size += (gint64)(st->st_size); if (*total_size >= MAX_COMMIT_SIZE) *remain_files = g_queue_new (); } else { seaf_sync_manager_update_active_path (seaf->sync_mgr, repo_id, path, S_IFREG, SYNC_STATUS_SYNCED, FALSE); } if (added && options && options->changeset) { /* ce may be updated. */ ce = index_name_exists (istate, path, strlen(path), 0); add_to_changeset (options->changeset, DIFF_STATUS_ADDED, ce->sha1, st, modifier, path, NULL); } } else { *total_size += (gint64)(st->st_size); g_queue_push_tail (*remain_files, g_strdup(path)); } if (ret < 0) { seaf_sync_manager_update_active_path (seaf->sync_mgr, repo_id, path, S_IFREG, SYNC_STATUS_ERROR, TRUE); // Only record index error when the file exists. if (seaf_util_exists (full_path)) { send_file_sync_error_notification (repo_id, NULL, path, SYNC_ERROR_ID_INDEX_ERROR); } } return ret; } typedef struct AddParams { const char *repo_id; int version; const char *modifier; struct index_state *istate; const char *worktree; SeafileCrypt *crypt; gboolean ignore_empty_dir; GList *ignore_list; gint64 *total_size; GQueue **remain_files; AddOptions *options; } AddParams; #ifndef WIN32 static int add_dir_recursive (const char *path, const char *full_path, SeafStat *st, AddParams *params, gboolean ignored) { AddOptions *options = params->options; GDir *dir; const char *dname; char *subpath, *full_subpath; int n, total; gboolean is_writable = TRUE; struct stat sub_st; char *base_name = NULL; if (seaf->ignore_symlinks && is_symlink(full_path)) { return 0; } dir = g_dir_open (full_path, 0, NULL); if (!dir) { seaf_warning ("Failed to open dir %s: %s.\n", full_path, strerror(errno)); seaf_sync_manager_update_active_path (seaf->sync_mgr, params->repo_id, path, S_IFDIR, SYNC_STATUS_ERROR, TRUE); if (!ignored) { send_file_sync_error_notification (params->repo_id, NULL, path, SYNC_ERROR_ID_INDEX_ERROR); } return 0; } base_name = g_path_get_basename(full_path); if (!seaf->hide_windows_incompatible_path_notification && check_path_ignore_on_windows (base_name)) { struct cache_entry *ce = index_name_exists (params->istate, path, strlen(path), 0); if (!ce) send_file_sync_error_notification (params->repo_id, NULL, path, SYNC_ERROR_ID_INVALID_PATH_ON_WINDOWS); } g_free (base_name); n = 0; total = 0; while ((dname = g_dir_read_name(dir)) != NULL) { ++total; #ifdef __APPLE__ char *norm_dname = g_utf8_normalize (dname, -1, G_NORMALIZE_NFC); subpath = g_build_path (PATH_SEPERATOR, path, norm_dname, NULL); g_free (norm_dname); #else subpath = g_build_path (PATH_SEPERATOR, path, dname, NULL); #endif full_subpath = g_build_filename (params->worktree, subpath, NULL); if (stat (full_subpath, &sub_st) < 0) { seaf_warning ("Failed to stat %s: %s.\n", full_subpath, strerror(errno)); g_free (subpath); g_free (full_subpath); continue; } if (ignored || should_ignore(full_path, dname, params->ignore_list)) { if (options && options->startup_scan) { if (S_ISDIR(sub_st.st_mode)) add_dir_recursive (subpath, full_subpath, &sub_st, params, TRUE); else seaf_sync_manager_update_active_path (seaf->sync_mgr, params->repo_id, subpath, S_IFREG, SYNC_STATUS_IGNORED, TRUE); } g_free (subpath); g_free (full_subpath); continue; } ++n; if (S_ISDIR(sub_st.st_mode)) add_dir_recursive (subpath, full_subpath, &sub_st, params, FALSE); else if (S_ISREG(sub_st.st_mode)) add_file (params->repo_id, params->version, params->modifier, params->istate, subpath, full_subpath, &sub_st, params->crypt, params->total_size, params->remain_files, params->options); g_free (subpath); g_free (full_subpath); } g_dir_close (dir); if (ignored) { seaf_sync_manager_update_active_path (seaf->sync_mgr, params->repo_id, path, S_IFDIR, SYNC_STATUS_IGNORED, TRUE); return 0; } if (options) is_writable = is_path_writable(params->repo_id, options->is_repo_ro, path); /* Update active path status for empty dir */ if (options && options->startup_scan && total == 0) { SyncStatus status; struct cache_entry *ce = index_name_exists (params->istate, path, strlen(path), 0); if (!ce) status = SYNC_STATUS_SYNCING; else status = SYNC_STATUS_SYNCED; if (status == SYNC_STATUS_SYNCED || is_writable) seaf_sync_manager_update_active_path (seaf->sync_mgr, params->repo_id, path, S_IFDIR, status, FALSE); } if (n == 0 && path[0] != 0 && is_writable) { if (!params->remain_files || *(params->remain_files) == NULL) { int rc = add_empty_dir_to_index (params->istate, path, st); if (rc == 1 && options && options->changeset) { unsigned char allzero[20] = {0}; add_to_changeset (options->changeset, DIFF_STATUS_DIR_ADDED, allzero, st, NULL, path, NULL); } } else g_queue_push_tail (*(params->remain_files), g_strdup(path)); } return 0; } /* * @remain_files: returns the files haven't been added under this path. * If it's set to NULL, no partial commit will be created. */ static int add_recursive (const char *repo_id, int version, const char *modifier, struct index_state *istate, const char *worktree, const char *path, SeafileCrypt *crypt, gboolean ignore_empty_dir, GList *ignore_list, gint64 *total_size, GQueue **remain_files, AddOptions *options) { char *full_path; SeafStat st; full_path = g_build_path (PATH_SEPERATOR, worktree, path, NULL); if (seaf_stat (full_path, &st) < 0) { /* Ignore broken symlinks on Linux and Mac OS X */ if (lstat (full_path, &st) == 0 && S_ISLNK(st.st_mode)) { g_free (full_path); return 0; } seaf_warning ("Failed to stat %s.\n", full_path); g_free (full_path); /* Ignore error. */ seaf_sync_manager_update_active_path (seaf->sync_mgr, repo_id, path, 0, SYNC_STATUS_ERROR, TRUE); return 0; } if (S_ISREG(st.st_mode)) { add_file (repo_id, version, modifier, istate, path, full_path, &st, crypt, total_size, remain_files, options); } else if (S_ISDIR(st.st_mode)) { AddParams params = { .repo_id = repo_id, .version = version, .modifier = modifier, .istate = istate, .worktree = worktree, .crypt = crypt, .ignore_empty_dir = ignore_empty_dir, .ignore_list = ignore_list, .total_size = total_size, .remain_files = remain_files, .options = options, }; add_dir_recursive (path, full_path, &st, ¶ms, FALSE); } g_free (full_path); return 0; } static gboolean is_empty_dir (const char *path, GList *ignore_list) { GDir *dir; const char *dname; gboolean ret = TRUE; dir = g_dir_open (path, 0, NULL); if (!dir) { return FALSE; } while ((dname = g_dir_read_name(dir)) != NULL) { if (!should_ignore(path, dname, ignore_list)) { ret = FALSE; break; } } g_dir_close (dir); return ret; } #else typedef struct IterCBData { AddParams *add_params; const char *parent; const char *full_parent; int n; /* If parent dir is ignored, all children are ignored too. */ gboolean ignored; } IterCBData; static int add_dir_recursive (const char *path, const char *full_path, SeafStat *st, AddParams *params, gboolean ignored); static int iter_dir_cb (wchar_t *full_parent_w, WIN32_FIND_DATAW *fdata, void *user_data, gboolean *stop) { IterCBData *data = user_data; AddParams *params = data->add_params; AddOptions *options = params->options; char *dname = NULL, *path = NULL, *full_path = NULL; SeafStat st; int ret = 0; dname = g_utf16_to_utf8 (fdata->cFileName, -1, NULL, NULL, NULL); if (!dname) { goto out; } path = g_build_path ("/", data->parent, dname, NULL); full_path = g_build_path ("/", params->worktree, path, NULL); seaf_stat_from_find_data (fdata, &st); if (data->ignored || should_ignore(data->full_parent, dname, params->ignore_list)) { if (options && options->startup_scan) { if (fdata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) add_dir_recursive (path, full_path, &st, params, TRUE); else seaf_sync_manager_update_active_path (seaf->sync_mgr, params->repo_id, path, S_IFREG, SYNC_STATUS_IGNORED, TRUE); } goto out; } if (fdata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ret = add_dir_recursive (path, full_path, &st, params, FALSE); else ret = add_file (params->repo_id, params->version, params->modifier, params->istate, path, full_path, &st, params->crypt, params->total_size, params->remain_files, params->options); ++(data->n); out: g_free (dname); g_free (path); g_free (full_path); return 0; } static int add_dir_recursive (const char *path, const char *full_path, SeafStat *st, AddParams *params, gboolean ignored) { AddOptions *options = params->options; IterCBData data; wchar_t *full_path_w; int ret = 0; gboolean is_writable = TRUE; memset (&data, 0, sizeof(data)); data.add_params = params; data.parent = path; data.full_parent = full_path; data.ignored = ignored; full_path_w = win32_long_path (full_path); ret = traverse_directory_win32 (full_path_w, iter_dir_cb, &data); g_free (full_path_w); /* Ignore traverse dir error. */ if (ret < 0) { seaf_sync_manager_update_active_path (seaf->sync_mgr, params->repo_id, path, S_IFDIR, SYNC_STATUS_ERROR, TRUE); if (!ignored) { send_file_sync_error_notification (params->repo_id, NULL, path, SYNC_ERROR_ID_INDEX_ERROR); } return 0; } if (ignored) { seaf_sync_manager_update_active_path (seaf->sync_mgr, params->repo_id, path, S_IFDIR, SYNC_STATUS_IGNORED, TRUE); return 0; } if (options) is_writable = is_path_writable(params->repo_id, options->is_repo_ro, path); /* Update active path status for empty dir */ if (options && options->startup_scan && ret == 0) { SyncStatus status; struct cache_entry *ce = index_name_exists (params->istate, path, strlen(path), 0); if (!ce) status = SYNC_STATUS_SYNCING; else status = SYNC_STATUS_SYNCED; if (status == SYNC_STATUS_SYNCED || is_writable) seaf_sync_manager_update_active_path (seaf->sync_mgr, params->repo_id, path, S_IFDIR, status, FALSE); } if (data.n == 0 && path[0] != 0 && !params->ignore_empty_dir && is_writable) { if (!params->remain_files || *(params->remain_files) == NULL) { int rc = add_empty_dir_to_index (params->istate, path, st); if (rc == 1 && options && options->changeset) { unsigned char allzero[20] = {0}; add_to_changeset (options->changeset, DIFF_STATUS_DIR_ADDED, allzero, st, NULL, path, NULL); } } else g_queue_push_tail (*(params->remain_files), g_strdup(path)); } return ret; } static int add_recursive (const char *repo_id, int version, const char *modifier, struct index_state *istate, const char *worktree, const char *path, SeafileCrypt *crypt, gboolean ignore_empty_dir, GList *ignore_list, gint64 *total_size, GQueue **remain_files, AddOptions *options) { char *full_path; SeafStat st; int ret = 0; full_path = g_build_path (PATH_SEPERATOR, worktree, path, NULL); if (seaf_stat (full_path, &st) < 0) { seaf_warning ("Failed to stat %s.\n", full_path); g_free (full_path); seaf_sync_manager_update_active_path (seaf->sync_mgr, repo_id, path, 0, SYNC_STATUS_ERROR, TRUE); /* Ignore error */ return 0; } if (S_ISREG(st.st_mode)) { ret = add_file (repo_id, version, modifier, istate, path, full_path, &st, crypt, total_size, remain_files, options); } else if (S_ISDIR(st.st_mode)) { AddParams params = { .repo_id = repo_id, .version = version, .modifier = modifier, .istate = istate, .worktree = worktree, .crypt = crypt, .ignore_empty_dir = ignore_empty_dir, .ignore_list = ignore_list, .total_size = total_size, .remain_files = remain_files, .options = options, }; ret = add_dir_recursive (path, full_path, &st, ¶ms, FALSE); } g_free (full_path); return ret; } static gboolean is_empty_dir (const char *path, GList *ignore_list) { WIN32_FIND_DATAW fdata; HANDLE handle; wchar_t *pattern; wchar_t *path_w; char *dname; int path_len_w; DWORD error; gboolean ret = TRUE; path_w = win32_long_path (path); path_len_w = wcslen(path_w); pattern = g_new0 (wchar_t, (path_len_w + 3)); wcscpy (pattern, path_w); wcscat (pattern, L"\\*"); handle = FindFirstFileW (pattern, &fdata); if (handle == INVALID_HANDLE_VALUE) { seaf_warning ("FindFirstFile failed %s: %lu.\n", path, GetLastError()); ret = FALSE; goto out; } do { if (wcscmp (fdata.cFileName, L".") == 0 || wcscmp (fdata.cFileName, L"..") == 0) continue; dname = g_utf16_to_utf8 (fdata.cFileName, -1, NULL, NULL, NULL); if (!dname || !should_ignore (path, dname, ignore_list)) { ret = FALSE; g_free (dname); FindClose (handle); goto out; } g_free (dname); } while (FindNextFileW (handle, &fdata) != 0); error = GetLastError(); if (error != ERROR_NO_MORE_FILES) { seaf_warning ("FindNextFile failed %s: %lu.\n", path, error); } FindClose (handle); out: g_free (path_w); g_free (pattern); return ret; } #endif /* WIN32 */ /* Returns whether the file should be removed from index. */ static gboolean check_locked_file_before_remove (LockedFileSet *fset, const char *path) { #if defined WIN32 || defined __APPLE__ if (!fset) return TRUE; LockedFile *file = locked_file_set_lookup (fset, path); gboolean ret = TRUE; if (file) ret = FALSE; return ret; #else return TRUE; #endif } static void remove_deleted (struct index_state *istate, const char *worktree, const char *prefix, GList *ignore_list, LockedFileSet *fset, const char *repo_id, gboolean is_repo_ro, ChangeSet *changeset) { struct cache_entry **ce_array = istate->cache; struct cache_entry *ce; char path[SEAF_PATH_MAX]; unsigned int i; SeafStat st; int ret; gboolean not_exist; char *full_prefix = g_strconcat (prefix, "/", NULL); int len = strlen(full_prefix); for (i = 0; i < istate->cache_nr; ++i) { ce = ce_array[i]; if (!is_path_writable (repo_id, is_repo_ro, ce->name)) continue; if (seaf_filelock_manager_is_file_locked (seaf->filelock_mgr, repo_id, ce->name)) { seaf_debug ("Remove deleted: %s is locked on server, ignore.\n", ce->name); continue; } if (prefix[0] != 0 && strcmp (ce->name, prefix) != 0 && strncmp (ce->name, full_prefix, len) != 0) continue; snprintf (path, SEAF_PATH_MAX, "%s/%s", worktree, ce->name); not_exist = FALSE; ret = seaf_stat (path, &st); if (ret < 0 && errno == ENOENT) not_exist = TRUE; if (S_ISDIR (ce->ce_mode)) { if (ce->ce_ctime.sec != 0 || ce_stage(ce) != 0) { if (not_exist || (ret == 0 && !S_ISDIR (st.st_mode))) { /* Add to changeset only if dir is removed. */ ce->ce_flags |= CE_REMOVE; if (changeset) /* Remove the parent dir from change set if it becomes * empty. If in the work tree the empty dir still exist, * we'll add it back to changeset in add_recursive() later. */ remove_from_changeset (changeset, DIFF_STATUS_DIR_DELETED, ce->name, TRUE, prefix); } else if (!is_empty_dir (path, ignore_list)) { /* Don't add to changeset if empty dir became non-empty. */ ce->ce_flags |= CE_REMOVE; } } } else { /* If ce->ctime is 0 and stage is 0, it was not successfully checked out. * In this case we don't want to mistakenly remove the file * from the repo. */ if ((not_exist || (ret == 0 && !S_ISREG (st.st_mode))) && (ce->ce_ctime.sec != 0 || ce_stage(ce) != 0) && check_locked_file_before_remove (fset, ce->name)) { ce_array[i]->ce_flags |= CE_REMOVE; if (changeset) remove_from_changeset (changeset, DIFF_STATUS_DELETED, ce->name, TRUE, prefix); } } } remove_marked_cache_entries (istate); g_free (full_prefix); } static int scan_worktree_for_changes (struct index_state *istate, SeafRepo *repo, SeafileCrypt *crypt, GList *ignore_list, LockedFileSet *fset) { remove_deleted (istate, repo->worktree, "", ignore_list, fset, repo->id, repo->is_readonly, repo->changeset); AddOptions options; memset (&options, 0, sizeof(options)); options.fset = fset; options.is_repo_ro = repo->is_readonly; options.changeset = repo->changeset; if (add_recursive (repo->id, repo->version, repo->email, istate, repo->worktree, "", crypt, FALSE, ignore_list, NULL, NULL, &options) < 0) return -1; return 0; } static gboolean check_full_path_ignore (const char *worktree, const char *path, GList *ignore_list) { char **tokens; guint i; guint n; gboolean ret = FALSE; tokens = g_strsplit (path, "/", 0); n = g_strv_length (tokens); for (i = 0; i < n; ++i) { /* don't check ignore_list */ if (should_ignore (NULL, tokens[i], ignore_list)) { ret = TRUE; goto out; } } char *full_path = g_build_path ("/", worktree, path, NULL); if (seaf_repo_check_ignore_file (ignore_list, full_path)) ret = TRUE; g_free (full_path); out: g_strfreev (tokens); return ret; } static int add_path_to_index (SeafRepo *repo, struct index_state *istate, SeafileCrypt *crypt, const char *path, GList *ignore_list, GList **scanned_dirs, gint64 *total_size, GQueue **remain_files, LockedFileSet *fset) { char *full_path; SeafStat st; AddOptions options; /* When a repo is initially added, a SCAN_DIR event will be created * for the worktree root "". */ if (path[0] == 0) { remove_deleted (istate, repo->worktree, "", ignore_list, fset, repo->id, repo->is_readonly, repo->changeset); memset (&options, 0, sizeof(options)); options.fset = fset; options.is_repo_ro = repo->is_readonly; options.startup_scan = TRUE; options.changeset = repo->changeset; add_recursive (repo->id, repo->version, repo->email, istate, repo->worktree, path, crypt, FALSE, ignore_list, total_size, remain_files, &options); return 0; } /* If we've recursively scanned the parent directory, don't need to scan * any files under it any more. */ GList *ptr; char *dir, *full_dir; for (ptr = *scanned_dirs; ptr; ptr = ptr->next) { dir = ptr->data; /* exact match */ if (strcmp (dir, path) == 0) { seaf_debug ("%s has been scanned before, skip adding.\n", path); return 0; } /* prefix match. */ full_dir = g_strconcat (dir, "/", NULL); if (strncmp (full_dir, path, strlen(full_dir)) == 0) { g_free (full_dir); seaf_debug ("%s has been scanned before, skip adding.\n", path); return 0; } g_free (full_dir); } if (check_full_path_ignore (repo->worktree, path, ignore_list)) return 0; full_path = g_build_filename (repo->worktree, path, NULL); if (seaf_stat (full_path, &st) < 0) { if (errno != ENOENT) send_file_sync_error_notification (repo->id, repo->name, path, SYNC_ERROR_ID_INDEX_ERROR); seaf_warning ("Failed to stat %s: %s.\n", path, strerror(errno)); g_free (full_path); return -1; } if (S_ISDIR(st.st_mode)) *scanned_dirs = g_list_prepend (*scanned_dirs, g_strdup(path)); memset (&options, 0, sizeof(options)); options.fset = fset; options.is_repo_ro = repo->is_readonly; options.changeset = repo->changeset; /* Add is always recursive */ add_recursive (repo->id, repo->version, repo->email, istate, repo->worktree, path, crypt, FALSE, ignore_list, total_size, remain_files, &options); g_free (full_path); return 0; } #if 0 static int add_path_to_index (SeafRepo *repo, struct index_state *istate, SeafileCrypt *crypt, const char *path, GList *ignore_list, GList **scanned_dirs, gint64 *total_size, GQueue **remain_files, LockedFileSet *fset) { /* If we've recursively scanned the parent directory, don't need to scan * any files under it any more. */ GList *ptr; char *dir, *full_dir; for (ptr = *scanned_dirs; ptr; ptr = ptr->next) { dir = ptr->data; /* Have scanned from root directory. */ if (dir[0] == 0) { seaf_debug ("%s has been scanned before, skip adding.\n", path); return 0; } /* exact match */ if (strcmp (dir, path) == 0) { seaf_debug ("%s has been scanned before, skip adding.\n", path); return 0; } /* prefix match. */ full_dir = g_strconcat (dir, "/", NULL); if (strncmp (full_dir, path, strlen(full_dir)) == 0) { g_free (full_dir); seaf_debug ("%s has been scanned before, skip adding.\n", path); return 0; } g_free (full_dir); } if (path[0] != 0 && check_full_path_ignore (repo->worktree, path, ignore_list)) return 0; remove_deleted (istate, repo->worktree, path, ignore_list, NULL, repo->id, repo->is_readonly, repo->changeset); *scanned_dirs = g_list_prepend (*scanned_dirs, g_strdup(path)); AddOptions options; memset (&options, 0, sizeof(options)); options.fset = fset; options.is_repo_ro = repo->is_readonly; options.changeset = repo->changeset; /* When something is changed in the root directory, update active path * sync status when scanning the worktree. This is inaccurate. This will * be changed after we process fs events on Mac more precisely. */ if (path[0] == 0) options.startup_scan = TRUE; /* Add is always recursive */ add_recursive (repo->id, repo->version, repo->email, istate, repo->worktree, path, crypt, FALSE, ignore_list, total_size, remain_files, &options); return 0; } #endif /* __APPLE__ */ static int add_remain_files (SeafRepo *repo, struct index_state *istate, SeafileCrypt *crypt, GQueue *remain_files, GList *ignore_list, gint64 *total_size) { char *path; char *full_path; SeafStat st; struct cache_entry *ce; while ((path = g_queue_pop_head (remain_files)) != NULL) { full_path = g_build_filename (repo->worktree, path, NULL); if (seaf_stat (full_path, &st) < 0) { seaf_warning ("Failed to stat %s: %s.\n", full_path, strerror(errno)); g_free (path); g_free (full_path); continue; } #ifndef WIN32 char *base_name = g_path_get_basename(full_path); if (!seaf->hide_windows_incompatible_path_notification && check_path_ignore_on_windows (base_name)) { send_file_sync_error_notification (repo->id, repo->name, path, SYNC_ERROR_ID_INVALID_PATH_ON_WINDOWS); } g_free (base_name); #endif if (S_ISREG(st.st_mode)) { gboolean added = FALSE; int ret = 0; ret = add_to_index (repo->id, repo->version, istate, path, full_path, &st, 0, crypt, index_cb, repo->email, &added); if (added) { ce = index_name_exists (istate, path, strlen(path), 0); add_to_changeset (repo->changeset, DIFF_STATUS_ADDED, ce->sha1, &st, repo->email, path, NULL); *total_size += (gint64)(st.st_size); if (*total_size >= MAX_COMMIT_SIZE) { g_free (path); g_free (full_path); break; } } else { seaf_sync_manager_update_active_path (seaf->sync_mgr, repo->id, path, S_IFREG, SYNC_STATUS_SYNCED, TRUE); } if (ret < 0) { seaf_sync_manager_update_active_path (seaf->sync_mgr, repo->id, path, S_IFREG, SYNC_STATUS_ERROR, TRUE); if (seaf_util_exists (full_path)) { send_file_sync_error_notification (repo->id, NULL, path, SYNC_ERROR_ID_INDEX_ERROR); } } } else if (S_ISDIR(st.st_mode)) { if (is_empty_dir (full_path, ignore_list)) { int rc = add_empty_dir_to_index (istate, path, &st); if (rc == 1) { unsigned char allzero[20] = {0}; add_to_changeset (repo->changeset, DIFF_STATUS_DIR_ADDED, allzero, &st, NULL, path, NULL); } } } g_free (path); g_free (full_path); } return 0; } static void try_add_empty_parent_dir_entry (const char *worktree, struct index_state *istate, const char *path) { if (index_name_exists (istate, path, strlen(path), 0) != NULL) return; char *parent_dir = g_path_get_dirname (path); /* Parent dir is the worktree dir. */ if (strcmp (parent_dir, ".") == 0) { g_free (parent_dir); return; } char *full_dir = g_build_filename (worktree, parent_dir, NULL); SeafStat st; if (seaf_stat (full_dir, &st) < 0) { goto out; } add_empty_dir_to_index_with_check (istate, parent_dir, &st); out: g_free (parent_dir); g_free (full_dir); } static void try_add_empty_parent_dir_entry_from_wt (const char *worktree, struct index_state *istate, GList *ignore_list, const char *path) { if (index_name_exists (istate, path, strlen(path), 0) != NULL) return; char *parent_dir = g_path_get_dirname (path); /* Parent dir is the worktree dir. */ if (strcmp (parent_dir, ".") == 0) { g_free (parent_dir); return; } char *full_dir = g_build_filename (worktree, parent_dir, NULL); SeafStat st; if (seaf_stat (full_dir, &st) < 0) { goto out; } if (is_empty_dir (full_dir, ignore_list)) { #ifdef WIN32 wchar_t *parent_dir_w = g_utf8_to_utf16 (parent_dir, -1, NULL, NULL, NULL); wchar_t *pw; for (pw = parent_dir_w; *pw != L'\0'; ++pw) if (*pw == L'/') *pw = L'\\'; wchar_t *long_path = win32_83_path_to_long_path (worktree, parent_dir_w, wcslen(parent_dir_w)); g_free (parent_dir_w); if (!long_path) { seaf_warning ("Convert %s to long path failed.\n", parent_dir); goto out; } char *utf8_path = g_utf16_to_utf8 (long_path, -1, NULL, NULL, NULL); if (!utf8_path) { g_free (long_path); goto out; } char *p; for (p = utf8_path; *p != 0; ++p) if (*p == '\\') *p = '/'; g_free (long_path); add_empty_dir_to_index (istate, utf8_path, &st); #else add_empty_dir_to_index (istate, parent_dir, &st); #endif } out: g_free (parent_dir); g_free (full_dir); } static void update_attributes (SeafRepo *repo, struct index_state *istate, const char *worktree, const char *path) { ChangeSet *changeset = repo->changeset; char *full_path; struct cache_entry *ce; SeafStat st; ce = index_name_exists (istate, path, strlen(path), 0); if (!ce) return; full_path = g_build_filename (worktree, path, NULL); if (seaf_stat (full_path, &st) < 0) { seaf_warning ("Failed to stat %s: %s.\n", full_path, strerror(errno)); g_free (full_path); return; } unsigned int new_mode = create_ce_mode (st.st_mode); if (new_mode != ce->ce_mode || st.st_mtime != ce->ce_mtime.sec) { ce->ce_mode = new_mode; ce->ce_mtime.sec = st.st_mtime; istate->cache_changed = 1; add_to_changeset (changeset, DIFF_STATUS_MODIFIED, ce->sha1, &st, repo->email, path, NULL); } g_free (full_path); } #ifdef WIN32 static void scan_subtree_for_deletion (const char *repo_id, struct index_state *istate, const char *worktree, const char *path, GList *ignore_list, LockedFileSet *fset, gboolean is_readonly, GList **scanned_dirs, ChangeSet *changeset) { wchar_t *path_w = NULL; wchar_t *dir_w = NULL; wchar_t *p; char *dir = NULL; char *p2; /* In most file systems, like NTFS, 8.3 format path should contain ~. * Also note that *~ files are ignored. */ if (!strchr (path, '~') || path[strlen(path)-1] == '~') return; path_w = g_utf8_to_utf16 (path, -1, NULL, NULL, NULL); for (p = path_w; *p != L'\0'; ++p) if (*p == L'/') *p = L'\\'; while (1) { p = wcsrchr (path_w, L'\\'); if (p) *p = L'\0'; else break; dir_w = win32_83_path_to_long_path (worktree, path_w, wcslen(path_w)); if (dir_w) break; } if (!dir_w) dir_w = wcsdup(L""); dir = g_utf16_to_utf8 (dir_w, -1, NULL, NULL, NULL); if (!dir) goto out; for (p2 = dir; *p2 != 0; ++p2) if (*p2 == '\\') *p2 = '/'; /* If we've recursively scanned the parent directory, don't need to scan * any files under it any more. */ GList *ptr; char *s, *full_s; for (ptr = *scanned_dirs; ptr; ptr = ptr->next) { s = ptr->data; /* Have scanned from root directory. */ if (s[0] == 0) { goto out; } /* exact match */ if (strcmp (s, path) == 0) { goto out; } /* prefix match. */ full_s = g_strconcat (s, "/", NULL); if (strncmp (full_s, dir, strlen(full_s)) == 0) { g_free (full_s); goto out; } g_free (full_s); } *scanned_dirs = g_list_prepend (*scanned_dirs, g_strdup(dir)); remove_deleted (istate, worktree, dir, ignore_list, fset, repo_id, is_readonly, changeset); /* After remove_deleted(), empty dirs are left not removed in changeset. * This can be fixed by removing the accurate deleted path. In most cases, * basename doesn't contain ~, so we can always get the accurate path. */ /* if (!convertion_failed) { */ /* char *basename = strrchr (path, '/'); */ /* char *deleted_path = NULL; */ /* if (basename) { */ /* deleted_path = g_build_path ("/", dir, basename, NULL); */ /* add_to_changeset (changeset, */ /* DIFF_STATUS_DELETED, */ /* NULL, */ /* NULL, */ /* NULL, */ /* deleted_path, */ /* NULL, */ /* FALSE); */ /* g_free (deleted_path); */ /* } */ /* } */ out: g_free (path_w); g_free (dir_w); g_free (dir); } #else static void scan_subtree_for_deletion (const char *repo_id, struct index_state *istate, const char *worktree, const char *path, GList *ignore_list, LockedFileSet *fset, gboolean is_readonly, GList **scanned_dirs, ChangeSet *changeset) { } #endif /* Return TRUE if the caller should stop processing next event. */ static gboolean handle_add_files (SeafRepo *repo, struct index_state *istate, SeafileCrypt *crypt, GList *ignore_list, LockedFileSet *fset, WTStatus *status, WTEvent *event, GList **scanned_dirs, gint64 *total_size) { SyncInfo *info; if (!repo->create_partial_commit) { /* XXX: We now use remain_files = NULL to signify not creating * partial commits. It's better to use total_size = NULL for * that purpose. */ add_path_to_index (repo, istate, crypt, event->path, ignore_list, scanned_dirs, total_size, NULL, NULL); } else if (!event->remain_files) { GQueue *remain_files = NULL; add_path_to_index (repo, istate, crypt, event->path, ignore_list, scanned_dirs, total_size, &remain_files, fset); if (*total_size >= MAX_COMMIT_SIZE) { seaf_message ("Creating partial commit after adding %s.\n", event->path); status->partial_commit = TRUE; /* An event for a new folder may contain many files. * If the total_size become larger than 100MB after adding * some of these files, the remaining file paths will be * cached in remain files. This way we don't need to scan * the folder again next time. */ if (remain_files) { if (g_queue_get_length (remain_files) == 0) { g_queue_free (remain_files); return TRUE; } seaf_message ("Remain files for %s.\n", event->path); /* Cache remaining files in the event structure. */ event->remain_files = remain_files; pthread_mutex_lock (&status->q_lock); g_queue_push_head (status->event_q, event); pthread_mutex_unlock (&status->q_lock); info = seaf_sync_manager_get_sync_info (seaf->sync_mgr, repo->id); if (!info->multipart_upload) { info->multipart_upload = TRUE; info->total_bytes = *total_size; } } return TRUE; } } else { seaf_message ("Adding remaining files for %s.\n", event->path); add_remain_files (repo, istate, crypt, event->remain_files, ignore_list, total_size); if (g_queue_get_length (event->remain_files) != 0) { pthread_mutex_lock (&status->q_lock); g_queue_push_head (status->event_q, event); pthread_mutex_unlock (&status->q_lock); return TRUE; } else { info = seaf_sync_manager_get_sync_info (seaf->sync_mgr, repo->id); info->end_multipart_upload = TRUE; return TRUE; } if (*total_size >= MAX_COMMIT_SIZE) return TRUE; } return FALSE; } #ifdef __APPLE__ /* struct _WTDirent { */ /* char *dname; */ /* struct stat st; */ /* }; */ /* typedef struct _WTDirent WTDirent; */ /* static gint */ /* compare_wt_dirents (gconstpointer a, gconstpointer b) */ /* { */ /* const WTDirent *dent_a = a, *dent_b = b; */ /* return (strcmp (dent_a->dname, dent_b->dname)); */ /* } */ /* static GList * */ /* get_sorted_wt_dirents (const char *dir_path, const char *full_dir_path, */ /* gboolean *error) */ /* { */ /* GDir *dir; */ /* GError *err = NULL; */ /* const char *name; */ /* char *dname; */ /* char *full_sub_path, *sub_path; */ /* WTDirent *dent; */ /* GList *ret = NULL; */ /* dir = g_dir_open (full_dir_path, 0, &err); */ /* if (!dir) { */ /* seaf_warning ("Failed to open dir %s: %s.\n", full_dir_path, err->message); */ /* *error = TRUE; */ /* return NULL; */ /* } */ /* while ((name = g_dir_read_name(dir)) != NULL) { */ /* dname = g_utf8_normalize (name, -1, G_NORMALIZE_NFC); */ /* sub_path = g_strconcat (dir_path, "/", dname, NULL); */ /* full_sub_path = g_strconcat (full_dir_path, "/", dname, NULL); */ /* dent = g_new0 (WTDirent, 1); */ /* dent->dname = dname; */ /* if (stat (full_sub_path, &dent->st) < 0) { */ /* seaf_warning ("Failed to stat %s: %s.\n", full_sub_path, strerror(errno)); */ /* g_free (dname); */ /* g_free (sub_path); */ /* g_free (full_sub_path); */ /* g_free (dent); */ /* continue; */ /* } */ /* ret = g_list_prepend (ret, dent); */ /* g_free (sub_path); */ /* g_free (full_sub_path); */ /* } */ /* g_dir_close (dir); */ /* ret = g_list_sort (ret, compare_wt_dirents); */ /* return ret; */ /* } */ /* static void */ /* wt_dirent_free (WTDirent *dent) */ /* { */ /* if (!dent) */ /* return; */ /* g_free (dent->dname); */ /* g_free (dent); */ /* } */ /* inline static char * */ /* concat_sub_path (const char *dir, const char *dname) */ /* { */ /* if (dir[0] != 0) */ /* return g_strconcat(dir, "/", dname, NULL); */ /* else */ /* return g_strdup(dname); */ /* } */ /* static int */ /* get_changed_paths_in_folder (SeafRepo *repo, struct index_state *istate, */ /* const char *dir_path, */ /* GList **add, GList **mod, GList **del) */ /* { */ /* char *full_dir_path; */ /* GList *wt_dents = NULL, *index_dents = NULL; */ /* gboolean error = FALSE; */ /* full_dir_path = g_build_filename(repo->worktree, dir_path, NULL); */ /* wt_dents = get_sorted_wt_dirents (dir_path, full_dir_path, &error); */ /* if (error) { */ /* g_free (full_dir_path); */ /* return -1; */ /* } */ /* index_dents = list_dirents_from_index (istate, dir_path); */ /* GList *p; */ /* IndexDirent *dent; */ /* for (p = index_dents; p; p = p->next) { */ /* dent = p->data; */ /* } */ /* GList *p1 = wt_dents, *p2 = index_dents; */ /* WTDirent *dent1; */ /* IndexDirent *dent2; */ /* while (p1 && p2) { */ /* dent1 = p1->data; */ /* dent2 = p2->data; */ /* int rc = strcmp (dent1->dname, dent2->dname); */ /* if (rc == 0) { */ /* if (S_ISREG(dent1->st.st_mode) && !dent2->is_dir) { */ /* if (dent1->st.st_mtime != dent2->ce->ce_mtime.sec) */ /* *mod = g_list_prepend (*mod, concat_sub_path(dir_path, dent1->dname)); */ /* } else if ((S_ISREG(dent1->st.st_mode) && dent2->is_dir) || */ /* (S_ISDIR(dent1->st.st_mode) && !dent2->is_dir)) { */ /* *add = g_list_prepend (*add, concat_sub_path(dir_path, dent1->dname)); */ /* *del = g_list_prepend (*del, concat_sub_path(dir_path, dent1->dname)); */ /* } */ /* p1 = p1->next; */ /* p2 = p2->next; */ /* } else if (rc < 0) { */ /* *add = g_list_prepend (*add, concat_sub_path(dir_path, dent1->dname)); */ /* p1 = p1->next; */ /* } else { */ /* *del = g_list_prepend (*del, concat_sub_path(dir_path, dent2->dname)); */ /* p2 = p2->next; */ /* } */ /* } */ /* while (p1) { */ /* dent1 = p1->data; */ /* *add = g_list_prepend (*add, concat_sub_path(dir_path, dent1->dname)); */ /* p1 = p1->next; */ /* } */ /* while (p2) { */ /* dent2 = p2->data; */ /* *del = g_list_prepend (*del, concat_sub_path(dir_path, dent2->dname)); */ /* p2 = p2->next; */ /* } */ /* g_free (full_dir_path); */ /* g_list_free_full (wt_dents, (GDestroyNotify)wt_dirent_free); */ /* g_list_free_full (index_dents, (GDestroyNotify)index_dirent_free); */ /* return 0; */ /* } */ #endif /* __APPLE__ */ static void update_active_file (SeafRepo *repo, const char *path, SeafStat *st, struct index_state *istate, gboolean ignored) { if (ignored) { seaf_sync_manager_update_active_path (seaf->sync_mgr, repo->id, path, S_IFREG, SYNC_STATUS_IGNORED, TRUE); } else { SyncStatus status; gboolean is_writable; struct cache_entry *ce = index_name_exists(istate, path, strlen(path), 0); if (!ce || ie_match_stat(ce, st, 0) != 0) status = SYNC_STATUS_SYNCING; else status = SYNC_STATUS_SYNCED; is_writable = is_path_writable (repo->id, repo->is_readonly, path); if (!is_writable && status == SYNC_STATUS_SYNCING) seaf_sync_manager_delete_active_path (seaf->sync_mgr, repo->id, path); else seaf_sync_manager_update_active_path (seaf->sync_mgr, repo->id, path, S_IFREG, status, FALSE); } } #ifdef WIN32 typedef struct _UpdatePathData { SeafRepo *repo; struct index_state *istate; GList *ignore_list; const char *parent; const char *full_parent; gboolean ignored; } UpdatePathData; static void update_active_path_recursive (SeafRepo *repo, const char *path, struct index_state *istate, GList *ignore_list, gboolean ignored); static int update_active_path_cb (wchar_t *full_parent_w, WIN32_FIND_DATAW *fdata, void *user_data, gboolean *stop) { UpdatePathData *upd_data = user_data; char *dname; char *path; gboolean ignored = FALSE; SeafStat st; dname = g_utf16_to_utf8 (fdata->cFileName, -1, NULL, NULL, NULL); if (!dname) return 0; path = g_build_path ("/", upd_data->parent, dname, NULL); if (upd_data->ignored || should_ignore (upd_data->full_parent, dname, upd_data->ignore_list)) ignored = TRUE; seaf_stat_from_find_data (fdata, &st); if (fdata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { update_active_path_recursive (upd_data->repo, path, upd_data->istate, upd_data->ignore_list, ignored); } else { update_active_file (upd_data->repo, path, &st, upd_data->istate, ignored); } g_free (dname); g_free (path); return 0; } static void update_active_path_recursive (SeafRepo *repo, const char *path, struct index_state *istate, GList *ignore_list, gboolean ignored) { char *full_path; wchar_t *full_path_w; int ret = 0; UpdatePathData upd_data; full_path = g_build_filename (repo->worktree, path, NULL); memset (&upd_data, 0, sizeof(upd_data)); upd_data.repo = repo; upd_data.istate = istate; upd_data.ignore_list = ignore_list; upd_data.parent = path; upd_data.full_parent = full_path; upd_data.ignored = ignored; full_path_w = win32_long_path (full_path); ret = traverse_directory_win32 (full_path_w, update_active_path_cb, &upd_data); g_free (full_path_w); g_free (full_path); if (ret < 0) return; /* Don't set sync status for read-only paths, since changes to read-only * files are ignored. */ if (!is_path_writable (repo->id, repo->is_readonly, path)) return; /* traverse_directory_win32() returns number of entries in the directory. */ if (ret == 0 && path[0] != 0) { if (ignored) { seaf_sync_manager_update_active_path (seaf->sync_mgr, repo->id, path, S_IFDIR, SYNC_STATUS_IGNORED, FALSE); } else { /* There is no need to update an empty dir. */ SyncStatus status; struct cache_entry *ce = index_name_exists(istate, path, strlen(path), 0); if (!ce) status = SYNC_STATUS_SYNCING; else status = SYNC_STATUS_SYNCED; seaf_sync_manager_update_active_path (seaf->sync_mgr, repo->id, path, S_IFDIR, status, FALSE); } } } #else static void update_active_path_recursive (SeafRepo *repo, const char *path, struct index_state *istate, GList *ignore_list, gboolean ignored) { GDir *dir; GError *error = NULL; const char *name; char *dname; char *full_path, *full_sub_path, *sub_path; struct stat st; gboolean ignore_sub; full_path = g_build_filename(repo->worktree, path, NULL); dir = g_dir_open (full_path, 0, &error); if (!dir) { seaf_warning ("Failed to open dir %s: %s.\n", full_path, error->message); g_free (full_path); return; } int n = 0; while ((name = g_dir_read_name(dir)) != NULL) { ++n; dname = g_utf8_normalize (name, -1, G_NORMALIZE_NFC); sub_path = g_strconcat (path, "/", dname, NULL); full_sub_path = g_strconcat (full_path, "/", dname, NULL); ignore_sub = FALSE; if (ignored || should_ignore(full_path, dname, ignore_list)) ignore_sub = TRUE; if (seaf->ignore_symlinks && is_symlink(full_sub_path)) { g_free (dname); g_free (sub_path); g_free (full_sub_path); continue; } if (stat (full_sub_path, &st) < 0) { seaf_warning ("Failed to stat %s: %s.\n", full_sub_path, strerror(errno)); g_free (dname); g_free (sub_path); g_free (full_sub_path); continue; } if (S_ISDIR(st.st_mode)) { update_active_path_recursive (repo, sub_path, istate, ignore_list, ignore_sub); } else if (S_ISREG(st.st_mode)) { update_active_file (repo, sub_path, &st, istate, ignore_sub); } g_free (dname); g_free (sub_path); g_free (full_sub_path); } g_dir_close (dir); g_free (full_path); /* Don't set sync status for read-only paths, since changes to read-only * files are ignored. */ if (!is_path_writable (repo->id, repo->is_readonly, path)) return; if (n == 0 && path[0] != 0) { if (ignored) { seaf_sync_manager_update_active_path (seaf->sync_mgr, repo->id, path, S_IFDIR, SYNC_STATUS_IGNORED, TRUE); } else { /* There is no need to update an empty dir. */ SyncStatus status; struct cache_entry *ce = index_name_exists(istate, path, strlen(path), 0); if (!ce) status = SYNC_STATUS_SYNCING; else status = SYNC_STATUS_SYNCED; seaf_sync_manager_update_active_path (seaf->sync_mgr, repo->id, path, S_IFDIR, status, TRUE); } } } #endif /* WIN32 */ static void process_active_path (SeafRepo *repo, const char *path, struct index_state *istate, GList *ignore_list) { SeafStat st; gboolean ignored = FALSE; char *fullpath = g_build_filename (repo->worktree, path, NULL); if (seaf_stat (fullpath, &st) < 0) { g_free (fullpath); return; } if (check_full_path_ignore (repo->worktree, path, ignore_list)) ignored = TRUE; if (S_ISREG(st.st_mode)) { if (!seaf_filelock_manager_is_file_locked(seaf->filelock_mgr, repo->id, path)) { update_active_file (repo, path, &st, istate, ignored); } } else { update_active_path_recursive (repo, path, istate, ignore_list, ignored); } g_free (fullpath); } #ifdef __APPLE__ /* static void */ /* process_active_folder (SeafRepo *repo, const char *dir, */ /* struct index_state *istate, GList *ignore_list) */ /* { */ /* GList *add = NULL, *mod = NULL, *del = NULL; */ /* GList *p; */ /* char *path; */ /* /\* Delete event will be triggered on the deleted dir too. *\/ */ /* if (!g_file_test (dir, G_FILE_TEST_IS_DIR)) */ /* return; */ /* if (get_changed_paths_in_folder (repo, istate, dir, &add, &mod, &del) < 0) { */ /* seaf_warning ("Failed to get changed paths under %s.\n", dir); */ /* return; */ /* } */ /* for (p = add; p; p = p->next) { */ /* path = p->data; */ /* process_active_path (repo, path, istate, ignore_list); */ /* } */ /* for (p = mod; p; p = p->next) { */ /* path = p->data; */ /* process_active_path (repo, path, istate, ignore_list); */ /* } */ /* g_list_free_full (add, g_free); */ /* g_list_free_full (mod, g_free); */ /* g_list_free_full (del, g_free); */ /* } */ #endif /* __APPLE__ */ static void update_path_sync_status (SeafRepo *repo, WTStatus *status, struct index_state *istate, GList *ignore_list) { char *path; while (1) { pthread_mutex_lock (&status->ap_q_lock); path = g_queue_pop_head (status->active_paths); pthread_mutex_unlock (&status->ap_q_lock); if (!path) break; /* #ifdef __APPLE__ */ /* process_active_folder (repo, path, istate, ignore_list); */ /* #else */ process_active_path (repo, path, istate, ignore_list); /* #endif */ g_free (path); } } /* Excel first writes update to a temporary file and then rename the file to * xlsx. Unfortunately the temp file dosen't have specific pattern. * We can only ignore renaming from non xlsx file to xlsx file. */ static gboolean ignore_xlsx_update (const char *src_path, const char *dst_path) { GPatternSpec *pattern = g_pattern_spec_new ("*.xlsx"); int ret = FALSE; if (!g_pattern_match_string(pattern, src_path) && g_pattern_match_string(pattern, dst_path)) ret = TRUE; g_pattern_spec_free (pattern); return ret; } static gboolean is_seafile_backup_file (const char *path) { GPatternSpec *pattern = g_pattern_spec_new ("*.sbak"); int ret = FALSE; if (g_pattern_match_string(pattern, path)) ret = TRUE; g_pattern_spec_free (pattern); return ret; } static void handle_rename (SeafRepo *repo, struct index_state *istate, SeafileCrypt *crypt, GList *ignore_list, LockedFileSet *fset, WTEvent *event, GList **scanned_del_dirs, gint64 *total_size) { char *fullpath = NULL; gboolean not_found, src_ignored, dst_ignored; seaf_sync_manager_delete_active_path (seaf->sync_mgr, repo->id, event->path); fullpath = g_build_path ("/", repo->worktree, event->new_path, NULL); // Check whether the renamed file is a symbolic link. if (seaf->ignore_symlinks && is_symlink(fullpath)) { g_free (fullpath); return; } g_free (fullpath); if (!is_path_writable(repo->id, repo->is_readonly, event->path) || !is_path_writable(repo->id, repo->is_readonly, event->new_path)) { seaf_debug ("Rename: %s or %s is not writable, ignore.\n", event->path, event->new_path); return; } if (seaf_filelock_manager_is_file_locked (seaf->filelock_mgr, repo->id, event->path)) { seaf_debug ("Rename: %s is locked on server, ignore.\n", event->path); /* send_sync_error_notification (repo->id, NULL, event->path, */ /* SYNC_ERROR_ID_FILE_LOCKED); */ return; } if (seaf_filelock_manager_is_file_locked (seaf->filelock_mgr, repo->id, event->new_path)) { seaf_debug ("Rename: %s is locked on server, ignore.\n", event->new_path); /* send_sync_error_notification (repo->id, NULL, event->new_path, */ /* SYNC_ERROR_ID_FILE_LOCKED); */ return; } src_ignored = check_full_path_ignore(repo->worktree, event->path, ignore_list); dst_ignored = check_full_path_ignore(repo->worktree, event->new_path, ignore_list); /* If the destination path is ignored, just remove the source path. */ if (dst_ignored) { if (!src_ignored && !is_seafile_backup_file (event->new_path) && check_locked_file_before_remove (fset, event->path)) { not_found = FALSE; remove_from_index_with_prefix (istate, event->path, ¬_found); if (not_found) scan_subtree_for_deletion (repo->id, istate, repo->worktree, event->path, ignore_list, fset, repo->is_readonly, scanned_del_dirs, repo->changeset); remove_from_changeset (repo->changeset, DIFF_STATUS_DELETED, event->path, FALSE, NULL); } return; } /* Now the destination path is not ignored. */ if (!src_ignored && !ignore_xlsx_update (event->path, event->new_path) && check_locked_file_before_remove (fset, event->path)) { not_found = FALSE; rename_index_entries (istate, event->path, event->new_path, ¬_found, NULL, NULL); if (not_found) scan_subtree_for_deletion (repo->id, istate, repo->worktree, event->path, ignore_list, fset, repo->is_readonly, scanned_del_dirs, repo->changeset); /* Moving files out of a dir may make it empty. */ try_add_empty_parent_dir_entry_from_wt (repo->worktree, istate, ignore_list, event->path); add_to_changeset (repo->changeset, DIFF_STATUS_RENAMED, NULL, NULL, NULL, event->path, event->new_path); } AddOptions options; memset (&options, 0, sizeof(options)); options.fset = fset; options.is_repo_ro = repo->is_readonly; options.changeset = repo->changeset; /* We should always scan the destination to compare with the renamed * index entries. For example, in the following case: * 1. file a.txt is updated; * 2. a.txt is moved to test/a.txt; * If the two operations are executed in a batch, the updated content * of a.txt won't be committed if we don't scan the destination, because * when we process the update event, a.txt is already not in its original * place. */ add_recursive (repo->id, repo->version, repo->email, istate, repo->worktree, event->new_path, crypt, FALSE, ignore_list, total_size, NULL, &options); } #ifdef WIN32 typedef struct FindOfficeData { const char *lock_file_name; char *office_file_name; } FindOfficeData; static int find_office_file_cb (wchar_t *parent, WIN32_FIND_DATAW *fdata, void *user_data, gboolean *stop) { FindOfficeData *data = user_data; const wchar_t *dname_w = fdata->cFileName; wchar_t *lock_name_w = NULL; if (wcslen(dname_w) < 2) return 0; if (wcsncmp (dname_w, L"~$", 2) == 0) return 0; lock_name_w = g_utf8_to_utf16 (data->lock_file_name, -1, NULL, NULL, NULL); /* Skip "~$" at the beginning. */ if (wcscmp (dname_w + 2, lock_name_w) == 0) { data->office_file_name = g_utf16_to_utf8 (dname_w, -1, NULL, NULL, NULL); *stop = TRUE; } g_free (lock_name_w); return 0; } static gboolean find_office_file_path (const char *worktree, const char *parent_dir, const char *lock_file_name, char **office_path) { char *fullpath = NULL; wchar_t *fullpath_w = NULL; FindOfficeData data; gboolean ret = FALSE; fullpath = g_build_path ("/", worktree, parent_dir, NULL); fullpath_w = win32_long_path (fullpath); data.lock_file_name = lock_file_name; data.office_file_name = NULL; if (traverse_directory_win32 (fullpath_w, find_office_file_cb, &data) < 0) { goto out; } if (data.office_file_name != NULL) { *office_path = g_build_path ("/", parent_dir, data.office_file_name, NULL); ret = TRUE; } out: g_free (fullpath); g_free (fullpath_w); return ret; } #endif #ifdef __APPLE__ static gboolean find_office_file_path (const char *worktree, const char *parent_dir, const char *lock_file_name, char **office_path) { GDir *dir = NULL; GError *error = NULL; char *fullpath = NULL; const char *dname; char *dname_nfc = NULL; char *dname_skip_head = NULL; gboolean ret = FALSE; fullpath = g_build_path ("/", worktree, parent_dir, NULL); dir = g_dir_open (fullpath, 0, &error); if (error) { seaf_warning ("Failed to open dir %s: %s.\n", fullpath, error->message); g_clear_error (&error); g_free (fullpath); return ret; } while ((dname = g_dir_read_name (dir)) != NULL) { dname_nfc = g_utf8_normalize (dname, -1, G_NORMALIZE_NFC); if (!dname_nfc) continue; if (g_utf8_strlen(dname_nfc, -1) < 2 || strncmp (dname_nfc, "~$", 2) == 0) { g_free (dname_nfc); continue; } dname_skip_head = g_utf8_find_next_char(g_utf8_find_next_char(dname_nfc, NULL), NULL); if (g_strcmp0 (dname_skip_head, lock_file_name) == 0) { *office_path = g_build_path ("/", parent_dir, dname_nfc, NULL); ret = TRUE; g_free (dname_nfc); break; } g_free (dname_nfc); } g_free (fullpath); g_dir_close (dir); return ret; } #endif #if defined WIN32 || defined __APPLE__ static gboolean is_office_lock_file (const char *worktree, const char *path, char **office_path) { gboolean ret; if (!g_regex_match (office_lock_pattern, path, 0, NULL)) return FALSE; /* Replace ~$abc.docx with abc.docx */ *office_path = g_regex_replace (office_lock_pattern, path, -1, 0, "\\1", 0, NULL); /* When the filename is long, sometimes the first two characters in the filename will be directly replaced with ~$. So if the office_path file doesn't exist, we have to match against all filenames in this directory, to find the office file's name. */ char *fullpath = g_build_path ("/", worktree, *office_path, NULL); if (seaf_util_exists (fullpath)) { g_free (fullpath); return TRUE; } g_free (fullpath); char *lock_file_name = g_path_get_basename(*office_path); char *parent_dir = g_path_get_dirname(*office_path); if (strcmp(parent_dir, ".") == 0) { g_free (parent_dir); parent_dir = g_strdup(""); } g_free (*office_path); *office_path = NULL; ret = find_office_file_path (worktree, parent_dir, lock_file_name, office_path); g_free (lock_file_name); g_free (parent_dir); return ret; } typedef struct LockOfficeJob { char repo_id[37]; char *path; gboolean lock; /* False if unlock */ } LockOfficeJob; static void lock_office_job_free (LockOfficeJob *job) { if (!job) return; g_free (job->path); g_free (job); } static void do_lock_office_file (LockOfficeJob *job) { SeafRepo *repo; char *fullpath = NULL; SeafStat st; repo = seaf_repo_manager_get_repo (seaf->repo_mgr, job->repo_id); if (!repo) return; fullpath = g_build_path ("/", repo->worktree, job->path, NULL); if (seaf_stat (fullpath, &st) < 0 || !S_ISREG(st.st_mode)) { g_free (fullpath); return; } g_free (fullpath); seaf_message ("Auto lock file %s/%s\n", repo->name, job->path); int status = seaf_filelock_manager_get_lock_status (seaf->filelock_mgr, repo->id, job->path); if (status != FILE_NOT_LOCKED) { return; } if (http_tx_manager_lock_file (seaf->http_tx_mgr, repo->effective_host, repo->use_fileserver_port, repo->token, repo->id, job->path) < 0) { seaf_warning ("Failed to lock %s in repo %.8s on server.\n", job->path, repo->id); return; } /* Mark file as locked locally so that the user can see the effect immediately. */ seaf_filelock_manager_mark_file_locked (seaf->filelock_mgr, repo->id, job->path, LOCKED_AUTO); } static void do_unlock_office_file (LockOfficeJob *job) { SeafRepo *repo; char *fullpath = NULL; SeafStat st; repo = seaf_repo_manager_get_repo (seaf->repo_mgr, job->repo_id); if (!repo) return; fullpath = g_build_path ("/", repo->worktree, job->path, NULL); if (seaf_stat (fullpath, &st) < 0 || !S_ISREG(st.st_mode)) { g_free (fullpath); return; } g_free (fullpath); seaf_message ("Auto unlock file %s/%s\n", repo->name, job->path); int status = seaf_filelock_manager_get_lock_status (seaf->filelock_mgr, repo->id, job->path); if (status != FILE_LOCKED_BY_ME_AUTO) { return; } if (http_tx_manager_unlock_file (seaf->http_tx_mgr, repo->effective_host, repo->use_fileserver_port, repo->token, repo->id, job->path) < 0) { seaf_warning ("Failed to unlock %s in repo %.8s on server.\n", job->path, repo->id); return; } /* Mark file as unlocked locally so that the user can see the effect immediately. */ seaf_filelock_manager_mark_file_unlocked (seaf->filelock_mgr, repo->id, job->path); } #if 0 static void unlock_closed_office_files () { GList *locked_files, *ptr; SeafRepo *repo; FileLockInfo *info; LockOfficeJob *job; locked_files = seaf_filelock_manager_get_auto_locked_files (seaf->filelock_mgr); for (ptr = locked_files; ptr; ptr = ptr->next) { info = ptr->data; seaf_message ("%s %s.\n", info->repo_id, info->path); repo = seaf_repo_manager_get_repo (seaf->repo_mgr, info->repo_id); if (!repo) continue; seaf_message ("1\n"); if (!do_check_file_locked (info->path, repo->worktree, FALSE)) { seaf_message ("2\n"); job = g_new0 (LockOfficeJob, 1); memcpy (job->repo_id, info->repo_id, 36); job->path = g_strdup(info->path); do_unlock_office_file (job); lock_office_job_free (job); } } g_list_free_full (locked_files, (GDestroyNotify)file_lock_info_free); } #endif static void * lock_office_file_worker (void *vdata) { GAsyncQueue *queue = (GAsyncQueue *)vdata; LockOfficeJob *job; /* unlock_closed_office_files (); */ while (1) { job = g_async_queue_pop (queue); if (!job) break; if (job->lock) do_lock_office_file (job); else do_unlock_office_file (job); lock_office_job_free (job); } return NULL; } static void lock_office_file_on_server (SeafRepo *repo, const char *path) { LockOfficeJob *job; GAsyncQueue *queue = seaf->repo_mgr->priv->lock_office_job_queue; if (!seaf_repo_manager_server_is_pro (seaf->repo_mgr, repo->server_url)) return; job = g_new0 (LockOfficeJob, 1); memcpy (job->repo_id, repo->id, 36); job->path = g_strdup(path); job->lock = TRUE; g_async_queue_push (queue, job); } static void unlock_office_file_on_server (SeafRepo *repo, const char *path) { LockOfficeJob *job; GAsyncQueue *queue = seaf->repo_mgr->priv->lock_office_job_queue; if (!seaf_repo_manager_server_is_pro (seaf->repo_mgr, repo->server_url)) return; job = g_new0 (LockOfficeJob, 1); memcpy (job->repo_id, repo->id, 36); job->path = g_strdup(path); job->lock = FALSE; g_async_queue_push (queue, job); } #endif static int apply_worktree_changes_to_index (SeafRepo *repo, struct index_state *istate, SeafileCrypt *crypt, GList *ignore_list, LockedFileSet *fset, GList **event_list) { WTStatus *status; WTEvent *event, *next_event; gboolean not_found; #if defined WIN32 || defined __APPLE__ char *office_path = NULL; #endif status = seaf_wt_monitor_get_worktree_status (seaf->wt_monitor, repo->id); if (!status) { seaf_warning ("Can't find worktree status for repo %s(%.8s).\n", repo->name, repo->id); return -1; } update_path_sync_status (repo, status, istate, ignore_list); GList *scanned_dirs = NULL, *scanned_del_dirs = NULL; WTEvent *last_event; pthread_mutex_lock (&status->q_lock); last_event = g_queue_peek_tail (status->event_q); pthread_mutex_unlock (&status->q_lock); if (!last_event) { seaf_message ("All events are processed for repo %s.\n", repo->id); status->partial_commit = FALSE; goto out; } gint64 total_size = 0; while (1) { pthread_mutex_lock (&status->q_lock); event = g_queue_pop_head (status->event_q); next_event = g_queue_peek_head (status->event_q); pthread_mutex_unlock (&status->q_lock); if (!event) break; WTEvent *copy = wt_event_new (event->ev_type, event->path, event->new_path); *event_list = g_list_prepend (*event_list, copy); #ifdef WIN32 // If a file or dir is moved, REMOVED and ADDED event will be emitted by the kernel. // When the kernel first emits the REMOVED event of the file or dir, and then emits the ADDED event of the file or dir, // it indicates that this is a move event. if (next_event && event->ev_type == WT_EVENT_DELETE) { char *event_base_name = g_path_get_basename (event->path); char *next_event_base_name = g_path_get_basename (next_event->path); if (next_event->ev_type == WT_EVENT_CREATE_OR_UPDATE && g_strcmp0(event_base_name, next_event_base_name) == 0) { WTEvent *new_event = wt_event_new (WT_EVENT_RENAME, event->path, next_event->path); pthread_mutex_lock (&status->q_lock); next_event = g_queue_pop_head (status->event_q); pthread_mutex_unlock (&status->q_lock); wt_event_free (event); wt_event_free (next_event); event = new_event; } g_free (event_base_name); g_free (next_event_base_name); } #endif /* Scanned dirs list is used to avoid redundant scan of consecutive CREATE_OR_UPDATE events. When we see other events, we should clear the list. Otherwise in some cases we'll get wrong result. For example, the following sequence (run with a script): 1. Add a dir with files 2. Delete the dir with files 3. Add back the same dir again. */ if (event->ev_type != WT_EVENT_CREATE_OR_UPDATE) { g_list_free_full (scanned_dirs, g_free); scanned_dirs = NULL; } switch (event->ev_type) { case WT_EVENT_CREATE_OR_UPDATE: /* If consecutive CREATE_OR_UPDATE events present in the event queue, only process the last one. */ if (next_event && next_event->ev_type == event->ev_type && strcmp (next_event->path, event->path) == 0) break; /* CREATE_OR_UPDATE event tells us the exact path of changed file/dir. * If the event path is not writable, we don't need to check the paths * under the event path. */ if (!is_path_writable(repo->id, repo->is_readonly, event->path)) { char *filename = g_path_get_basename (event->path); if (seaf_repo_manager_is_ignored_hidden_file(filename)) { g_free (filename); break; } g_free (filename); char *fullpath = g_build_path(PATH_SEPERATOR, repo->worktree, event->path, NULL); struct cache_entry *ce = index_name_exists(istate, event->path, strlen(event->path), 0); SeafStat st; if (ce != NULL && seaf_stat (fullpath, &st) == 0 && ce->ce_mtime.sec == st.st_mtime && ce->ce_size == st.st_size) { g_free (fullpath); break; } send_file_sync_error_notification (repo->id, repo->name, event->path, SYNC_ERROR_ID_UPDATE_TO_READ_ONLY_REPO); seaf_debug ("%s is not writable, ignore.\n", event->path); g_free (fullpath); break; } #if defined WIN32 || defined __APPLE__ office_path = NULL; if (is_office_lock_file (repo->worktree, event->path, &office_path)) lock_office_file_on_server (repo, office_path); g_free (office_path); #endif if (handle_add_files (repo, istate, crypt, ignore_list, fset, status, event, &scanned_dirs, &total_size)) goto out; break; case WT_EVENT_SCAN_DIR: if (handle_add_files (repo, istate, crypt, ignore_list, fset, status, event, &scanned_dirs, &total_size)) goto out; break; case WT_EVENT_DELETE: seaf_sync_manager_delete_active_path (seaf->sync_mgr, repo->id, event->path); #if defined WIN32 || defined __APPLE__ office_path = NULL; if (is_office_lock_file (repo->worktree, event->path, &office_path)) unlock_office_file_on_server (repo, office_path); g_free (office_path); #endif if (check_full_path_ignore(repo->worktree, event->path, ignore_list)) break; if (!is_path_writable(repo->id, repo->is_readonly, event->path)) { seaf_debug ("%s is not writable, ignore.\n", event->path); break; } if (seaf_filelock_manager_is_file_locked (seaf->filelock_mgr, repo->id, event->path)) { seaf_debug ("Delete: %s is locked on server, ignore.\n", event->path); /* send_sync_error_notification (repo->id, NULL, event->path, */ /* SYNC_ERROR_ID_FILE_LOCKED); */ break; } if (check_locked_file_before_remove (fset, event->path)) { not_found = FALSE; remove_from_index_with_prefix (istate, event->path, ¬_found); if (not_found) scan_subtree_for_deletion (repo->id, istate, repo->worktree, event->path, ignore_list, fset, repo->is_readonly, &scanned_del_dirs, repo->changeset); remove_from_changeset (repo->changeset, DIFF_STATUS_DELETED, event->path, FALSE, NULL); try_add_empty_parent_dir_entry_from_wt (repo->worktree, istate, ignore_list, event->path); } break; case WT_EVENT_RENAME: handle_rename (repo, istate, crypt, ignore_list, fset, event, &scanned_del_dirs, &total_size); break; case WT_EVENT_ATTRIB: if (!is_path_writable(repo->id, repo->is_readonly, event->path)) { seaf_debug ("%s is not writable, ignore.\n", event->path); break; } update_attributes (repo, istate, repo->worktree, event->path); break; case WT_EVENT_OVERFLOW: seaf_warning ("Kernel event queue overflowed, fall back to scan.\n"); scan_worktree_for_changes (istate, repo, crypt, ignore_list, fset); break; } if (event == last_event) { wt_event_free (event); seaf_message ("All events are processed for repo %s.\n", repo->id); status->partial_commit = FALSE; break; } else wt_event_free (event); } out: wt_status_unref (status); string_list_free (scanned_dirs); string_list_free (scanned_del_dirs); return 0; } static int index_add (SeafRepo *repo, struct index_state *istate, gboolean is_force_commit, GList **event_list) { SeafileCrypt *crypt = NULL; LockedFileSet *fset = NULL; GList *ignore_list = NULL; int ret = 0; if (repo->encrypted) { crypt = seafile_crypt_new (repo->enc_version, repo->enc_key, repo->enc_iv); } #if defined WIN32 || defined __APPLE__ if (repo->version > 0) fset = seaf_repo_manager_get_locked_file_set (seaf->repo_mgr, repo->id); #endif ignore_list = seaf_repo_load_ignore_files (repo->worktree); if (!is_force_commit) { if (apply_worktree_changes_to_index (repo, istate, crypt, ignore_list, fset, event_list) < 0) { seaf_warning ("Failed to apply worktree changes to index.\n"); ret = -1; } } else if (scan_worktree_for_changes (istate, repo, crypt, ignore_list, fset) < 0) { seaf_warning ("Failed to scan worktree for changes.\n"); ret = -1; } seaf_repo_free_ignore_files (ignore_list); #if defined WIN32 || defined __APPLE__ locked_file_set_free (fset); #endif g_free (crypt); return ret; } static int commit_tree (SeafRepo *repo, const char *root_id, const char *desc, char commit_id[]) { SeafCommit *commit; commit = seaf_commit_new (NULL, repo->id, root_id, repo->email ? repo->email : "unknown", seaf->client_id, desc, 0); commit->parent_id = g_strdup (repo->head->commit_id); /* Add this computer's name to commit. */ commit->device_name = g_strdup(seaf->client_name); commit->client_version = g_strdup (SEAFILE_CLIENT_VERSION); seaf_repo_to_commit (repo, commit); if (seaf_commit_manager_add_commit (seaf->commit_mgr, commit) < 0) return -1; seaf_branch_set_commit (repo->head, commit->commit_id); seaf_branch_manager_update_branch (seaf->branch_mgr, repo->head); strcpy (commit_id, commit->commit_id); seaf_commit_unref (commit); return 0; } static gboolean compare_index_changeset (struct index_state *istate, ChangeSet *changeset) { struct cache_entry *ce; int i; gboolean ret = TRUE; for (i = 0; i < istate->cache_nr; ++i) { ce = istate->cache[i]; if (!(ce->ce_flags & CE_ADDED)) continue; seaf_message ("checking %s in changeset.\n", ce->name); if (!changeset_check_path (changeset, ce->name, ce->sha1, ce->ce_mode, ce->ce_mtime.sec)) ret = FALSE; } return ret; } #if 0 static int print_index (struct index_state *istate) { int i; struct cache_entry *ce; char id[41]; seaf_message ("Totally %u entries in index, version %u.\n", istate->cache_nr, istate->version); for (i = 0; i < istate->cache_nr; ++i) { ce = istate->cache[i]; rawdata_to_hex (ce->sha1, id, 20); seaf_message ("%s, %s, %o, %"G_GINT64_FORMAT", %s, %"G_GINT64_FORMAT", %d\n", ce->name, id, ce->ce_mode, ce->ce_mtime.sec, ce->modifier, ce->ce_size, ce_stage(ce)); } return 0; } #endif static void print_event_log (const char *repo_name, const char *repo_id, const char *commit_id, GList *event_list) { GList *ptr; WTEvent *event; char *name; int i = 0; GString *msg = g_string_new (""); g_string_append_printf (msg, "%s %s %s\n", repo_name, repo_id, commit_id); for (ptr = event_list; ptr; ptr = ptr->next) { event = ptr->data; i++; switch (event->ev_type) { case WT_EVENT_CREATE_OR_UPDATE: name = "create/update"; break; case WT_EVENT_SCAN_DIR: name = "scan dir"; break; case WT_EVENT_DELETE: name = "delete"; break; case WT_EVENT_RENAME: name = "rename"; break; case WT_EVENT_OVERFLOW: name = "overflow"; break; default: name = "unknown"; } g_string_append_printf (msg, "[event %d] %s, %s %s\n", i, name, event->path, event->new_path?event->new_path:""); } seafile_event_message (msg->str); g_string_free (msg, TRUE); } char * seaf_repo_index_commit (SeafRepo *repo, gboolean is_force_commit, gboolean is_initial_commit, GError **error) { SeafRepoManager *mgr = repo->manager; struct index_state istate; char index_path[SEAF_PATH_MAX]; SeafCommit *head = NULL; char *new_root_id = NULL; char commit_id[41]; ChangeSet *changeset = NULL; GList *diff_results = NULL; char *desc = NULL; char *ret = NULL; GList *event_list = NULL; if (!check_worktree_common (repo)) return NULL; memset (&istate, 0, sizeof(istate)); snprintf (index_path, SEAF_PATH_MAX, "%s/%s", mgr->index_dir, repo->id); if (read_index_from (&istate, index_path, repo->version) < 0) { seaf_warning ("Failed to load index.\n"); g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_INTERNAL, "Internal data structure error"); return NULL; } changeset = changeset_new (repo->id); if (!changeset) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_INTERNAL, "Internal data structure error"); goto out; } repo->changeset = changeset; if (index_add (repo, &istate, is_force_commit, &event_list) < 0) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Failed to add"); goto out; } if (!istate.cache_changed) goto out; new_root_id = commit_tree_from_changeset (changeset); if (!new_root_id) { seaf_warning ("Create commit tree failed for repo %s\n", repo->id); g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Failed to generate commit"); goto out; } head = seaf_commit_manager_get_commit (seaf->commit_mgr, repo->id, repo->version, repo->head->commit_id); if (!head) { seaf_warning ("Head commit %s for repo %s not found\n", repo->head->commit_id, repo->id); g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_INTERNAL, "Data corrupt"); goto out; } if (strcmp (head->root_id, new_root_id) == 0) { seaf_message ("No change to the fs tree of repo %s\n", repo->id); /* If no file modification and addition are missing, and the new root * id is the same as the old one, skip commiting. */ if (!is_initial_commit && !is_force_commit) compare_index_changeset (&istate, changeset); update_index (&istate, index_path); goto out; } diff_commit_roots (repo->id, repo->version, head->root_id, new_root_id, &diff_results, TRUE); desc = diff_results_to_description (diff_results); if (!desc) desc = g_strdup(""); if (commit_tree (repo, new_root_id, desc, commit_id) < 0) { seaf_warning ("Failed to save commit file"); g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_INTERNAL, "Internal error"); goto out; } if (event_list) { event_list = g_list_reverse (event_list); print_event_log (repo->name, repo->id, commit_id, event_list); } if (update_index (&istate, index_path) < 0) { g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_INTERNAL, "Internal error"); goto out; } g_signal_emit_by_name (seaf, "repo-committed", repo); ret = g_strdup(commit_id); out: if (event_list) { g_list_free_full (event_list, (GDestroyNotify)wt_event_free); } g_free (desc); seaf_commit_unref (head); g_free (new_root_id); changeset_free (changeset); g_list_free_full (diff_results, (GDestroyNotify)diff_entry_free); discard_index (&istate); return ret; } #ifdef DEBUG_UNPACK_TREES static void print_unpack_result (struct index_state *result) { int i; struct cache_entry *ce; for (i = 0; i < result->cache_nr; ++i) { ce = result->cache[i]; printf ("%s\t", ce->name); if (ce->ce_flags & CE_UPDATE) printf ("update/add\n"); else if (ce->ce_flags & CE_WT_REMOVE) printf ("remove\n"); else printf ("unchange\n"); } } static int print_index (struct index_state *istate) { printf ("Index timestamp: %d\n", istate->timestamp.sec); int i; struct cache_entry *ce; char id[41]; printf ("Totally %u entries in index.\n", istate->cache_nr); for (i = 0; i < istate->cache_nr; ++i) { ce = istate->cache[i]; rawdata_to_hex (ce->sha1, id, 20); printf ("%s\t%s\t%o\t%d\t%d\n", ce->name, id, ce->ce_mode, ce->ce_ctime.sec, ce->ce_mtime.sec); } return 0; } #endif /* DEBUG_UNPACK_TREES */ GList * seaf_repo_diff (SeafRepo *repo, const char *old, const char *new, int fold_dir_diff, char **error) { SeafCommit *c1 = NULL, *c2 = NULL; int ret = 0; GList *diff_entries = NULL; c2 = seaf_commit_manager_get_commit (seaf->commit_mgr, repo->id, repo->version, new); if (!c2) { *error = g_strdup("Can't find new commit"); return NULL; } if (old == NULL || old[0] == '\0') { if (c2->parent_id && c2->second_parent_id) { ret = diff_merge (c2, &diff_entries, fold_dir_diff); seaf_commit_unref (c2); if (ret < 0) { *error = g_strdup("Failed to do diff"); g_list_free_full (diff_entries, (GDestroyNotify)diff_entry_free); return NULL; } return diff_entries; } if (!c2->parent_id) { seaf_commit_unref (c2); return NULL; } c1 = seaf_commit_manager_get_commit (seaf->commit_mgr, repo->id, repo->version, c2->parent_id); } else { c1 = seaf_commit_manager_get_commit (seaf->commit_mgr, repo->id, repo->version, old); } if (!c1) { *error = g_strdup("Can't find old commit"); seaf_commit_unref (c2); return NULL; } /* do diff */ ret = diff_commits (c1, c2, &diff_entries, fold_dir_diff); if (ret < 0) { g_list_free_full (diff_entries, (GDestroyNotify)diff_entry_free); diff_entries = NULL; *error = g_strdup("Failed to do diff"); } seaf_commit_unref (c1); seaf_commit_unref (c2); return diff_entries; } int checkout_empty_dir (const char *worktree, const char *name, gint64 mtime, struct cache_entry *ce) { char *path; path = build_checkout_path (worktree, name, strlen(name)); if (!path) return FETCH_CHECKOUT_FAILED; if (!seaf_util_exists (path) && seaf_util_mkdir (path, 0777) < 0) { seaf_warning ("Failed to create empty dir %s in checkout.\n", path); g_free (path); return FETCH_CHECKOUT_FAILED; } if (mtime != 0 && seaf_set_file_time (path, mtime) < 0) { seaf_warning ("Failed to set mtime for %s.\n", path); } SeafStat st; seaf_stat (path, &st); fill_stat_cache_info (ce, &st); g_free (path); return FETCH_CHECKOUT_SUCCESS; } static struct cache_entry * cache_entry_from_diff_entry (DiffEntry *de) { int size, namelen; struct cache_entry *ce; namelen = strlen(de->name); size = cache_entry_size(namelen); ce = calloc(1, size); memcpy(ce->name, de->name, namelen); ce->ce_flags = namelen; memcpy (ce->sha1, de->sha1, 20); ce->modifier = g_strdup(de->modifier); ce->ce_size = de->size; ce->ce_mtime.sec = de->mtime; if (S_ISREG(de->mode)) ce->ce_mode = create_ce_mode (de->mode); else ce->ce_mode = S_IFDIR; return ce; } #define UPDATE_CACHE_SIZE_LIMIT 100 * (1 << 20) /* 100MB */ typedef struct FileTxData { char repo_id[37]; int repo_version; SeafileCrypt *crypt; HttpTxTask *http_task; char conflict_head_id[41]; GAsyncQueue *finished_tasks; const char *worktree; LockedFileSet *fset; } FileTxData; typedef struct FileTxTask { char *path; struct cache_entry *ce; DiffEntry *de; gboolean new_ce; gboolean skip_fetch; int result; gboolean no_checkout; gboolean force_conflict; } FileTxTask; static void file_tx_task_free (FileTxTask *task) { if (!task) return; g_free (task->path); g_free (task); } struct _UpdateAux { int fd; SeafileCrypt *crypt; char *content; int size; void *user_data; }; typedef struct _UpdateAux UpdateAux; static size_t fill_block (void *contents, size_t realsize, void *userp) { UpdateAux *aux = userp; int rc = 0; int ret = realsize; char *dec_out = NULL; int dec_out_len = -1; if (aux->crypt) { rc = seafile_decrypt (&dec_out, &dec_out_len, contents, realsize, aux->crypt); if (rc != 0) { seaf_warning ("Decrypt block failed.\n"); return -1; } ret = writen (aux->fd, dec_out, dec_out_len); } else { ret = writen (aux->fd, contents, realsize); } g_free (dec_out); return ret; } static size_t update_block_cb (void *contents, size_t size, size_t nmemb, void *userp) { size_t realsize = size * nmemb; UpdateAux *aux = userp; HttpTxTask *task = aux->user_data; int ret = realsize; if (task) { if (task->state == HTTP_TASK_STATE_CANCELED || task->all_stop) { return 0; } } aux->size += realsize; if (fill_block (contents, realsize, aux) < 0) { return 0; } /* Update global transferred bytes. */ g_atomic_int_add (&(seaf->sync_mgr->recv_bytes), realsize); /* Update transferred bytes for this task */ if (task) g_atomic_int_add (&task->tx_bytes, realsize); /* If uploaded bytes exceeds the limit, wait until the counter * is reset. We check the counter every 100 milliseconds, so we * can waste up to 100 milliseconds without sending data after * the counter is reset. */ while (1) { gint sent = g_atomic_int_get(&(seaf->sync_mgr->recv_bytes)); if (seaf->sync_mgr->download_limit > 0 && sent > seaf->sync_mgr->download_limit) /* 100 milliseconds */ g_usleep (100000); else break; } return ret; } static size_t update_enc_block_cb (void *contents, size_t size, size_t nmemb, void *userp) { size_t realsize = size * nmemb; UpdateAux *aux = userp; HttpTxTask *task = aux->user_data; int ret = realsize; if (task) { if (task->state == HTTP_TASK_STATE_CANCELED || task->all_stop) { return 0; } } aux->content = g_realloc (aux->content, aux->size + realsize); if (!aux->content) { seaf_warning ("Not enough memory.\n"); return 0; } memcpy (aux->content + aux->size, contents, realsize); aux->size += realsize; /* Update global transferred bytes. */ g_atomic_int_add (&(seaf->sync_mgr->recv_bytes), realsize); /* Update transferred bytes for this task */ if (task) g_atomic_int_add (&task->tx_bytes, realsize); /* If uploaded bytes exceeds the limit, wait until the counter * is reset. We check the counter every 100 milliseconds, so we * can waste up to 100 milliseconds without sending data after * the counter is reset. */ while (1) { gint sent = g_atomic_int_get(&(seaf->sync_mgr->recv_bytes)); if (seaf->sync_mgr->download_limit > 0 && sent > seaf->sync_mgr->download_limit) /* 100 milliseconds */ g_usleep (100000); else break; } return ret; } static int checkout_block_cb (const char *repo_id, const char *block_id, int fd, SeafileCrypt *crypt, CheckoutBlockAux *user_data) { HttpTxTask *task = user_data->task; int ret = 0; int error_id = SYNC_ERROR_ID_NO_ERROR; UpdateAux aux = {0}; aux.fd = fd; aux.crypt = crypt; aux.user_data = task; if (crypt) { if (http_tx_manager_get_block(seaf->http_tx_mgr, user_data->repo_id, block_id, user_data->host, user_data->token, user_data->use_fileserver_port, &error_id, update_enc_block_cb, &aux) < 0) { if (task->state == HTTP_TASK_STATE_CANCELED) { ret = -1; goto out; } if (task->error == SYNC_ERROR_ID_NO_ERROR) { task->error = error_id; } ret = -1; seaf_warning ("Failed to get block %s from server.\n", block_id); goto out; } if (fill_block(aux.content, aux.size, &aux) < 0) { ret = -1; seaf_warning ("Failed to fill block %s.\n", block_id); goto out; } } else { if (http_tx_manager_get_block(seaf->http_tx_mgr, user_data->repo_id, block_id, user_data->host, user_data->token, user_data->use_fileserver_port, &error_id, update_block_cb, &aux) < 0) { if (task->state == HTTP_TASK_STATE_CANCELED) { ret = -1; goto out; } if (task->error == SYNC_ERROR_ID_NO_ERROR) { task->error = error_id; } ret = -1; seaf_warning ("Failed to get block %s from server.\n", block_id); goto out; } } if (task) task->done_download += aux.size; out: g_free (aux.content); return ret; } static gboolean check_path_conflict (const char *path, char **orig_path) { gboolean is_conflict = FALSE; GError *error = NULL; is_conflict = g_regex_match (conflict_pattern, path, 0, NULL); if (is_conflict) { *orig_path = g_regex_replace_literal (conflict_pattern, path, -1, 0, "", 0, &error); if (!*orig_path) is_conflict = FALSE; } return is_conflict; } /* static void cleanup_file_blocks_http (HttpTxTask *task, const char *file_id) { Seafile *file; int i; char *block_id; int *pcnt; file = seaf_fs_manager_get_seafile (seaf->fs_mgr, task->repo_id, task->repo_version, file_id); if (!file) { seaf_warning ("Failed to load seafile object %s:%s\n", task->repo_id, file_id); return; } for (i = 0; i < file->n_blocks; ++i) { block_id = file->blk_sha1s[i]; pthread_mutex_lock (&task->ref_cnt_lock); pcnt = g_hash_table_lookup (task->blk_ref_cnts, block_id); if (pcnt) { --(*pcnt); if (*pcnt > 0) { pthread_mutex_unlock (&task->ref_cnt_lock); continue; } } seaf_block_manager_remove_block (seaf->block_mgr, task->repo_id, task->repo_version, block_id); g_hash_table_remove (task->blk_ref_cnts, block_id); pthread_mutex_unlock (&task->ref_cnt_lock); } seafile_unref (file); } */ // just skip all downloaded blocks. static void calculate_block_offset(Seafile *file, gint64 *block_map, gint64 *skip_buffer_size, int *block_offset, gint64 requested_offset) { gint64 offset = 0; int i = 0; *skip_buffer_size = 0; for (; i < file->n_blocks ; ++i) { offset += block_map[i]; if (offset > requested_offset) break; *skip_buffer_size = offset; } *block_offset = i; } #define CACHE_BLOCK_MAP_THRESHOLD (2 << 23) /* 8MB */ static void check_and_get_block_offset (SeafRepoManager *mgr, CheckoutBlockAux *aux, const char *repo_id, int version, const char *file_id, const char *file_path, gint64 *skip_buffer_size, int *block_offset) { Seafile *seafile = NULL; SeafStat st; int n_blocks = 0; gint64 *block_map = NULL; char *tmp_path = NULL; gboolean path_exists = FALSE; seafile = seaf_fs_manager_get_seafile (seaf->fs_mgr, repo_id, version, file_id); if (!seafile) { return; } if (seafile->file_size <= CACHE_BLOCK_MAP_THRESHOLD) { goto out; } tmp_path = g_strconcat (file_path, SEAF_TMP_EXT, NULL); path_exists = (seaf_stat (tmp_path, &st) == 0); if (!path_exists) { goto out; } char file_id_attr[41]; ssize_t len; len = seaf_getxattr (tmp_path, SEAFILE_FILE_ID_ATTR, file_id_attr, sizeof(file_id_attr)); if (len < 0) { goto out; } // File has been changed on server. if (g_strcmp0 (file_id, file_id_attr) != 0) { goto out; } pthread_rwlock_rdlock (&mgr->priv->block_map_lock); block_map = g_hash_table_lookup (mgr->priv->block_map_cache_table, file_id); pthread_rwlock_unlock (&mgr->priv->block_map_lock); if (!block_map) { if (http_tx_manager_get_file_block_map (seaf->http_tx_mgr, repo_id, file_id, aux->host, aux->token, aux->use_fileserver_port, &block_map, &n_blocks) == 0) { if (n_blocks != seafile->n_blocks) { seaf_warning ("Block number return from server does not match" "seafile object. File-id is %s." "Returned %d, expect %d\n", seafile->file_id, n_blocks, seafile->n_blocks); } else { pthread_rwlock_wrlock (&mgr->priv->block_map_lock); g_hash_table_replace (mgr->priv->block_map_cache_table, g_strdup(file_id), block_map); pthread_rwlock_unlock (&mgr->priv->block_map_lock); } } } if (block_map) { calculate_block_offset(seafile, block_map, skip_buffer_size, block_offset, st.st_size); } out: g_free (tmp_path); seafile_unref (seafile); } int seaf_repo_manager_checkout_file (SeafRepo *repo, const char *file_id, const char *file_path, guint32 mode, guint64 mtime, SeafileCrypt *crypt, const char *in_repo_path, const char *conflict_head_id, gboolean force_conflict, gboolean *conflicted, const char *email, CheckoutBlockAux *aux) { int ret; gint64 skip_buffer_size = 0; int block_offset = 0; if (!crypt) check_and_get_block_offset (seaf->repo_mgr, aux, repo->id, repo->version, file_id, file_path, &skip_buffer_size, &block_offset); FileCheckoutData file_data = {0}; int error_id = FETCH_CHECKOUT_SUCCESS; file_data.repo_id = repo->id; file_data.version = repo->version; file_data.file_id = file_id; file_data.file_path = file_path; file_data.mode = mode; file_data.mtime = mtime; file_data.crypt = crypt; file_data.in_repo_path = in_repo_path; file_data.conflict_head_id = conflict_head_id; file_data.force_conflict = force_conflict; file_data.conflicted = conflicted; file_data.email = email; file_data.skip_buffer_size = skip_buffer_size; file_data.block_offset = block_offset; ret = seaf_fs_manager_checkout_file (seaf->fs_mgr, &file_data, &error_id, checkout_block_cb, aux); return ret; } static int checkout_file_http (FileTxData *data, FileTxTask *file_task) { char *repo_id = data->repo_id; int repo_version = data->repo_version; struct cache_entry *ce = file_task->ce; DiffEntry *de = file_task->de; SeafileCrypt *crypt = data->crypt; gboolean no_checkout = file_task->no_checkout; gboolean force_conflict = file_task->force_conflict; const char *worktree = data->worktree; const char *conflict_head_id = data->conflict_head_id; LockedFileSet *fset = data->fset; HttpTxTask *http_task = data->http_task; SeafStat st; char file_id[41]; gboolean locked_on_server = FALSE; if (no_checkout) return FETCH_CHECKOUT_SUCCESS; if (should_ignore_on_checkout (de->name, NULL)) return FETCH_CHECKOUT_SUCCESS; rawdata_to_hex (de->sha1, file_id, 20); locked_on_server = seaf_filelock_manager_is_file_locked (seaf->filelock_mgr, repo_id, de->name); #if defined WIN32 || defined __APPLE__ if (do_check_file_locked (de->name, worktree, locked_on_server)) { if (!locked_file_set_lookup (fset, de->name)) send_file_sync_error_notification (repo_id, NULL, de->name, SYNC_ERROR_ID_FILE_LOCKED_BY_APP); locked_file_set_add_update (fset, de->name, LOCKED_OP_UPDATE, ce->ce_mtime.sec, file_id); /* Stay in syncing status if the file is locked. */ return FETCH_CHECKOUT_SUCCESS; } #endif /* Temporarily unlock the file if it's locked on server, so that the client * itself can write to it. */ if (locked_on_server) seaf_filelock_manager_unlock_wt_file (seaf->filelock_mgr, repo_id, de->name); /* then checkout the file. */ gboolean conflicted = FALSE; CheckoutBlockAux *aux = g_new0 (CheckoutBlockAux, 1); aux->repo_id = g_strdup (repo_id); aux->host = g_strdup (http_task->host); aux->token = g_strdup (http_task->token); aux->use_fileserver_port = http_task->use_fileserver_port; aux->task = http_task; gint64 skip_buffer_size = 0; int block_offset = 0; if (!crypt) check_and_get_block_offset (seaf->repo_mgr, aux, repo_id, repo_version, file_id, file_task->path, &skip_buffer_size, &block_offset); if (skip_buffer_size > 0) http_task->done_download += skip_buffer_size; FileCheckoutData file_data = {0}; int error_id = FETCH_CHECKOUT_SUCCESS; file_data.repo_id = repo_id; file_data.version = repo_version; file_data.file_id = file_id; file_data.file_path = file_task->path; file_data.mode = de->mode; file_data.mtime = de->mtime; file_data.crypt = crypt; file_data.in_repo_path = de->name; file_data.conflict_head_id = conflict_head_id; file_data.force_conflict = force_conflict; file_data.conflicted = &conflicted; if (http_task->username) file_data.email = http_task->username; else file_data.email = http_task->email; file_data.skip_buffer_size = skip_buffer_size; file_data.block_offset = block_offset; if (seaf_fs_manager_checkout_file (seaf->fs_mgr, &file_data, &error_id, checkout_block_cb, aux) < 0) { seaf_warning ("Failed to checkout file %s.\n", file_task->path); if (seaf_filelock_manager_is_file_locked (seaf->filelock_mgr, repo_id, de->name)) seaf_filelock_manager_lock_wt_file (seaf->filelock_mgr, repo_id, de->name); free_checkout_block_aux (aux); return error_id; } free_checkout_block_aux (aux); if (locked_on_server) seaf_filelock_manager_lock_wt_file (seaf->filelock_mgr, repo_id, de->name); // cleanup_file_blocks_http (http_task, file_id); if (conflicted) { send_file_sync_error_notification (repo_id, NULL, de->name, SYNC_ERROR_ID_CONFLICT); } else if (!http_task->is_clone) { char *orig_path = NULL; if (check_path_conflict (de->name, &orig_path)) send_file_sync_error_notification (repo_id, NULL, orig_path, SYNC_ERROR_ID_CONFLICT); g_free (orig_path); } /* finally fill cache_entry info */ /* Only update index if we checked out the file without any error * or conflicts. The ctime of the entry will remain 0 if error. */ seaf_stat (file_task->path, &st); fill_stat_cache_info (ce, &st); return FETCH_CHECKOUT_SUCCESS; } static void fetch_file_thread_func (gpointer data, gpointer user_data) { FileTxTask *task = data; FileTxData *tx_data = user_data; GAsyncQueue *finished_tasks = tx_data->finished_tasks; DiffEntry *de = task->de; char *repo_id = tx_data->repo_id; char file_id[41]; gboolean is_clone = tx_data->http_task->is_clone; int repo_version = tx_data->repo_version; struct cache_entry *ce = task->ce; SeafileCrypt *crypt = tx_data->crypt; char *path = task->path; HttpTxTask *http_task = tx_data->http_task; SeafStat st; gboolean path_exists = FALSE; int rc = FETCH_CHECKOUT_SUCCESS; if (task->skip_fetch) goto out; rawdata_to_hex (de->sha1, file_id, 20); path_exists = (seaf_stat (path, &st) == 0); /* seaf_message ("Download file %s for repo %s\n", de->name, repo_id); */ if (!is_clone) seaf_sync_manager_update_active_path (seaf->sync_mgr, repo_id, de->name, de->mode, SYNC_STATUS_SYNCING, TRUE); if (path_exists && S_ISREG(st.st_mode)) { if (st.st_mtime == ce->ce_mtime.sec) { /* Worktree and index are consistent. */ if (memcmp (de->sha1, ce->sha1, 20) == 0) { seaf_debug ("wt and index are consistent. no need to checkout.\n"); task->no_checkout = TRUE; /* Update mode if necessary. */ if (de->mode != ce->ce_mode) { #ifndef WIN32 chmod (path, de->mode & ~S_IFMT); ce->ce_mode = de->mode; #endif } /* Update mtime if necessary. */ if (de->mtime != ce->ce_mtime.sec) { seaf_set_file_time (path, de->mtime); ce->ce_mtime.sec = de->mtime; } fill_stat_cache_info (ce, &st); goto out; } /* otherwise we have to checkout the file. */ } else { if (compare_file_content (path, &st, de->sha1, crypt, repo_version) == 0) { /* This happens after the worktree file was updated, * but the index was not. Just need to update the index. */ seaf_debug ("update index only.\n"); task->no_checkout = TRUE; fill_stat_cache_info (ce, &st); goto out; } else { /* Conflict. The worktree file was updated by the user. */ seaf_message ("File %s is updated by user. " "Will checkout to conflict file later.\n", path); task->force_conflict = TRUE; } } } /* Download the blocks of this file. */ rc = checkout_file_http (tx_data, task); if (http_task->state == HTTP_TASK_STATE_CANCELED) { rc = FETCH_CHECKOUT_CANCELED; seaf_debug ("Transfer canceled.\n"); } else if (rc == FETCH_CHECKOUT_TRANSFER_ERROR) { rc = FETCH_CHECKOUT_TRANSFER_ERROR; seaf_warning ("Transfer failed.\n"); } out: task->result = rc; g_async_queue_push (finished_tasks, task); } static gboolean check_case_conflict (const char *path1, const char *path2, char **conflict_path) { if (!path1 || !path2 || g_strcmp0 (path1, ".") == 0 || g_strcmp0 (path2, ".") == 0) { return FALSE; } // no case conflict if (strcasecmp (path1, path2) != 0) { return FALSE; } char *base_name1 = g_path_get_basename (path1); char *base_name2 = g_path_get_basename (path2); char *parent_dir1 = g_path_get_dirname (path1); char *parent_dir2 = g_path_get_dirname (path2); gboolean ret = FALSE; // case conflict if (strcmp (base_name1, base_name2) != 0) { *conflict_path = g_strdup (path2); ret = TRUE; goto out; } // find conflict path ret = check_case_conflict (parent_dir1, parent_dir2, conflict_path); out: g_free (base_name1); g_free (base_name2); g_free (parent_dir1); g_free (parent_dir2); return ret; } // Since file creation is asynchronous, the file may not have been created locally at the time of checking for case conflicts, // so an additional check for the name of the file being created is required. static gboolean is_adding_files_case_conflict (GList **adding_files, const char *name, char **conflict_path) { #if defined WIN32 || defined __APPLE__ GList *ptr; SeafStat st; ptr = *adding_files; char *path; for (; ptr; ptr = ptr->next) { path = ptr->data; if (check_case_conflict (path, name, conflict_path)){ return TRUE; } } return FALSE; #else return FALSE; #endif } static int schedule_file_fetch (GThreadPool *tpool, const char *repo_id, const char *repo_name, const char *worktree, struct index_state *istate, DiffEntry *de, const char *conflict_head_id, LockedFileSet *fset, GHashTable *pending_tasks, GHashTable *case_conflict_hash, GHashTable *no_case_conflict_hash, GList **adding_files) { struct cache_entry *ce; gboolean new_ce = FALSE; gboolean skip_fetch = FALSE; char *path = NULL; FileTxTask *file_task; gboolean no_checkout = FALSE; char *conflict_path = NULL; ce = index_name_exists (istate, de->name, strlen(de->name), 0); if (!ce) { ce = cache_entry_from_diff_entry (de); new_ce = TRUE; } IgnoreReason reason; if (should_ignore_on_checkout (de->name, &reason)) { seaf_message ("Path %s is invalid on Windows, skip checkout\n", de->name); if (reason == IGNORE_REASON_END_SPACE_PERIOD) send_file_sync_error_notification (repo_id, repo_name, de->name, SYNC_ERROR_ID_PATH_END_SPACE_PERIOD); else if (reason == IGNORE_REASON_INVALID_CHARACTER) send_file_sync_error_notification (repo_id, repo_name, de->name, SYNC_ERROR_ID_PATH_INVALID_CHARACTER); skip_fetch = TRUE; } if (!skip_fetch && (is_path_case_conflict (worktree, de->name, &conflict_path, no_case_conflict_hash) || is_adding_files_case_conflict(adding_files, de->name, &conflict_path))) { if (conflict_path && !g_hash_table_lookup(case_conflict_hash, conflict_path)) { seaf_message ("Path %s is case conflict, skip checkout\n", conflict_path); send_file_sync_error_notification (repo_id, repo_name, conflict_path, SYNC_ERROR_ID_CASE_CONFLICT); g_hash_table_insert (case_conflict_hash, conflict_path, conflict_path); } else if (conflict_path) { g_free (conflict_path); } skip_fetch = TRUE; no_checkout = TRUE; } if (!skip_fetch) { path = build_checkout_path (worktree, de->name, strlen(de->name)); if (!path) { if (new_ce) cache_entry_free (ce); return FETCH_CHECKOUT_FAILED; } } char *de_name = g_strdup(de->name); *adding_files = g_list_prepend (*adding_files, de_name); file_task = g_new0 (FileTxTask, 1); file_task->de = de; file_task->ce = ce; file_task->path = path; file_task->new_ce = new_ce; file_task->skip_fetch = skip_fetch; file_task->no_checkout = no_checkout; if (!g_hash_table_lookup (pending_tasks, de->name)) { g_hash_table_insert (pending_tasks, g_strdup(de->name), file_task); g_thread_pool_push (tpool, file_task, NULL); } else { file_tx_task_free (file_task); } return FETCH_CHECKOUT_SUCCESS; } static void handle_dir_added_de (const char *repo_id, const char *repo_name, const char *worktree, struct index_state *istate, DiffEntry *de, GHashTable *no_case_conflict_hash) { seaf_debug ("Checkout empty dir %s.\n", de->name); struct cache_entry *ce; gboolean add_ce = FALSE; ce = index_name_exists (istate, de->name, strlen(de->name), 0); if (!ce) { ce = cache_entry_from_diff_entry (de); add_ce = TRUE; } IgnoreReason reason; if (should_ignore_on_checkout (de->name, &reason)) { seaf_message ("Path %s is invalid on Windows, skip checkout\n", de->name); if (reason == IGNORE_REASON_END_SPACE_PERIOD) send_file_sync_error_notification (repo_id, repo_name, de->name, SYNC_ERROR_ID_PATH_END_SPACE_PERIOD); else if (reason == IGNORE_REASON_INVALID_CHARACTER) send_file_sync_error_notification (repo_id, repo_name, de->name, SYNC_ERROR_ID_PATH_INVALID_CHARACTER); goto update_index; } if (is_path_case_conflict(worktree, de->name, NULL, no_case_conflict_hash)) { seaf_message ("Path %s is case conflict, skip checkout\n", de->name); send_file_sync_error_notification (repo_id, repo_name, de->name, SYNC_ERROR_ID_CASE_CONFLICT); goto update_index; } checkout_empty_dir (worktree, de->name, de->mtime, ce); seaf_sync_manager_update_active_path (seaf->sync_mgr, repo_id, de->name, de->mode, SYNC_STATUS_SYNCED, TRUE); update_index: if (add_ce) { if (!(ce->ce_flags & CE_REMOVE)) { add_index_entry (istate, ce, (ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE)); } } else ce->ce_mtime.sec = de->mtime; } #define DEFAULT_DOWNLOAD_THREADS 3 static int download_files_http (const char *repo_id, int repo_version, const char *worktree, struct index_state *istate, const char *index_path, SeafileCrypt *crypt, HttpTxTask *http_task, GList *results, const char *conflict_head_id, LockedFileSet *fset, GHashTable *no_case_conflict_hash) { struct cache_entry *ce; DiffEntry *de; gint64 checkout_size = 0; GThreadPool *tpool; GAsyncQueue *finished_tasks; GHashTable *pending_tasks; GHashTable *case_conflict_hash; GList *adding_files = NULL; GList *ptr; FileTxTask *task; int ret = FETCH_CHECKOUT_SUCCESS; gboolean checkout_file_failed = FALSE; finished_tasks = g_async_queue_new (); FileTxData data; memset (&data, 0, sizeof(data)); memcpy (data.repo_id, repo_id, 36); data.repo_version = repo_version; data.crypt = crypt; data.http_task = http_task; memcpy (data.conflict_head_id, conflict_head_id, 40); data.finished_tasks = finished_tasks; data.worktree = worktree; data.fset = fset; tpool = g_thread_pool_new (fetch_file_thread_func, &data, DEFAULT_DOWNLOAD_THREADS, FALSE, NULL); pending_tasks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)file_tx_task_free); case_conflict_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); for (ptr = results; ptr != NULL; ptr = ptr->next) { de = ptr->data; if (de->status == DIFF_STATUS_DIR_ADDED) { handle_dir_added_de (repo_id, http_task->repo_name, worktree, istate, de, no_case_conflict_hash); } else if (de->status == DIFF_STATUS_ADDED || de->status == DIFF_STATUS_MODIFIED) { if (FETCH_CHECKOUT_FAILED == schedule_file_fetch (tpool, repo_id, http_task->repo_name, worktree, istate, de, conflict_head_id, fset, pending_tasks, case_conflict_hash, no_case_conflict_hash, &adding_files)) continue; } } /* If there is no file need to be downloaded, return immediately. */ if (g_hash_table_size(pending_tasks) == 0) { if (results != NULL) update_index (istate, index_path); goto out; } char file_id[41]; while ((task = g_async_queue_pop (finished_tasks)) != NULL) { ce = task->ce; de = task->de; rawdata_to_hex (de->sha1, file_id, 20); /* seaf_message ("Finished downloading file %s for repo %s\n", */ /* de->name, repo_id); */ if (task->result == FETCH_CHECKOUT_CANCELED || task->result == FETCH_CHECKOUT_TRANSFER_ERROR) { ret = task->result; if (task->new_ce) cache_entry_free (task->ce); http_task->all_stop = TRUE; goto out; } int rc = task->result; // Record a file-level sync error when failed to checkout file. if (rc == FETCH_CHECKOUT_FAILED) { if (checkout_file_failed) { seaf_repo_manager_record_sync_error (repo_id, http_task->repo_name, de->name, SYNC_ERROR_ID_CHECKOUT_FILE); } else { checkout_file_failed = TRUE; send_file_sync_error_notification (repo_id, http_task->repo_name, de->name, SYNC_ERROR_ID_CHECKOUT_FILE); } } if (!http_task->is_clone) { SyncStatus status; if (rc == FETCH_CHECKOUT_FAILED) status = SYNC_STATUS_ERROR; else status = SYNC_STATUS_SYNCED; seaf_sync_manager_update_active_path (seaf->sync_mgr, repo_id, de->name, de->mode, status, TRUE); } if (task->new_ce) { if (!(ce->ce_flags & CE_REMOVE)) { add_index_entry (istate, task->ce, (ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE)); } } else { ce->ce_mtime.sec = de->mtime; ce->ce_size = de->size; memcpy (ce->sha1, de->sha1, 20); if (ce->modifier) g_free (ce->modifier); ce->modifier = g_strdup(de->modifier); ce->ce_mode = create_ce_mode (de->mode); } g_hash_table_remove (pending_tasks, de->name); if (g_hash_table_size (pending_tasks) == 0) break; /* Save index file to disk after checking out some size of files. * This way we don't need to re-compare too many files if this * checkout is interrupted. */ checkout_size += ce->ce_size; if (checkout_size >= UPDATE_CACHE_SIZE_LIMIT) { update_index (istate, index_path); checkout_size = 0; } } update_index (istate, index_path); out: /* Wait until all threads exit. * This is necessary when the download is canceled or encountered error. */ g_thread_pool_free (tpool, TRUE, TRUE); /* Free all pending file task structs. */ g_hash_table_destroy (pending_tasks); g_hash_table_destroy (case_conflict_hash); if (adding_files) string_list_free (adding_files); g_async_queue_unref (finished_tasks); return ret; } static gboolean expand_dir_added_cb (SeafFSManager *mgr, const char *path, SeafDirent *dent, void *user_data, gboolean *stop) { GList **expanded = user_data; DiffEntry *de = NULL; unsigned char sha1[20]; hex_to_rawdata (dent->id, sha1, 20); if (S_ISDIR(dent->mode) && strcmp(dent->id, EMPTY_SHA1) == 0) de = diff_entry_new (DIFF_TYPE_COMMITS, DIFF_STATUS_DIR_ADDED, sha1, path); else if (S_ISREG(dent->mode)) de = diff_entry_new (DIFF_TYPE_COMMITS, DIFF_STATUS_ADDED, sha1, path); if (de) { de->mtime = dent->mtime; de->mode = dent->mode; de->modifier = g_strdup(dent->modifier); de->size = dent->size; *expanded = g_list_prepend (*expanded, de); } return TRUE; } /* * Expand DIR_ADDED results into multiple ADDED results. */ static int expand_diff_results (const char *repo_id, int version, const char *remote_root, const char *local_root, GList **results) { GList *ptr, *next; DiffEntry *de; char obj_id[41]; GList *expanded = NULL; ptr = *results; while (ptr) { de = ptr->data; next = ptr->next; if (de->status == DIFF_STATUS_DIR_ADDED) { *results = g_list_delete_link (*results, ptr); rawdata_to_hex (de->sha1, obj_id, 20); if (seaf_fs_manager_traverse_path (seaf->fs_mgr, repo_id, version, remote_root, de->name, expand_dir_added_cb, &expanded) < 0) { diff_entry_free (de); goto error; } diff_entry_free (de); } ptr = next; } expanded = g_list_reverse (expanded); *results = g_list_concat (*results, expanded); return 0; error: g_list_free_full (expanded, (GDestroyNotify)diff_entry_free); return -1; } static int do_rename_in_worktree (DiffEntry *de, const char *worktree) { char *old_path, *new_path; int ret = 0; old_path = g_build_filename (worktree, de->name, NULL); if (seaf_util_exists (old_path)) { new_path = build_checkout_path (worktree, de->new_name, strlen(de->new_name)); if (!new_path) { ret = -1; goto out; } if (seaf_util_rename (old_path, new_path) < 0) { seaf_warning ("Failed to rename %s to %s: %s.\n", old_path, new_path, strerror(errno)); ret = -1; } g_free (new_path); } out: g_free (old_path); return ret; } static gboolean is_built_in_ignored_file (const char *filename) { GPatternSpec **spec = ignore_patterns; while (*spec) { if (g_pattern_match_string(*spec, filename)) return TRUE; spec++; } if (!seaf->sync_extra_temp_file) { spec = office_temp_ignore_patterns; while (*spec) { if (g_pattern_match_string(*spec, filename)) return TRUE; spec++; } } return FALSE; } #ifdef WIN32 /* * @path: path relative to the worktree, utf-8 encoded * @path_w: absolute path include worktree, utf-16 encoded. * Return 0 when successfully deleted the folder; otherwise -1. */ static int delete_worktree_dir_recursive_win32 (struct index_state *istate, const char *path, const wchar_t *path_w) { WIN32_FIND_DATAW fdata; HANDLE handle; wchar_t *pattern; wchar_t *sub_path_w; char *sub_path, *dname; int path_len_w; DWORD error; int ret = 0; guint64 mtime; gboolean builtin_ignored = FALSE; path_len_w = wcslen(path_w); pattern = g_new0 (wchar_t, (path_len_w + 3)); wcscpy (pattern, path_w); wcscat (pattern, L"\\*"); handle = FindFirstFileW (pattern, &fdata); g_free (pattern); if (handle == INVALID_HANDLE_VALUE) { seaf_warning ("FindFirstFile failed %s: %lu.\n", path, GetLastError()); return -1; } do { if (wcscmp (fdata.cFileName, L".") == 0 || wcscmp (fdata.cFileName, L"..") == 0) continue; dname = g_utf16_to_utf8 (fdata.cFileName, -1, NULL, NULL, NULL); if (!dname) continue; sub_path_w = g_new0 (wchar_t, path_len_w + wcslen(fdata.cFileName) + 2); wcscpy (sub_path_w, path_w); wcscat (sub_path_w, L"\\"); wcscat (sub_path_w, fdata.cFileName); sub_path = g_strconcat (path, "/", dname, NULL); builtin_ignored = is_built_in_ignored_file(dname); g_free (dname); if (fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (delete_worktree_dir_recursive_win32 (istate, sub_path, sub_path_w) < 0) { ret = -1; } } else { struct cache_entry *ce; /* Files like .DS_Store and Thumbs.db should be deleted any way. */ if (!builtin_ignored) { mtime = (guint64)file_time_to_unix_time (&fdata.ftLastWriteTime); ce = index_name_exists (istate, sub_path, strlen(sub_path), 0); if (!ce || (!is_eml_file (dname) && ce->ce_mtime.sec != mtime)) { seaf_message ("File %s is changed, skip deleting it.\n", sub_path); g_free (sub_path_w); g_free (sub_path); ret = -1; continue; } } if (!DeleteFileW (sub_path_w)) { error = GetLastError(); seaf_warning ("Failed to delete file %s: %lu.\n", sub_path, error); ret = -1; } } g_free (sub_path_w); g_free (sub_path); } while (FindNextFileW (handle, &fdata) != 0); error = GetLastError(); if (error != ERROR_NO_MORE_FILES) { seaf_warning ("FindNextFile failed %s: %lu.\n", path, error); ret = -1; } FindClose (handle); if (ret < 0) return ret; int n = 0; while (!RemoveDirectoryW (path_w)) { error = GetLastError(); seaf_warning ("Failed to remove dir %s: %lu.\n", path, error); if (error != ERROR_DIR_NOT_EMPTY) { ret = -1; break; } if (++n >= 3) { ret = -1; break; } /* Sleep 100ms and retry. */ g_usleep (100000); seaf_warning ("Retry remove dir %s.\n", path); } return ret; } #else static int delete_worktree_dir_recursive (struct index_state *istate, const char *path, const char *full_path) { GDir *dir; const char *dname; char *dname_nfc; GError *error = NULL; char *sub_path, *full_sub_path; SeafStat st; int ret = 0; gboolean builtin_ignored = FALSE; dir = g_dir_open (full_path, 0, &error); if (!dir) { seaf_warning ("Failed to open dir %s: %s.\n", full_path, error->message); return -1; } while ((dname = g_dir_read_name (dir)) != NULL) { dname_nfc = g_utf8_normalize (dname, -1, G_NORMALIZE_NFC); sub_path = g_build_path ("/", path, dname_nfc, NULL); full_sub_path = g_build_path ("/", full_path, dname_nfc, NULL); builtin_ignored = is_built_in_ignored_file (dname_nfc); g_free (dname_nfc); if (lstat (full_sub_path, &st) < 0) { seaf_warning ("Failed to stat %s.\n", full_sub_path); g_free (sub_path); g_free (full_sub_path); ret = -1; continue; } if (S_ISDIR(st.st_mode)) { if (delete_worktree_dir_recursive (istate, sub_path, full_sub_path) < 0) ret = -1; } else { struct cache_entry *ce; /* Files like .DS_Store and Thumbs.db should be deleted any way. */ if (!builtin_ignored) { ce = index_name_exists (istate, sub_path, strlen(sub_path), 0); if (!ce || ce->ce_mtime.sec != st.st_mtime) { seaf_message ("File %s is changed, skip deleting it.\n", full_sub_path); g_free (sub_path); g_free (full_sub_path); ret = -1; continue; } } /* Delete all other file types. */ if (seaf_util_unlink (full_sub_path) < 0) { seaf_warning ("Failed to delete file %s: %s.\n", full_sub_path, strerror(errno)); ret = -1; } } g_free (sub_path); g_free (full_sub_path); } g_dir_close (dir); if (ret < 0) return ret; if (g_rmdir (full_path) < 0) { seaf_warning ("Failed to delete dir %s: %s.\n", full_path, strerror(errno)); ret = -1; } return ret; } #endif /* WIN32 */ #define SEAFILE_RECYCLE_BIN_FOLDER "recycle-bin" static int move_dir_to_recycle_bin (const char *dir_path) { char *trash_folder = g_build_path ("/", seaf->worktree_dir, SEAFILE_RECYCLE_BIN_FOLDER, NULL); if (checkdir_with_mkdir (trash_folder) < 0) { seaf_warning ("Seafile trash folder %s doesn't exist and cannot be created.\n", trash_folder); g_free (trash_folder); return -1; } g_free (trash_folder); char *basename = g_path_get_basename (dir_path); char *dst_path = g_build_path ("/", seaf->worktree_dir, SEAFILE_RECYCLE_BIN_FOLDER, basename, NULL); int ret = 0; int n; char *tmp_path; for (n = 1; n < 10; ++n) { if (g_file_test (dst_path, G_FILE_TEST_EXISTS)) { tmp_path = g_strdup_printf ("%s(%d)", dst_path, n); g_free (dst_path); dst_path = tmp_path; continue; } break; } if (seaf_util_rename (dir_path, dst_path) < 0) { seaf_warning ("Failed to move %s to Seafile recycle bin %s: %s\n", dir_path, dst_path, strerror(errno)); ret = -1; goto out; } seaf_message ("Moved folder %s to Seafile recycle bin %s.\n", dir_path, dst_path); out: g_free (basename); g_free (dst_path); return ret; } static void delete_worktree_dir (const char *repo_id, const char *repo_name, struct index_state *istate, const char *worktree, const char *path) { char *full_path = g_build_path ("/", worktree, path, NULL); #ifdef WIN32 wchar_t *full_path_w = win32_long_path (full_path); delete_worktree_dir_recursive_win32 (istate, path, full_path_w); g_free (full_path_w); #else delete_worktree_dir_recursive(istate, path, full_path); #endif /* If for some reason the dir cannot be removed, try to move it to a trash folder * under Seafile folder. Otherwise the removed folder will be created agian on the * server, which will confuse the users. */ if (g_file_test (full_path, G_FILE_TEST_EXISTS)) { if (move_dir_to_recycle_bin (full_path) == 0) send_file_sync_error_notification (repo_id, repo_name, path, SYNC_ERROR_ID_REMOVE_UNCOMMITTED_FOLDER); } g_free (full_path); } static void update_sync_status (struct cache_entry *ce, void *user_data) { char *repo_id = user_data; seaf_sync_manager_update_active_path (seaf->sync_mgr, repo_id, ce->name, ce->ce_mode, SYNC_STATUS_SYNCED, TRUE); } static int convert_rename_to_checkout (const char *repo_id, int repo_version, const char *root_id, DiffEntry *de, GList **entries) { if (de->status == DIFF_STATUS_RENAMED) { char file_id[41]; SeafDirent *dent = NULL; DiffEntry *new_de = NULL; rawdata_to_hex (de->sha1, file_id, 20); dent = seaf_fs_manager_get_dirent_by_path (seaf->fs_mgr, repo_id, repo_version, root_id, de->new_name, NULL); if (!dent) { seaf_warning ("Failed to find %s in repo %s\n", de->new_name, repo_id); return -1; } new_de = diff_entry_new (DIFF_TYPE_COMMITS, DIFF_STATUS_ADDED, de->sha1, de->new_name); if (new_de) { new_de->mtime = dent->mtime; new_de->mode = dent->mode; new_de->modifier = g_strdup(dent->modifier); new_de->size = dent->size; *entries = g_list_prepend (*entries, new_de); } seaf_dirent_free (dent); } else if (de->status == DIFF_STATUS_DIR_RENAMED) { GList *expanded = NULL; if (seaf_fs_manager_traverse_path (seaf->fs_mgr, repo_id, repo_version, root_id, de->new_name, expand_dir_added_cb, &expanded) < 0) { g_list_free_full (expanded, (GDestroyNotify)diff_entry_free); return -1; } *entries = g_list_concat (*entries, expanded); } return 0; } static gboolean load_enc_keys_cb (sqlite3_stmt *stmt, void *vcrypt) { SeafileCrypt *crypt = vcrypt; const char *key, *iv; key = (const char *)sqlite3_column_text(stmt, 0); iv = (const char *)sqlite3_column_text(stmt, 1); if (crypt->version == 1) { hex_to_rawdata (key, crypt->key, 16); hex_to_rawdata (iv, crypt->iv, 16); } else if (crypt->version >= 2) { hex_to_rawdata (key, crypt->key, 32); hex_to_rawdata (iv, crypt->iv, 16); } return FALSE; } static int load_crypt_from_enc_info (SeafRepoManager *manager, const char *repo_id, SeafileCrypt *crypt) { sqlite3 *db = manager->priv->db; char sql[256]; int n; pthread_mutex_lock (&manager->priv->db_lock); snprintf (sql, sizeof(sql), "SELECT key, iv FROM RepoKeys WHERE repo_id='%s'", repo_id); n = sqlite_foreach_selected_row (db, sql, load_enc_keys_cb, crypt); if (n < 0) { pthread_mutex_unlock (&manager->priv->db_lock); return -1; } pthread_mutex_unlock (&manager->priv->db_lock); return 0; } int seaf_repo_fetch_and_checkout (HttpTxTask *http_task, const char *remote_head_id) { char *repo_id; int repo_version; gboolean is_clone; char *worktree; char *passwd; SeafRepo *repo = NULL; SeafBranch *master = NULL; SeafCommit *remote_head = NULL, *master_head = NULL; char index_path[SEAF_PATH_MAX]; struct index_state istate; int ret = FETCH_CHECKOUT_SUCCESS; GList *results = NULL; SeafileCrypt *crypt = NULL; GList *ignore_list = NULL; LockedFileSet *fset = NULL; GHashTable *no_case_conflict_hash = NULL; repo_id = http_task->repo_id; repo_version = http_task->repo_version; is_clone = http_task->is_clone; worktree = http_task->worktree; passwd = http_task->passwd; memset (&istate, 0, sizeof(istate)); snprintf (index_path, SEAF_PATH_MAX, "%s/%s", seaf->repo_mgr->index_dir, repo_id); if (read_index_from (&istate, index_path, repo_version) < 0) { seaf_warning ("Failed to load index.\n"); return FETCH_CHECKOUT_FAILED; } no_case_conflict_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); if (!is_clone) { repo = seaf_repo_manager_get_repo (seaf->repo_mgr, repo_id); if (!repo) { seaf_warning ("Failed to get repo %.8s.\n", repo_id); goto out; } master = seaf_branch_manager_get_branch (seaf->branch_mgr, repo_id, "master"); if (!master) { seaf_warning ("Failed to get master branch for repo %.8s.\n", repo_id); ret = FETCH_CHECKOUT_FAILED; 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 master head %s of repo %.8s.\n", repo_id, master->commit_id); ret = FETCH_CHECKOUT_FAILED; goto out; } } if (!is_clone) worktree = repo->worktree; remote_head = seaf_commit_manager_get_commit (seaf->commit_mgr, repo_id, repo_version, remote_head_id); if (!remote_head) { seaf_warning ("Failed to get remote head %s of repo %.8s.\n", repo_id, remote_head_id); ret = FETCH_CHECKOUT_FAILED; goto out; } if (diff_commit_roots (repo_id, repo_version, master_head ? master_head->root_id : EMPTY_SHA1, remote_head->root_id, &results, TRUE) < 0) { seaf_warning ("Failed to diff for repo %.8s.\n", repo_id); ret = FETCH_CHECKOUT_FAILED; goto out; } GList *ptr; DiffEntry *de; /* Expand DIR_ADDED diff entries. */ if (expand_diff_results (repo_id, repo_version, remote_head->root_id, master_head ? master_head->root_id : EMPTY_SHA1, &results) < 0) { ret = FETCH_CHECKOUT_FAILED; goto out; } #ifdef WIN32 for (ptr = results; ptr; ptr = ptr->next) { de = ptr->data; if (de->status == DIFF_STATUS_DIR_RENAMED || de->status == DIFF_STATUS_DIR_DELETED) { if (do_check_dir_locked (de->name, worktree)) { seaf_message ("File(s) in dir %s are locked by other program, " "skip rename/delete.\n", de->name); send_file_sync_error_notification (repo_id, NULL, de->name, SYNC_ERROR_ID_FOLDER_LOCKED_BY_APP); ret = FETCH_CHECKOUT_LOCKED; goto out; } } else if (de->status == DIFF_STATUS_RENAMED) { gboolean locked_on_server = seaf_filelock_manager_is_file_locked (seaf->filelock_mgr, repo_id, de->name); if (do_check_file_locked (de->name, worktree, locked_on_server)) { seaf_message ("File %s is locked by other program, skip rename.\n", de->name); send_file_sync_error_notification (repo_id, NULL, de->name, SYNC_ERROR_ID_FILE_LOCKED_BY_APP); ret = FETCH_CHECKOUT_LOCKED; goto out; } } } #endif if (remote_head->encrypted) { if (!is_clone) { crypt = seafile_crypt_new (repo->enc_version, repo->enc_key, repo->enc_iv); } else if (passwd){ unsigned char enc_key[32], enc_iv[16]; seafile_decrypt_repo_enc_key (remote_head->enc_version, passwd, remote_head->random_key, remote_head->salt, enc_key, enc_iv); crypt = seafile_crypt_new (remote_head->enc_version, enc_key, enc_iv); } else { // resync an encrypted repo, get key and iv from db. crypt = g_new0 (SeafileCrypt, 1); crypt->version = remote_head->enc_version; load_crypt_from_enc_info (seaf->repo_mgr, repo_id, crypt); } } ignore_list = seaf_repo_load_ignore_files (worktree); struct cache_entry *ce; #if defined WIN32 || defined __APPLE__ fset = seaf_repo_manager_get_locked_file_set (seaf->repo_mgr, repo_id); #endif for (ptr = results; ptr; ptr = ptr->next) { de = ptr->data; if (de->status == DIFF_STATUS_DELETED) { seaf_debug ("Delete file %s.\n", de->name); ce = index_name_exists (&istate, de->name, strlen(de->name), 0); if (!ce) continue; if (should_ignore_on_checkout (de->name, NULL)) { remove_from_index_with_prefix (&istate, de->name, NULL); try_add_empty_parent_dir_entry (worktree, &istate, de->name); continue; } gboolean locked_on_server = seaf_filelock_manager_is_file_locked (seaf->filelock_mgr, repo_id, de->name); if (locked_on_server) seaf_filelock_manager_unlock_wt_file (seaf->filelock_mgr, repo_id, de->name); #if defined WIN32 || defined __APPLE__ if (!do_check_file_locked (de->name, worktree, locked_on_server)) { locked_file_set_remove (fset, de->name, FALSE); if (!is_path_case_conflict (worktree, de->name, NULL, no_case_conflict_hash)) { delete_path (worktree, de->name, de->mode, ce->ce_mtime.sec); } else { seaf_message ("Path %s is case conflict, skip delete\n", de->name); send_file_sync_error_notification (repo_id, NULL, de->name, SYNC_ERROR_ID_CASE_CONFLICT); } } else { if (!locked_file_set_lookup (fset, de->name)) send_file_sync_error_notification (repo_id, http_task->repo_name, de->name, SYNC_ERROR_ID_FILE_LOCKED_BY_APP); locked_file_set_add_update (fset, de->name, LOCKED_OP_DELETE, ce->ce_mtime.sec, NULL); } #else delete_path (worktree, de->name, de->mode, ce->ce_mtime.sec); #endif /* No need to lock wt file again since it's deleted. */ remove_from_index_with_prefix (&istate, de->name, NULL); try_add_empty_parent_dir_entry (worktree, &istate, de->name); } else if (de->status == DIFF_STATUS_DIR_DELETED) { seaf_debug ("Delete dir %s.\n", de->name); /* Nothing to delete. */ if (!master_head || strcmp(master_head->root_id, EMPTY_SHA1) == 0) continue; if (should_ignore_on_checkout (de->name, NULL)) { seaf_message ("Path %s is invalid on Windows, skip delete.\n", de->name); remove_from_index_with_prefix (&istate, de->name, NULL); try_add_empty_parent_dir_entry (worktree, &istate, de->name); continue; } if (!is_path_case_conflict (worktree, de->name, NULL, no_case_conflict_hash)) { delete_worktree_dir (repo_id, http_task->repo_name, &istate, worktree, de->name); } else { seaf_message ("Path %s is case conflict, skip delete\n", de->name); send_file_sync_error_notification (repo_id, NULL, de->name, SYNC_ERROR_ID_CASE_CONFLICT); } /* Remove all index entries under this directory */ remove_from_index_with_prefix (&istate, de->name, NULL); try_add_empty_parent_dir_entry (worktree, &istate, de->name); } } for (ptr = results; ptr; ptr = ptr->next) { de = ptr->data; if (de->status == DIFF_STATUS_RENAMED || de->status == DIFF_STATUS_DIR_RENAMED) { seaf_debug ("Rename %s to %s.\n", de->name, de->new_name); #ifdef WIN32 IgnoreReason reason; if (should_ignore_on_checkout (de->new_name, &reason)) { seaf_message ("Path %s is invalid on Windows, skip rename.\n", de->new_name); if (reason == IGNORE_REASON_END_SPACE_PERIOD) send_file_sync_error_notification (repo_id, http_task->repo_name, de->new_name, SYNC_ERROR_ID_PATH_END_SPACE_PERIOD); else if (reason == IGNORE_REASON_INVALID_CHARACTER) send_file_sync_error_notification (repo_id, http_task->repo_name, de->new_name, SYNC_ERROR_ID_PATH_INVALID_CHARACTER); continue; } else if (should_ignore_on_checkout (de->name, NULL)) { /* If the server renames an invalid path to a valid path, * directly checkout the valid path. The checkout will merge * with any existing files. */ convert_rename_to_checkout (repo_id, repo_version, remote_head->root_id, de, &results); continue; } #endif if (seaf_filelock_manager_is_file_locked (seaf->filelock_mgr, repo_id, de->name)) seaf_filelock_manager_unlock_wt_file (seaf->filelock_mgr, repo_id, de->name); gboolean old_path_conflict = is_path_case_conflict(worktree, de->name, NULL, no_case_conflict_hash); gboolean new_path_conflict = is_path_case_conflict(worktree, de->new_name, NULL, no_case_conflict_hash); if (!old_path_conflict && !new_path_conflict) { do_rename_in_worktree (de, worktree); } else if (old_path_conflict) { seaf_message ("Case conflict path %s is renamed to %s without case conflict, check it out\n", de->name, de->new_name); convert_rename_to_checkout (repo_id, repo_version, remote_head->root_id, de, &results); continue; } else if (new_path_conflict) { // check if file has been changed and delete old path. if (de->status == DIFF_STATUS_DIR_RENAMED) { seaf_message ("Path %s is renamed to %s, which has case conflict and will not be checked out. Delete it\n", de->name, de->new_name); send_file_sync_error_notification (repo_id, NULL, de->new_name, SYNC_ERROR_ID_CASE_CONFLICT); delete_worktree_dir (repo_id, http_task->repo_name, &istate, worktree, de->name); } else { ce = index_name_exists (&istate, de->name, strlen(de->name), 0); if (ce) { seaf_message ("Path %s is renamed to %s, which has case conflict and will not be checked out. Delete it\n", de->name, de->new_name); send_file_sync_error_notification (repo_id, NULL, de->new_name, SYNC_ERROR_ID_CASE_CONFLICT); delete_path (worktree, de->name, de->mode, ce->ce_mtime.sec); } } } /* update_sync_status updates the sync status for each renamed path. * The renamed file/folder becomes "synced" immediately after rename. */ if (!is_clone) rename_index_entries (&istate, de->name, de->new_name, NULL, update_sync_status, repo_id); else rename_index_entries (&istate, de->name, de->new_name, NULL, NULL, NULL); /* Moving files out of a dir may make it empty. */ try_add_empty_parent_dir_entry (worktree, &istate, de->name); } } if (istate.cache_changed) update_index (&istate, index_path); for (ptr = results; ptr; ptr = ptr->next) { de = ptr->data; if (de->status == DIFF_STATUS_ADDED || de->status == DIFF_STATUS_MODIFIED) { http_task->total_download += de->size; } } ret = download_files_http (repo_id, repo_version, worktree, &istate, index_path, crypt, http_task, results, remote_head_id, fset, no_case_conflict_hash); out: discard_index (&istate); seaf_branch_unref (master); seaf_commit_unref (master_head); seaf_commit_unref (remote_head); g_list_free_full (results, (GDestroyNotify)diff_entry_free); g_free (crypt); if (ignore_list) seaf_repo_free_ignore_files (ignore_list); #if defined WIN32 || defined __APPLE__ locked_file_set_free (fset); #endif g_hash_table_destroy (no_case_conflict_hash); return ret; } int seaf_repo_manager_set_repo_worktree (SeafRepoManager *mgr, SeafRepo *repo, const char *worktree) { if (g_access(worktree, F_OK) != 0) return -1; if (repo->worktree) g_free (repo->worktree); repo->worktree = g_strdup(worktree); if (seaf_repo_manager_set_repo_property (mgr, repo->id, "worktree", repo->worktree) < 0) return -1; repo->worktree_invalid = FALSE; return 0; } void seaf_repo_manager_invalidate_repo_worktree (SeafRepoManager *mgr, SeafRepo *repo) { if (repo->worktree_invalid) return; repo->worktree_invalid = TRUE; if (repo->auto_sync && (repo->sync_interval == 0)) { if (seaf_wt_monitor_unwatch_repo (seaf->wt_monitor, repo->id) < 0) { seaf_warning ("failed to unwatch repo %s.\n", repo->id); } } } void seaf_repo_manager_validate_repo_worktree (SeafRepoManager *mgr, SeafRepo *repo) { if (!repo->worktree_invalid) return; repo->worktree_invalid = FALSE; 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.\n", repo->id); } } } SeafRepoManager* seaf_repo_manager_new (SeafileSession *seaf) { SeafRepoManager *mgr = g_new0 (SeafRepoManager, 1); mgr->priv = g_new0 (SeafRepoManagerPriv, 1); mgr->seaf = seaf; mgr->index_dir = g_build_path (PATH_SEPERATOR, seaf->seaf_dir, INDEX_DIR, NULL); pthread_mutex_init (&mgr->priv->db_lock, NULL); mgr->priv->checkout_tasks_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); ignore_patterns = g_new0 (GPatternSpec*, G_N_ELEMENTS(ignore_table)); int i; for (i = 0; ignore_table[i] != NULL; i++) { ignore_patterns[i] = g_pattern_spec_new (ignore_table[i]); } office_temp_ignore_patterns[0] = g_pattern_spec_new("~$*"); /* for files like ~WRL0001.tmp for docx and *.tmp for xlsx and pptx */ office_temp_ignore_patterns[1] = g_pattern_spec_new("*.tmp"); office_temp_ignore_patterns[2] = g_pattern_spec_new(".~lock*#"); office_temp_ignore_patterns[3] = NULL; GError *error = NULL; conflict_pattern = g_regex_new (CONFLICT_PATTERN, 0, 0, &error); if (error) { seaf_warning ("Failed to create regex '%s': %s\n", CONFLICT_PATTERN, error->message); g_clear_error (&error); } office_lock_pattern = g_regex_new (OFFICE_LOCK_PATTERN, 0, 0, &error); if (error) { seaf_warning ("Failed to create regex '%s': %s\n", OFFICE_LOCK_PATTERN, error->message); g_clear_error (&error); } mgr->priv->repo_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); pthread_rwlock_init (&mgr->priv->lock, NULL); mgr->priv->lock_office_job_queue = g_async_queue_new (); pthread_mutex_init (&mgr->priv->errors_lock, NULL); mgr->priv->block_map_cache_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); pthread_rwlock_init (&mgr->priv->block_map_lock, NULL); return mgr; } int seaf_repo_manager_init (SeafRepoManager *mgr) { if (checkdir_with_mkdir (mgr->index_dir) < 0) { seaf_warning ("Index dir %s does not exist and is unable to create\n", mgr->index_dir); return -1; } /* Load all the repos into memory on the client side. */ load_repos (mgr, mgr->seaf->seaf_dir); /* Load folder permissions from db. */ init_folder_perms (mgr); return 0; } static void watch_repos (SeafRepoManager *mgr) { GHashTableIter iter; SeafRepo *repo; gpointer key, value; g_hash_table_iter_init (&iter, mgr->priv->repo_hash); while (g_hash_table_iter_next (&iter, &key, &value)) { repo = value; if (repo->auto_sync && !repo->worktree_invalid && (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.\n", repo->id); /* If we fail to add watch at the beginning, sync manager * will periodically check repo status and retry. */ } } } } #define REMOVE_OBJECTS_BATCH 1000 static int remove_store (const char *top_store_dir, const char *store_id, int *count) { char *obj_dir = NULL; GDir *dir1, *dir2; const char *dname1, *dname2; char *path1, *path2; obj_dir = g_build_filename (top_store_dir, store_id, NULL); dir1 = g_dir_open (obj_dir, 0, NULL); if (!dir1) { g_free (obj_dir); return 0; } seaf_message ("Removing store %s\n", obj_dir); while ((dname1 = g_dir_read_name(dir1)) != NULL) { path1 = g_build_filename (obj_dir, dname1, NULL); dir2 = g_dir_open (path1, 0, NULL); if (!dir2) { seaf_warning ("Failed to open obj dir %s.\n", path1); g_dir_close (dir1); g_free (path1); g_free (obj_dir); return -1; } while ((dname2 = g_dir_read_name(dir2)) != NULL) { path2 = g_build_filename (path1, dname2, NULL); g_unlink (path2); /* To prevent using too much IO, only remove 1000 objects per 5 seconds. */ if (++(*count) > REMOVE_OBJECTS_BATCH) { g_usleep (5 * G_USEC_PER_SEC); *count = 0; } g_free (path2); } g_dir_close (dir2); g_rmdir (path1); g_free (path1); } g_dir_close (dir1); g_rmdir (obj_dir); g_free (obj_dir); return 0; } static void cleanup_deleted_stores_by_type (const char *type) { char *top_store_dir; const char *repo_id; top_store_dir = g_build_filename (seaf->seaf_dir, "deleted_store", type, NULL); GError *error = NULL; GDir *dir = g_dir_open (top_store_dir, 0, &error); if (!dir) { seaf_warning ("Failed to open store dir %s: %s.\n", top_store_dir, error->message); g_free (top_store_dir); return; } int count = 0; while ((repo_id = g_dir_read_name(dir)) != NULL) { remove_store (top_store_dir, repo_id, &count); } g_free (top_store_dir); g_dir_close (dir); } static void * cleanup_deleted_stores (void *vdata) { while (1) { cleanup_deleted_stores_by_type ("commits"); cleanup_deleted_stores_by_type ("fs"); cleanup_deleted_stores_by_type ("blocks"); g_usleep (60 * G_USEC_PER_SEC); } return NULL; } int seaf_repo_manager_start (SeafRepoManager *mgr) { pthread_t tid; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); int rc; watch_repos (mgr); rc = pthread_create (&tid, &attr, cleanup_deleted_stores, NULL); if (rc != 0) { seaf_warning ("Failed to start cleanup thread: %s\n", strerror(rc)); } #if defined WIN32 || defined __APPLE__ rc = pthread_create (&tid, &attr, lock_office_file_worker, mgr->priv->lock_office_job_queue); if (rc != 0) { seaf_warning ("Failed to start lock office file thread: %s\n", strerror(rc)); } #endif return 0; } SeafRepo* seaf_repo_manager_create_new_repo (SeafRepoManager *mgr, const char *name, const char *desc) { SeafRepo *repo; char *repo_id; repo_id = gen_uuid (); repo = seaf_repo_new (repo_id, name, desc); if (!repo) { g_free (repo_id); return NULL; } g_free (repo_id); /* we directly create dir because it shouldn't exist */ /* if (seaf_repo_mkdir (repo, base) < 0) { */ /* seaf_repo_free (repo); */ /* goto out; */ /* } */ seaf_repo_manager_add_repo (mgr, repo); return repo; } int seaf_repo_manager_add_repo (SeafRepoManager *manager, SeafRepo *repo) { char sql[256]; sqlite3 *db = manager->priv->db; pthread_mutex_lock (&manager->priv->db_lock); snprintf (sql, sizeof(sql), "REPLACE INTO Repo VALUES ('%s');", repo->id); sqlite_query_exec (db, sql); pthread_mutex_unlock (&manager->priv->db_lock); /* There may be a "deletion record" for this repo when it was deleted * last time. */ seaf_repo_manager_remove_garbage_repo (manager, repo->id); repo->manager = manager; if (pthread_rwlock_wrlock (&manager->priv->lock) < 0) { seaf_warning ("[repo mgr] failed to lock repo cache.\n"); return -1; } g_hash_table_insert (manager->priv->repo_hash, g_strdup(repo->id), repo); pthread_rwlock_unlock (&manager->priv->lock); return 0; } int seaf_repo_manager_mark_repo_deleted (SeafRepoManager *mgr, SeafRepo *repo) { char sql[256]; pthread_mutex_lock (&mgr->priv->db_lock); snprintf (sql, sizeof(sql), "INSERT INTO DeletedRepo VALUES ('%s')", repo->id); if (sqlite_query_exec (mgr->priv->db, sql) < 0) { pthread_mutex_unlock (&mgr->priv->db_lock); return -1; } pthread_mutex_unlock (&mgr->priv->db_lock); repo->delete_pending = TRUE; return 0; } static gboolean get_garbage_repo_id (sqlite3_stmt *stmt, void *vid_list) { GList **ret = vid_list; char *repo_id; repo_id = g_strdup((const char *)sqlite3_column_text (stmt, 0)); *ret = g_list_prepend (*ret, repo_id); return TRUE; } GList * seaf_repo_manager_list_garbage_repos (SeafRepoManager *mgr) { GList *repo_ids = NULL; pthread_mutex_lock (&mgr->priv->db_lock); sqlite_foreach_selected_row (mgr->priv->db, "SELECT repo_id FROM GarbageRepos", get_garbage_repo_id, &repo_ids); pthread_mutex_unlock (&mgr->priv->db_lock); return repo_ids; } void seaf_repo_manager_remove_garbage_repo (SeafRepoManager *mgr, const char *repo_id) { char sql[256]; pthread_mutex_lock (&mgr->priv->db_lock); snprintf (sql, sizeof(sql), "DELETE FROM GarbageRepos WHERE repo_id='%s'", repo_id); sqlite_query_exec (mgr->priv->db, sql); pthread_mutex_unlock (&mgr->priv->db_lock); } void seaf_repo_manager_remove_repo_ondisk (SeafRepoManager *mgr, const char *repo_id, gboolean add_deleted_record) { char sql[256]; /* We don't need to care about I/O errors here, since we can * GC any unreferenced repo data later. */ if (add_deleted_record) { snprintf (sql, sizeof(sql), "REPLACE INTO GarbageRepos VALUES ('%s')", repo_id); if (sqlite_query_exec (mgr->priv->db, sql) < 0) goto out; } /* Once the item in Repo table is deleted, the repo is gone. * This is the "commit point". */ pthread_mutex_lock (&mgr->priv->db_lock); snprintf (sql, sizeof(sql), "DELETE FROM Repo WHERE repo_id = '%s'", repo_id); if (sqlite_query_exec (mgr->priv->db, sql) < 0) goto out; snprintf (sql, sizeof(sql), "DELETE FROM DeletedRepo WHERE repo_id = '%s'", repo_id); sqlite_query_exec (mgr->priv->db, sql); pthread_mutex_unlock (&mgr->priv->db_lock); /* remove index */ char path[SEAF_PATH_MAX]; snprintf (path, SEAF_PATH_MAX, "%s/%s", mgr->index_dir, repo_id); seaf_util_unlink (path); /* remove branch */ GList *p; GList *branch_list = seaf_branch_manager_get_branch_list (seaf->branch_mgr, repo_id); for (p = branch_list; p; p = p->next) { SeafBranch *b = (SeafBranch *)p->data; seaf_repo_manager_branch_repo_unmap (mgr, b); seaf_branch_manager_del_branch (seaf->branch_mgr, repo_id, b->name); } seaf_branch_list_free (branch_list); /* delete repo property firstly */ seaf_repo_manager_del_repo_property (mgr, repo_id); pthread_mutex_lock (&mgr->priv->db_lock); snprintf (sql, sizeof(sql), "DELETE FROM RepoPasswd WHERE repo_id = '%s'", repo_id); sqlite_query_exec (mgr->priv->db, sql); snprintf (sql, sizeof(sql), "DELETE FROM RepoKeys WHERE repo_id = '%s'", repo_id); sqlite_query_exec (mgr->priv->db, sql); snprintf (sql, sizeof(sql), "DELETE FROM MergeInfo WHERE repo_id = '%s'", repo_id); sqlite_query_exec (mgr->priv->db, sql); snprintf (sql, sizeof(sql), "DELETE FROM LockedFiles WHERE repo_id = '%s'", repo_id); sqlite_query_exec (mgr->priv->db, sql); snprintf (sql, sizeof(sql), "DELETE FROM FolderUserPerms WHERE repo_id = '%s'", repo_id); sqlite_query_exec (mgr->priv->db, sql); snprintf (sql, sizeof(sql), "DELETE FROM FolderGroupPerms WHERE repo_id = '%s'", repo_id); sqlite_query_exec (mgr->priv->db, sql); snprintf (sql, sizeof(sql), "DELETE FROM FolderPermTimestamp WHERE repo_id = '%s'", repo_id); sqlite_query_exec (mgr->priv->db, sql); seaf_filelock_manager_remove (seaf->filelock_mgr, repo_id); out: pthread_mutex_unlock (&mgr->priv->db_lock); } static char * gen_deleted_store_path (const char *type, const char *repo_id) { int n = 1; char *path = NULL; char *name = NULL; path = g_build_filename (seaf->deleted_store, type, repo_id, NULL); while (g_access(path, F_OK) == 0 && n < 10) { g_free (path); name = g_strdup_printf ("%s(%d)", repo_id, n); path = g_build_filename (seaf->deleted_store, type, name, NULL); g_free (name); ++n; } if (n == 10) { g_free (path); return NULL; } return path; } void seaf_repo_manager_move_repo_store (SeafRepoManager *mgr, const char *type, const char *repo_id) { char *src = NULL; char *dst = NULL; src = g_build_filename (seaf->seaf_dir, "storage", type, repo_id, NULL); dst = gen_deleted_store_path (type, repo_id); if (dst) { g_rename (src, dst); } g_free (src); g_free (dst); } /* Move commits, fs stores into "deleted_store" directory. */ static void move_repo_stores (SeafRepoManager *mgr, SeafRepo *repo) { seaf_repo_manager_move_repo_store (mgr, "commits", repo->id); seaf_repo_manager_move_repo_store (mgr, "fs", repo->id); } int seaf_repo_manager_del_repo (SeafRepoManager *mgr, SeafRepo *repo) { seaf_repo_manager_remove_repo_ondisk (mgr, repo->id, (repo->version > 0) ? TRUE : FALSE); seaf_sync_manager_remove_active_path_info (seaf->sync_mgr, repo->id); remove_folder_perms (mgr, repo->id); move_repo_stores (mgr, repo); if (pthread_rwlock_wrlock (&mgr->priv->lock) < 0) { seaf_warning ("[repo mgr] failed to lock repo cache.\n"); return -1; } g_hash_table_remove (mgr->priv->repo_hash, repo->id); pthread_rwlock_unlock (&mgr->priv->lock); #if defined WIN32 || defined __APPLE__ || defined COMPILE_LINUX_WS seaf_notif_manager_unsubscribe_repo (seaf->notif_mgr, repo); #endif seaf_repo_free (repo); return 0; } /* Return the internal Repo in hashtable. The caller should not free the returned Repo. */ SeafRepo* seaf_repo_manager_get_repo (SeafRepoManager *manager, const gchar *id) { SeafRepo *res; if (pthread_rwlock_rdlock (&manager->priv->lock) < 0) { seaf_warning ("[repo mgr] failed to lock repo cache.\n"); return NULL; } res = g_hash_table_lookup (manager->priv->repo_hash, id); pthread_rwlock_unlock (&manager->priv->lock); if (res && !res->delete_pending) return res; return NULL; } gboolean seaf_repo_manager_repo_exists (SeafRepoManager *manager, const gchar *id) { SeafRepo *res; if (pthread_rwlock_rdlock (&manager->priv->lock) < 0) { seaf_warning ("[repo mgr] failed to lock repo cache.\n"); return FALSE; } res = g_hash_table_lookup (manager->priv->repo_hash, id); pthread_rwlock_unlock (&manager->priv->lock); if (res && !res->delete_pending) return TRUE; return FALSE; } static int save_branch_repo_map (SeafRepoManager *manager, SeafBranch *branch) { char *sql; sqlite3 *db = manager->priv->db; pthread_mutex_lock (&manager->priv->db_lock); sql = sqlite3_mprintf ("REPLACE INTO RepoBranch VALUES (%Q, %Q)", branch->repo_id, branch->name); sqlite_query_exec (db, sql); sqlite3_free (sql); pthread_mutex_unlock (&manager->priv->db_lock); return 0; } int seaf_repo_manager_branch_repo_unmap (SeafRepoManager *manager, SeafBranch *branch) { char *sql; sqlite3 *db = manager->priv->db; pthread_mutex_lock (&manager->priv->db_lock); sql = sqlite3_mprintf ("DELETE FROM RepoBranch WHERE branch_name = %Q" " AND repo_id = %Q", branch->name, branch->repo_id); if (sqlite_query_exec (db, sql) < 0) { seaf_warning ("Unmap branch repo failed\n"); pthread_mutex_unlock (&manager->priv->db_lock); sqlite3_free (sql); return -1; } sqlite3_free (sql); pthread_mutex_unlock (&manager->priv->db_lock); return 0; } static void load_repo_commit (SeafRepoManager *manager, SeafRepo *repo, SeafBranch *branch) { SeafCommit *commit; commit = seaf_commit_manager_get_commit_compatible (manager->seaf->commit_mgr, repo->id, branch->commit_id); if (!commit) { seaf_warning ("Commit %s is missing\n", branch->commit_id); repo->is_corrupted = TRUE; return; } set_head_common (repo, branch); seaf_repo_from_commit (repo, commit); seaf_commit_unref (commit); } static gboolean load_keys_cb (sqlite3_stmt *stmt, void *vrepo) { SeafRepo *repo = vrepo; const char *key, *iv; key = (const char *)sqlite3_column_text(stmt, 0); iv = (const char *)sqlite3_column_text(stmt, 1); if (repo->enc_version == 1) { hex_to_rawdata (key, repo->enc_key, 16); hex_to_rawdata (iv, repo->enc_iv, 16); } else if (repo->enc_version >= 2) { hex_to_rawdata (key, repo->enc_key, 32); hex_to_rawdata (iv, repo->enc_iv, 16); } return FALSE; } static int load_repo_passwd (SeafRepoManager *manager, SeafRepo *repo) { sqlite3 *db = manager->priv->db; char sql[256]; int n; pthread_mutex_lock (&manager->priv->db_lock); snprintf (sql, sizeof(sql), "SELECT key, iv FROM RepoKeys WHERE repo_id='%s'", repo->id); n = sqlite_foreach_selected_row (db, sql, load_keys_cb, repo); if (n < 0) { pthread_mutex_unlock (&manager->priv->db_lock); return -1; } pthread_mutex_unlock (&manager->priv->db_lock); return 0; } static gboolean load_property_cb (sqlite3_stmt *stmt, void *pvalue) { char **value = pvalue; char *v = (char *) sqlite3_column_text (stmt, 0); *value = g_strdup (v); /* Only one result. */ return FALSE; } static char * load_repo_property (SeafRepoManager *manager, const char *repo_id, const char *key) { sqlite3 *db = manager->priv->db; char sql[256]; char *value = NULL; pthread_mutex_lock (&manager->priv->db_lock); snprintf(sql, 256, "SELECT value FROM RepoProperty WHERE " "repo_id='%s' and key='%s'", repo_id, key); if (sqlite_foreach_selected_row (db, sql, load_property_cb, &value) < 0) { seaf_warning ("Error read property %s for repo %s.\n", key, repo_id); pthread_mutex_unlock (&manager->priv->db_lock); return NULL; } pthread_mutex_unlock (&manager->priv->db_lock); return value; } static gboolean load_branch_cb (sqlite3_stmt *stmt, void *vrepo) { SeafRepo *repo = vrepo; SeafRepoManager *manager = repo->manager; char *branch_name = (char *) sqlite3_column_text (stmt, 0); SeafBranch *branch = seaf_branch_manager_get_branch (manager->seaf->branch_mgr, repo->id, branch_name); if (branch == NULL) { seaf_warning ("Broken branch name for repo %s\n", repo->id); repo->is_corrupted = TRUE; return FALSE; } load_repo_commit (manager, repo, branch); seaf_branch_unref (branch); /* Only one result. */ return FALSE; } 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 = (g_strcmp0 (basename, repo_name) == 0); g_free (basename); return ret; } static SeafRepo * load_repo (SeafRepoManager *manager, const char *repo_id) { char sql[256]; SeafRepo *repo = seaf_repo_new(repo_id, NULL, NULL); if (!repo) { seaf_warning ("[repo mgr] failed to alloc repo.\n"); return NULL; } repo->manager = manager; snprintf(sql, 256, "SELECT branch_name FROM RepoBranch WHERE repo_id='%s'", repo->id); if (sqlite_foreach_selected_row (manager->priv->db, sql, load_branch_cb, repo) < 0) { seaf_warning ("Error read branch for repo %s.\n", repo->id); seaf_repo_free (repo); return NULL; } /* If repo head is set but failed to load branch or commit. */ if (repo->is_corrupted) { seaf_repo_free (repo); /* remove_repo_ondisk (manager, repo_id); */ return NULL; } /* Repo head may be not set if it's just cloned but not checked out yet. */ if (repo->head == NULL) { /* the repo do not have a head branch, try to load 'master' branch */ SeafBranch *branch = seaf_branch_manager_get_branch (manager->seaf->branch_mgr, repo->id, "master"); if (branch != NULL) { SeafCommit *commit; commit = seaf_commit_manager_get_commit_compatible (manager->seaf->commit_mgr, repo->id, branch->commit_id); if (commit) { seaf_repo_from_commit (repo, commit); seaf_commit_unref (commit); } else { seaf_warning ("[repo-mgr] Can not find commit %s\n", branch->commit_id); repo->is_corrupted = TRUE; } seaf_branch_unref (branch); } else { seaf_warning ("[repo-mgr] Failed to get branch master"); repo->is_corrupted = TRUE; } } if (repo->is_corrupted) { seaf_repo_free (repo); /* remove_repo_ondisk (manager, repo_id); */ return NULL; } load_repo_passwd (manager, repo); char *value; value = load_repo_property (manager, repo->id, REPO_AUTO_SYNC); if (g_strcmp0(value, "false") == 0) { repo->auto_sync = 0; } g_free (value); repo->worktree = load_repo_property (manager, repo->id, "worktree"); if (repo->worktree) repo->worktree_invalid = FALSE; repo->email = load_repo_property (manager, repo->id, REPO_PROP_EMAIL); repo->username = load_repo_property (manager, repo->id, REPO_PROP_USERNAME); repo->token = load_repo_property (manager, repo->id, REPO_PROP_TOKEN); /* May be NULL if this property is not set in db. */ repo->server_url = load_repo_property (manager, repo->id, REPO_PROP_SERVER_URL); if (repo->head != NULL && seaf_repo_check_worktree (repo) < 0) { if (seafile_session_config_get_allow_invalid_worktree(seaf)) { seaf_warning ("Worktree for repo \"%s\" is invalid, but still keep it.\n", repo->name); repo->worktree_invalid = TRUE; } else { seaf_message ("Worktree for repo \"%s\" is invalid, delete it.\n", repo->name); seaf_repo_manager_del_repo (manager, repo); return NULL; } } /* load readonly property */ value = load_repo_property (manager, repo->id, REPO_PROP_IS_READONLY); if (g_strcmp0(value, "true") == 0) repo->is_readonly = TRUE; else repo->is_readonly = FALSE; g_free (value); /* load sync period property */ value = load_repo_property (manager, repo->id, REPO_PROP_SYNC_INTERVAL); if (value) { int interval = atoi(value); if (interval > 0) repo->sync_interval = interval; } g_free (value); if (repo->worktree) { gboolean wt_repo_name_same = is_wt_repo_name_same (repo->worktree, repo->name); value = load_repo_property (manager, repo->id, REPO_SYNC_WORKTREE_NAME); if (g_strcmp0 (value, "true") == 0) { /* If need to sync worktree name with library name, update worktree folder name. */ if (!wt_repo_name_same) update_repo_worktree_name (repo, repo->name, FALSE); } else { /* If an existing repo's worktree folder name is the same as repo name, but * sync_worktree_name property is not set, set it. */ if (wt_repo_name_same) save_repo_property (manager, repo->id, REPO_SYNC_WORKTREE_NAME, "true"); } g_free (value); } g_hash_table_insert (manager->priv->repo_hash, g_strdup(repo->id), repo); return repo; } static sqlite3* open_db (SeafRepoManager *manager, const char *seaf_dir) { sqlite3 *db; char *db_path; db_path = g_build_filename (seaf_dir, "repo.db", NULL); if (sqlite_open_db (db_path, &db) < 0) return NULL; g_free (db_path); manager->priv->db = db; char *sql = "CREATE TABLE IF NOT EXISTS Repo (repo_id TEXT PRIMARY KEY);"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS DeletedRepo (repo_id TEXT PRIMARY KEY);"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS RepoBranch (" "repo_id TEXT PRIMARY KEY, branch_name TEXT);"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS RepoLanToken (" "repo_id TEXT PRIMARY KEY, token TEXT);"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS RepoTmpToken (" "repo_id TEXT, peer_id TEXT, token TEXT, timestamp INTEGER, " "PRIMARY KEY (repo_id, peer_id));"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS RepoPasswd " "(repo_id TEXT PRIMARY KEY, passwd TEXT NOT NULL);"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS RepoKeys " "(repo_id TEXT PRIMARY KEY, key TEXT NOT NULL, iv TEXT NOT NULL);"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS RepoProperty (" "repo_id TEXT, key TEXT, value TEXT);"; sqlite_query_exec (db, sql); sql = "CREATE INDEX IF NOT EXISTS RepoIndex ON RepoProperty (repo_id);"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS MergeInfo (" "repo_id TEXT PRIMARY KEY, in_merge INTEGER, branch TEXT);"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS CommonAncestor (" "repo_id TEXT PRIMARY KEY, ca_id TEXT, head_id TEXT);"; sqlite_query_exec (db, sql); /* Version 1 repos will be added to this table after deletion. * GC will scan this table and remove the objects and blocks for the repos. */ sql = "CREATE TABLE IF NOT EXISTS GarbageRepos (repo_id TEXT PRIMARY KEY);"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS LockedFiles (repo_id TEXT, path TEXT, " "operation TEXT, old_mtime INTEGER, file_id TEXT, new_path TEXT, " "PRIMARY KEY (repo_id, path));"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS FolderUserPerms (" "repo_id TEXT, path TEXT, permission TEXT);"; sqlite_query_exec (db, sql); sql = "CREATE INDEX IF NOT EXISTS folder_user_perms_repo_id_idx " "ON FolderUserPerms (repo_id);"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS FolderGroupPerms (" "repo_id TEXT, path TEXT, permission TEXT);"; sqlite_query_exec (db, sql); sql = "CREATE INDEX IF NOT EXISTS folder_group_perms_repo_id_idx " "ON FolderGroupPerms (repo_id);"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS FolderPermTimestamp (" "repo_id TEXT, timestamp INTEGER, PRIMARY KEY (repo_id));"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS ServerProperty (" "server_url TEXT, key TEXT, value TEXT, PRIMARY KEY (server_url, key));"; sqlite_query_exec (db, sql); sql = "CREATE INDEX IF NOT EXISTS ServerIndex ON ServerProperty (server_url);"; sqlite_query_exec (db, sql); sql = "CREATE TABLE IF NOT EXISTS FileSyncError (" "id INTEGER PRIMARY KEY AUTOINCREMENT, repo_id TEXT, repo_name TEXT, " "path TEXT, err_id INTEGER, timestamp INTEGER);"; sqlite_query_exec (db, sql); sql = "CREATE INDEX IF NOT EXISTS FileSyncErrorIndex ON FileSyncError (repo_id, path)"; sqlite_query_exec (db, sql); return db; } static gboolean load_repo_cb (sqlite3_stmt *stmt, void *vmanager) { SeafRepoManager *manager = vmanager; const char *repo_id; repo_id = (const char *) sqlite3_column_text (stmt, 0); load_repo (manager, repo_id); return TRUE; } static gboolean remove_deleted_repo (sqlite3_stmt *stmt, void *vmanager) { SeafRepoManager *manager = vmanager; const char *repo_id; repo_id = (const char *) sqlite3_column_text (stmt, 0); seaf_repo_manager_remove_repo_ondisk (manager, repo_id, TRUE); return TRUE; } static void load_repos (SeafRepoManager *manager, const char *seaf_dir) { sqlite3 *db = open_db(manager, seaf_dir); if (!db) return; char *sql; sql = "SELECT repo_id FROM DeletedRepo"; if (sqlite_foreach_selected_row (db, sql, remove_deleted_repo, manager) < 0) { seaf_warning ("Error removing deleted repos.\n"); return; } sql = "SELECT repo_id FROM Repo;"; if (sqlite_foreach_selected_row (db, sql, load_repo_cb, manager) < 0) { seaf_warning ("Error read repo db.\n"); return; } } static void save_repo_property (SeafRepoManager *manager, const char *repo_id, const char *key, const char *value) { char *sql; sqlite3 *db = manager->priv->db; pthread_mutex_lock (&manager->priv->db_lock); sql = sqlite3_mprintf ("SELECT repo_id FROM RepoProperty WHERE repo_id=%Q AND key=%Q", repo_id, key); if (sqlite_check_for_existence(db, sql)) { sqlite3_free (sql); sql = sqlite3_mprintf ("UPDATE RepoProperty SET value=%Q" "WHERE repo_id=%Q and key=%Q", value, repo_id, key); sqlite_query_exec (db, sql); sqlite3_free (sql); } else { sqlite3_free (sql); sql = sqlite3_mprintf ("INSERT INTO RepoProperty VALUES (%Q, %Q, %Q)", repo_id, key, value); sqlite_query_exec (db, sql); sqlite3_free (sql); } pthread_mutex_unlock (&manager->priv->db_lock); } int seaf_repo_manager_set_repo_property (SeafRepoManager *manager, const char *repo_id, const char *key, const char *value) { SeafRepo *repo; repo = seaf_repo_manager_get_repo (manager, repo_id); if (!repo) return -1; if (strcmp(key, REPO_AUTO_SYNC) == 0) { if (!seaf->started) { seaf_message ("System not started, skip setting auto sync value.\n"); return 0; } if (g_strcmp0(value, "true") == 0) { repo->auto_sync = 1; if (repo->sync_interval == 0) seaf_wt_monitor_watch_repo (seaf->wt_monitor, repo->id, repo->worktree); repo->last_sync_time = 0; } else { repo->auto_sync = 0; if (repo->sync_interval == 0) seaf_wt_monitor_unwatch_repo (seaf->wt_monitor, repo->id); /* Cancel current sync task if any. */ seaf_sync_manager_cancel_sync_task (seaf->sync_mgr, repo->id); seaf_sync_manager_remove_active_path_info (seaf->sync_mgr, repo->id); } } if (strcmp(key, REPO_PROP_SYNC_INTERVAL) == 0) { if (!seaf->started) { seaf_message ("System not started, skip setting auto sync value.\n"); return 0; } int interval = atoi(value); if (interval > 0) { repo->sync_interval = interval; if (repo->auto_sync) seaf_wt_monitor_unwatch_repo (seaf->wt_monitor, repo->id); } else { repo->sync_interval = 0; if (repo->auto_sync) seaf_wt_monitor_watch_repo (seaf->wt_monitor, repo->id, repo->worktree); } } if (strcmp (key, REPO_PROP_SERVER_URL) == 0) { char *url = canonical_server_url (value); if (!repo->server_url) { /* Called from clone-mgr. */ repo->server_url = url; } else { g_free (repo->server_url); repo->server_url = url; g_free (repo->effective_host); repo->effective_host = NULL; } save_repo_property (manager, repo_id, key, url); return 0; } if (strcmp (key, REPO_PROP_IS_READONLY) == 0) { if (g_strcmp0 (value, "true") == 0) repo->is_readonly = TRUE; else repo->is_readonly = FALSE; } if (strcmp (key, REPO_PROP_USERNAME) == 0) { if (!repo->username) { /* Called from clone-mgr. */ repo->username = g_strdup (value); } else { g_free (repo->username); repo->username = g_strdup(value); } save_repo_property (manager, repo_id, key, value); return 0; } save_repo_property (manager, repo_id, key, value); return 0; } char * seaf_repo_manager_get_repo_property (SeafRepoManager *manager, const char *repo_id, const char *key) { return load_repo_property (manager, repo_id, key); } static void seaf_repo_manager_del_repo_property (SeafRepoManager *manager, const char *repo_id) { char *sql; sqlite3 *db = manager->priv->db; pthread_mutex_lock (&manager->priv->db_lock); sql = sqlite3_mprintf ("DELETE FROM RepoProperty WHERE repo_id = %Q", repo_id); sqlite_query_exec (db, sql); sqlite3_free (sql); pthread_mutex_unlock (&manager->priv->db_lock); } static void seaf_repo_manager_del_repo_property_by_key (SeafRepoManager *manager, const char *repo_id, const char *key) { char *sql; sqlite3 *db = manager->priv->db; pthread_mutex_lock (&manager->priv->db_lock); sql = sqlite3_mprintf ("DELETE FROM RepoProperty " "WHERE repo_id = %Q " " AND key = %Q", repo_id, key); sqlite_query_exec (db, sql); sqlite3_free (sql); pthread_mutex_unlock (&manager->priv->db_lock); } static int save_repo_enc_info (SeafRepoManager *manager, SeafRepo *repo) { sqlite3 *db = manager->priv->db; char sql[512]; char key[65], iv[33]; if (repo->enc_version == 1) { rawdata_to_hex (repo->enc_key, key, 16); rawdata_to_hex (repo->enc_iv, iv, 16); } else if (repo->enc_version >= 2) { rawdata_to_hex (repo->enc_key, key, 32); rawdata_to_hex (repo->enc_iv, iv, 16); } snprintf (sql, sizeof(sql), "REPLACE INTO RepoKeys VALUES ('%s', '%s', '%s')", repo->id, key, iv); if (sqlite_query_exec (db, sql) < 0) return -1; return 0; } int seaf_repo_manager_set_repo_passwd (SeafRepoManager *manager, SeafRepo *repo, const char *passwd) { int ret; if (seafile_decrypt_repo_enc_key (repo->enc_version, passwd, repo->random_key, repo->salt, repo->enc_key, repo->enc_iv) < 0) return -1; pthread_mutex_lock (&manager->priv->db_lock); ret = save_repo_enc_info (manager, repo); pthread_mutex_unlock (&manager->priv->db_lock); return ret; } int seaf_repo_manager_save_repo_enc_info (SeafRepoManager *manager, const char *repo_id, const char *key, const char *iv) { sqlite3 *db = manager->priv->db; char sql[512]; snprintf (sql, sizeof(sql), "REPLACE INTO RepoKeys VALUES ('%s', '%s', '%s')", repo_id, key, iv); if (sqlite_query_exec (db, sql) < 0) return -1; return 0; } int seaf_repo_manager_load_repo_enc_info (SeafRepoManager *manager, SeafRepo *repo) { load_repo_passwd (manager, repo); return 0; } GList* seaf_repo_manager_get_repo_list (SeafRepoManager *manager, int start, int limit) { GList *repo_list = NULL; GHashTableIter iter; SeafRepo *repo; gpointer key, value; if (pthread_rwlock_rdlock (&manager->priv->lock) < 0) { seaf_warning ("[repo mgr] failed to lock repo cache.\n"); return NULL; } g_hash_table_iter_init (&iter, manager->priv->repo_hash); while (g_hash_table_iter_next (&iter, &key, &value)) { repo = value; if (!repo->delete_pending) repo_list = g_list_prepend (repo_list, repo); } pthread_rwlock_unlock (&manager->priv->lock); return repo_list; } GList * seaf_repo_manager_get_repo_id_list_by_server (SeafRepoManager *manager, const char *server_url) { GList *repo_id_list = NULL; GHashTableIter iter; SeafRepo *repo; gpointer key, value; if (pthread_rwlock_rdlock (&manager->priv->lock) < 0) { seaf_warning ("[repo mgr] failed to lock repo cache.\n"); return NULL; } g_hash_table_iter_init (&iter, manager->priv->repo_hash); while (g_hash_table_iter_next (&iter, &key, &value)) { repo = value; if (!repo->delete_pending && g_strcmp0 (repo->server_url, server_url) == 0) repo_id_list = g_list_prepend (repo_id_list, g_strdup(repo->id)); } pthread_rwlock_unlock (&manager->priv->lock); return repo_id_list; } int seaf_repo_manager_set_repo_email (SeafRepoManager *mgr, SeafRepo *repo, const char *email) { g_free (repo->email); repo->email = g_strdup(email); save_repo_property (mgr, repo->id, REPO_PROP_EMAIL, email); return 0; } int seaf_repo_manager_set_repo_token (SeafRepoManager *manager, SeafRepo *repo, const char *token) { g_free (repo->token); repo->token = g_strdup(token); save_repo_property (manager, repo->id, REPO_PROP_TOKEN, token); return 0; } int seaf_repo_manager_remove_repo_token (SeafRepoManager *manager, SeafRepo *repo) { g_free (repo->token); repo->token = NULL; seaf_repo_manager_del_repo_property_by_key(manager, repo->id, REPO_PROP_TOKEN); return 0; } int seaf_repo_manager_set_repo_relay_info (SeafRepoManager *mgr, const char *repo_id, const char *relay_addr, const char *relay_port) { save_repo_property (mgr, repo_id, REPO_PROP_RELAY_ADDR, relay_addr); save_repo_property (mgr, repo_id, REPO_PROP_RELAY_PORT, relay_port); return 0; } void seaf_repo_manager_get_repo_relay_info (SeafRepoManager *mgr, const char *repo_id, char **relay_addr, char **relay_port) { char *addr, *port; addr = load_repo_property (mgr, repo_id, REPO_PROP_RELAY_ADDR); port = load_repo_property (mgr, repo_id, REPO_PROP_RELAY_PORT); if (relay_addr && addr) *relay_addr = addr; if (relay_port && port) *relay_port = port; } static void update_server_properties (SeafRepoManager *mgr, const char *repo_id, const char *new_server_url) { char *old_server_url = NULL; char *sql = NULL; old_server_url = seaf_repo_manager_get_repo_property (mgr, repo_id, REPO_PROP_SERVER_URL); if (!old_server_url) return; pthread_mutex_lock (&mgr->priv->db_lock); sql = sqlite3_mprintf ("UPDATE ServerProperty SET server_url=%Q WHERE " "server_url=%Q;", new_server_url, old_server_url); sqlite_query_exec (mgr->priv->db, sql); pthread_mutex_unlock (&mgr->priv->db_lock); sqlite3_free (sql); g_free (old_server_url); } int seaf_repo_manager_update_repos_server_host (SeafRepoManager *mgr, const char *old_server_url, const char *new_server_url) { GList *ptr, *repos = seaf_repo_manager_get_repo_list (seaf->repo_mgr, 0, -1); SeafRepo *r; char *canon_old_server_url = canonical_server_url(old_server_url); char *canon_new_server_url = canonical_server_url(new_server_url); for (ptr = repos; ptr; ptr = ptr->next) { r = ptr->data; char *server_url = seaf_repo_manager_get_repo_property (seaf->repo_mgr, r->id, REPO_PROP_SERVER_URL); if (g_strcmp0(server_url, canon_old_server_url) == 0) { /* Update server property before server_url is changed. */ update_server_properties (mgr, r->id, canon_new_server_url); seaf_repo_manager_set_repo_property ( seaf->repo_mgr, r->id, REPO_PROP_SERVER_URL, canon_new_server_url); } g_free (server_url); } g_list_free (repos); g_free (canon_old_server_url); g_free (canon_new_server_url); return 0; } char * seaf_repo_manager_get_server_property (SeafRepoManager *mgr, const char *server_url, const char *key) { char *sql = sqlite3_mprintf ("SELECT value FROM ServerProperty WHERE " "server_url=%Q AND key=%Q;", server_url, key); char *value; pthread_mutex_lock (&mgr->priv->db_lock); value = sqlite_get_string (mgr->priv->db, sql); pthread_mutex_unlock (&mgr->priv->db_lock); sqlite3_free (sql); return value; } int seaf_repo_manager_set_server_property (SeafRepoManager *mgr, const char *server_url, const char *key, const char *value) { char *sql; int ret; char *canon_server_url = canonical_server_url(server_url); pthread_mutex_lock (&mgr->priv->db_lock); sql = sqlite3_mprintf ("REPLACE INTO ServerProperty VALUES (%Q, %Q, %Q);", canon_server_url, key, value); ret = sqlite_query_exec (mgr->priv->db, sql); pthread_mutex_unlock (&mgr->priv->db_lock); sqlite3_free (sql); g_free (canon_server_url); return ret; } gboolean seaf_repo_manager_server_is_pro (SeafRepoManager *mgr, const char *server_url) { gboolean ret = FALSE; char *is_pro = seaf_repo_manager_get_server_property (seaf->repo_mgr, server_url, SERVER_PROP_IS_PRO); if (is_pro != NULL && strcasecmp (is_pro, "true") == 0) ret = TRUE; g_free (is_pro); return ret; } /* * Read ignored files from ignore.txt */ GList *seaf_repo_load_ignore_files (const char *worktree) { GList *list = NULL; SeafStat st; FILE *fp; char *full_path, *pattern; char path[SEAF_PATH_MAX]; full_path = g_build_path (PATH_SEPERATOR, worktree, IGNORE_FILE, NULL); if (seaf_stat (full_path, &st) < 0) goto error; if (!S_ISREG(st.st_mode)) goto error; fp = g_fopen(full_path, "r"); if (fp == NULL) goto error; while (fgets(path, SEAF_PATH_MAX, fp) != NULL) { /* remove leading and trailing whitespace, including \n \r. */ g_strstrip (path); /* ignore comment and blank line */ if (path[0] == '#' || path[0] == '\0') continue; /* Change 'foo/' to 'foo/ *'. */ if (path[strlen(path)-1] == '/') pattern = g_strdup_printf("%s/%s*", worktree, path); else pattern = g_strdup_printf("%s/%s", worktree, path); list = g_list_prepend(list, pattern); } fclose(fp); g_free (full_path); return list; error: g_free (full_path); return NULL; } gboolean seaf_repo_check_ignore_file (GList *ignore_list, const char *fullpath) { char *str; SeafStat st; GPatternSpec *ignore_spec; GList *p; str = g_strdup(fullpath); int rc = seaf_stat(str, &st); if (rc == 0 && S_ISDIR(st.st_mode)) { g_free (str); str = g_strconcat (fullpath, "/", NULL); } for (p = ignore_list; p != NULL; p = p->next) { char *pattern = (char *)p->data; ignore_spec = g_pattern_spec_new(pattern); if (g_pattern_match_string(ignore_spec, str)) { g_free (str); g_pattern_spec_free(ignore_spec); return TRUE; } g_pattern_spec_free(ignore_spec); } g_free (str); return FALSE; } /* * Free ignored file list */ void seaf_repo_free_ignore_files (GList *ignore_list) { GList *p; if (ignore_list == NULL) return; for (p = ignore_list; p != NULL; p = p->next) free(p->data); g_list_free (ignore_list); }