seafile/daemon/repo-mgr.c

6308 lines
188 KiB
C

/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
#include "common.h"
#include <glib/gstdio.h>
#ifdef WIN32
#include <windows.h>
#include <shlobj.h>
#endif
#include <ccnet.h>
#include "utils.h"
#define DEBUG_FLAG SEAFILE_DEBUG_SYNC
#include "log.h"
#include "status.h"
#include "vc-utils.h"
#include "merge.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 "unpack-trees.h"
#include "diff-simple.h"
#include "db.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
struct _SeafRepoManagerPriv {
GHashTable *repo_hash;
sqlite3 *db;
pthread_mutex_t db_lock;
GHashTable *checkout_tasks_hash;
pthread_rwlock_t lock;
};
static const char *ignore_table[] = {
/* tmp files under Linux */
"*~",
/* Emacs tmp files */
"#*#",
/* windows image cache */
"Thumbs.db",
/* For Mac */
".DS_Store",
NULL,
};
static GPatternSpec** ignore_patterns;
static GPatternSpec* office_temp_ignore_patterns[4];
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);
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);
}
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);
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);
return 0;
}
static gboolean
load_folder_perm (sqlite3_stmt *stmt, void *data)
{
GList **p_perms = data;
const char *path, *permission;
path = sqlite3_column_text (stmt, 0);
permission = sqlite3_column_text (stmt, 1);
FolderPerm *perm = folder_perm_new (path, permission);
*p_perms = g_list_prepend (*p_perms, perm);
return TRUE;
}
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));
}
GList *
seaf_repo_manager_load_folder_perms (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;
}
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 (GList *user_perms,
GList *group_perms,
gboolean is_repo_readonly,
const char *path)
{
char *permission = NULL;
char *abs_path = NULL;
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);
g_free (abs_path);
if (!permission)
return !is_repo_readonly;
if (strcmp (permission, "rw") == 0)
return TRUE;
else
return FALSE;
}
gboolean
is_repo_id_valid (const char *id)
{
if (!id)
return FALSE;
return is_uuid_valid (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;
repo->net_browsable = 0;
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) {
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->token);
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)
memcpy (repo->magic, commit->magic, 32);
else if (repo->enc_version == 2) {
memcpy (repo->magic, commit->magic, 64);
memcpy (repo->random_key, commit->random_key, 96);
}
}
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)
commit->magic = g_strdup (repo->magic);
else if (commit->enc_version == 2) {
commit->magic = g_strdup (repo->magic);
commit->random_key = g_strdup (repo->random_key);
}
}
commit->no_local_history = repo->no_local_history;
commit->version = repo->version;
}
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) {
g_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 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
should_ignore_on_checkout (const char *file_path)
{
gboolean ret = FALSE;
#ifdef WIN32
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);
#endif
return ret;
}
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)
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;
}
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) < 0) {
g_warning ("Failed to index file %s.\n", path);
return -1;
}
return 0;
}
#define MAX_COMMIT_SIZE 100 * (1 << 20) /* 100MB */
typedef struct _AddOptions {
LockedFileSet *fset;
GList *user_perms;
GList *group_perms;
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;
if (options)
is_writable = is_path_writable(options->user_perms, options->group_perms,
options->is_repo_ro, path);
if (options && options->startup_scan) {
struct cache_entry *ce;
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)
seaf_sync_manager_update_active_path (seaf->sync_mgr,
repo_id,
path,
S_IFREG,
status);
}
if (!is_writable)
return ret;
#ifdef WIN32
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
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);
}
} 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);
}
} else
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);
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;
dir = g_dir_open (full_path, 0, NULL);
if (!dir) {
g_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);
return 0;
}
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);
}
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 (options)
is_writable = is_path_writable(options->user_perms, options->group_perms,
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);
}
if (n == 0 && path[0] != 0 && is_writable) {
if (!params->remain_files || *(params->remain_files) == NULL)
add_empty_dir_to_index (params->istate, path, st);
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;
}
g_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);
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, &params, 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);
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);
}
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);
return 0;
}
if (options)
is_writable = is_path_writable(options->user_perms, options->group_perms,
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);
}
if (data.n == 0 && path[0] != 0 && !params->ignore_empty_dir && is_writable) {
if (!params->remain_files || *(params->remain_files) == NULL)
add_empty_dir_to_index (params->istate, path, st);
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) {
g_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);
/* 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, &params, 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 (!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)
{
#ifdef WIN32
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,
GList *user_perms, GList *group_perms, gboolean is_repo_ro)
{
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 (user_perms, group_perms, is_repo_ro, 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 ((not_exist || (ret == 0 && !S_ISDIR (st.st_mode))
|| !is_empty_dir (path, ignore_list)) &&
(ce->ce_ctime.sec != 0 || ce_stage(ce) != 0))
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;
}
}
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,
GList *user_perms, GList *group_perms)
{
remove_deleted (istate, repo->worktree, "", ignore_list, fset,
user_perms, group_perms, repo->is_readonly);
AddOptions options;
memset (&options, 0, sizeof(options));
options.fset = fset;
options.user_perms = user_perms;
options.group_perms = group_perms;
options.is_repo_ro = repo->is_readonly;
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;
}
#ifndef __APPLE__
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, GList *user_perms, GList *group_perms)
{
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,
user_perms, group_perms, repo->is_readonly);
memset (&options, 0, sizeof(options));
options.fset = fset;
options.user_perms = user_perms;
options.group_perms = group_perms;
options.is_repo_ro = repo->is_readonly;
options.startup_scan = TRUE;
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) {
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.user_perms = user_perms;
options.group_perms = group_perms;
options.is_repo_ro = repo->is_readonly;
/* 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;
}
#else
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, GList *user_perms, GList *group_perms)
{
SeafStat st;
/* 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,
user_perms, group_perms, repo->is_readonly);
*scanned_dirs = g_list_prepend (*scanned_dirs, g_strdup(path));
AddOptions options;
memset (&options, 0, sizeof(options));
options.fset = fset;
options.user_perms = user_perms;
options.group_perms = group_perms;
options.is_repo_ro = repo->is_readonly;
/* 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;
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;
}
if (S_ISREG(st.st_mode)) {
gboolean added = FALSE;
add_to_index (repo->id, repo->version, istate, path, full_path,
&st, 0, crypt, index_cb, repo->email, &added);
if (added) {
*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);
}
} else if (S_ISDIR(st.st_mode)) {
if (is_empty_dir (full_path, ignore_list))
add_empty_dir_to_index (istate, path, &st);
}
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);
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_ce_mode (struct index_state *istate, const char *worktree, const char *path)
{
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)
ce->ce_mode = new_mode;
istate->cache_changed = 1;
}
#ifdef WIN32
static void
scan_subtree_for_deletion (struct index_state *istate,
const char *worktree,
const char *path,
GList *ignore_list,
LockedFileSet *fset,
GList *user_perms,
GList *group_perms,
gboolean is_readonly,
GList **scanned_dirs)
{
wchar_t *path_w;
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';
dir_w = win32_83_path_to_long_path (worktree, path_w, wcslen(path_w));
if (dir_w)
break;
if (!p)
break;
}
if (!dir_w)
dir_w = wcsdup(L"");
dir = g_utf16_to_utf8 (dir_w, -1, NULL, NULL, NULL);
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,
user_perms, group_perms, is_readonly);
out:
g_free (path_w);
g_free (dir_w);
g_free (dir);
}
#else
static void
scan_subtree_for_deletion (struct index_state *istate,
const char *worktree,
const char *path,
GList *ignore_list,
LockedFileSet *fset,
GList *user_perms,
GList *group_perms,
gboolean is_readonly,
GList **scanned_dirs)
{
}
#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,
GList *user_perms, GList *group_perms,
WTStatus *status, WTEvent *event,
GList **scanned_dirs, gint64 *total_size)
{
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,
user_perms, group_perms);
} 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,
user_perms, group_perms);
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);
}
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;
}
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,
GList *user_perms,
GList *group_perms)
{
if (ignored) {
seaf_sync_manager_update_active_path (seaf->sync_mgr,
repo->id,
path,
S_IFREG,
SYNC_STATUS_IGNORED);
} 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 (user_perms, group_perms, 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);
}
}
#ifdef WIN32
typedef struct _UpdatePathData {
SeafRepo *repo;
struct index_state *istate;
GList *ignore_list;
GList *user_perms;
GList *group_perms;
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,
GList *user_perms,
GList *group_perms);
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;
SyncStatus status;
gboolean ignored = FALSE;
SeafStat st;
dname = g_utf16_to_utf8 (fdata->cFileName, -1, NULL, NULL, NULL);
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,
upd_data->user_perms,
upd_data->group_perms);
} else {
update_active_file (upd_data->repo,
path,
&st,
upd_data->istate,
ignored,
upd_data->user_perms,
upd_data->group_perms);
}
g_free (dname);
g_free (path);
}
static void
update_active_path_recursive (SeafRepo *repo,
const char *path,
struct index_state *istate,
GList *ignore_list,
gboolean ignored,
GList *user_perms,
GList *group_perms)
{
char *full_path;
wchar_t *full_path_w;
int ret = 0;
SyncStatus status;
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;
upd_data.user_perms = user_perms;
upd_data.group_perms = group_perms;
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 (user_perms, group_perms, 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);
} 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);
}
}
}
#else
static void
update_active_path_recursive (SeafRepo *repo,
const char *path,
struct index_state *istate,
GList *ignore_list,
gboolean ignored,
GList *user_perms,
GList *group_perms)
{
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 (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, user_perms, group_perms);
} else if (S_ISREG(st.st_mode)) {
update_active_file (repo, sub_path, &st, istate,
ignore_sub, user_perms, group_perms);
}
g_free (dname);
g_free (sub_path);
g_free (full_sub_path);
}
g_dir_close (dir);
/* Don't set sync status for read-only paths, since changes to read-only
* files are ignored.
*/
if (!is_path_writable (user_perms, group_perms, 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);
} 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);
}
}
}
#endif /* WIN32 */
static void
process_active_path (SeafRepo *repo, const char *path,
struct index_state *istate, GList *ignore_list,
GList *user_perms, GList *group_perms)
{
SeafStat st;
SyncStatus status;
gboolean ignored = FALSE;
char *fullpath = g_build_filename (repo->worktree, path, NULL);
if (seaf_stat (fullpath, &st) < 0) {
seaf_warning ("Failed to stat %s: %s.\n", fullpath, strerror(errno));
g_free (fullpath);
return;
}
if (check_full_path_ignore (repo->worktree, path, ignore_list))
ignored = TRUE;
if (S_ISREG(st.st_mode)) {
update_active_file (repo, path, &st, istate, ignored,
user_perms, group_perms);
} else {
update_active_path_recursive (repo, path, istate, ignore_list, ignored,
user_perms, group_perms);
}
g_free (fullpath);
}
#ifdef __APPLE__
static void
process_active_folder (SeafRepo *repo, const char *dir,
struct index_state *istate, GList *ignore_list,
GList *user_perms, GList *group_perms)
{
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,
user_perms, group_perms);
}
for (p = mod; p; p = p->next) {
path = p->data;
process_active_path (repo, path, istate, ignore_list,
user_perms, group_perms);
}
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,
GList *user_perms, GList *group_perms)
{
char *path, *dir;
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,
user_perms, group_perms);
#else
process_active_path (repo, path, istate, ignore_list,
user_perms, group_perms);
#endif
}
}
static int
apply_worktree_changes_to_index (SeafRepo *repo, struct index_state *istate,
SeafileCrypt *crypt, GList *ignore_list,
LockedFileSet *fset,
GList *user_perms, GList *group_perms)
{
WTStatus *status;
WTEvent *event, *next_event;
gboolean not_found;
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,
user_perms, group_perms);
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;
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(user_perms, group_perms,
repo->is_readonly, event->path)) {
seaf_debug ("%s is not writable, ignore.\n", event->path);
break;
}
if (handle_add_files (repo, istate, crypt, ignore_list,
fset, user_perms, group_perms,
status, event,
&scanned_dirs, &total_size))
goto out;
break;
case WT_EVENT_SCAN_DIR:
if (handle_add_files (repo, istate, crypt, ignore_list,
fset, user_perms, group_perms,
status, event,
&scanned_dirs, &total_size))
goto out;
break;
case WT_EVENT_DELETE:
if (!is_path_writable(user_perms, group_perms,
repo->is_readonly, event->path)) {
seaf_debug ("%s is not writable, ignore.\n", event->path);
break;
}
if (check_locked_file_before_remove (fset, event->path)) {
not_found = FALSE;
remove_from_index_with_prefix (istate, event->path, &not_found);
if (not_found)
scan_subtree_for_deletion (istate,
repo->worktree, event->path,
ignore_list, fset,
user_perms, group_perms,
repo->is_readonly,
&scanned_del_dirs);
try_add_empty_parent_dir_entry_from_wt (repo->worktree,
istate,
ignore_list,
event->path);
}
break;
case WT_EVENT_RENAME:
if (!is_path_writable(user_perms, group_perms,
repo->is_readonly, event->path) ||
!is_path_writable(user_perms, group_perms,
repo->is_readonly, event->new_path)) {
seaf_debug ("Rename: %s or %s is not writable, ignore.\n",
event->path, event->new_path);
break;
}
/* If the destination path is ignored, just remove the source path. */
if (check_full_path_ignore (repo->worktree, event->new_path,
ignore_list)) {
if (check_locked_file_before_remove (fset, event->path)) {
not_found = FALSE;
remove_from_index_with_prefix (istate, event->path, &not_found);
if (not_found)
scan_subtree_for_deletion (istate,
repo->worktree, event->path,
ignore_list, fset,
user_perms, group_perms,
repo->is_readonly,
&scanned_del_dirs);
}
break;
}
if (check_locked_file_before_remove (fset, event->path)) {
not_found = FALSE;
rename_index_entries (istate, event->path, event->new_path, &not_found,
NULL, NULL);
if (not_found)
scan_subtree_for_deletion (istate,
repo->worktree, event->path,
ignore_list, fset,
user_perms, group_perms,
repo->is_readonly,
&scanned_del_dirs);
/* 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);
}
AddOptions options;
memset (&options, 0, sizeof(options));
options.fset = fset;
options.is_repo_ro = repo->is_readonly;
/* 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,
NULL, NULL, &options);
break;
case WT_EVENT_ATTRIB:
if (!is_path_writable(user_perms, group_perms,
repo->is_readonly, event->path)) {
seaf_debug ("%s is not writable, ignore.\n", event->path);
break;
}
update_ce_mode (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,
user_perms, group_perms);
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 void
handle_unmerged_index_entries (SeafRepo *repo, struct index_state *istate,
SeafileCrypt *crypt, GList *ignore_list)
{
struct cache_entry **ce_array = istate->cache;
struct cache_entry *ce;
char path[SEAF_PATH_MAX];
unsigned int i;
SeafStat st;
int ret;
GList *unmerged_paths = NULL;
char *last_name = "";
retry:
for (i = 0; i < istate->cache_nr; ++i) {
ce = ce_array[i];
if (ce_stage(ce) == 0)
continue;
snprintf (path, SEAF_PATH_MAX, "%s/%s", repo->worktree, ce->name);
ret = seaf_stat (path, &st);
if (S_ISDIR (ce->ce_mode)) {
if (ret < 0 || !S_ISDIR (st.st_mode)
|| !is_empty_dir (path, ignore_list))
ce->ce_flags |= CE_REMOVE;
else if (strcmp (ce->name, last_name) != 0) {
unmerged_paths = g_list_append (unmerged_paths, g_strdup(ce->name));
last_name = ce->name;
}
} else {
if (ret < 0 || !S_ISREG (st.st_mode))
ce->ce_flags |= CE_REMOVE;
else if (strcmp (ce->name, last_name) != 0) {
unmerged_paths = g_list_append (unmerged_paths, g_strdup(ce->name));
last_name = ce->name;
}
}
}
remove_marked_cache_entries (istate);
GList *ptr;
char *ce_name;
for (ptr = unmerged_paths; ptr; ptr = ptr->next) {
ce_name = ptr->data;
snprintf (path, SEAF_PATH_MAX, "%s/%s", repo->worktree, ce_name);
ret = seaf_stat (path, &st);
if (ret < 0) {
seaf_warning ("Failed to stat %s: %s.\n", path, strerror(errno));
string_list_free (unmerged_paths);
unmerged_paths = NULL;
goto retry;
}
if (S_ISDIR (st.st_mode)) {
if (is_empty_dir (path, ignore_list))
add_empty_dir_to_index (istate, ce_name, &st);
} else {
gboolean added;
add_to_index (repo->id, repo->version, istate, ce_name, path,
&st, 0, crypt, index_cb, repo->email, &added);
}
}
string_list_free (unmerged_paths);
}
static int
index_add (SeafRepo *repo, struct index_state *istate,
gboolean is_force_commit, gboolean handle_unmerged)
{
SeafileCrypt *crypt = NULL;
LockedFileSet *fset = NULL;
GList *ignore_list = NULL;
GList *user_perms = NULL, *group_perms = NULL;
GList *ptr;
int ret = 0;
if (repo->encrypted) {
crypt = seafile_crypt_new (repo->enc_version, repo->enc_key, repo->enc_iv);
}
#ifdef WIN32
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);
user_perms = seaf_repo_manager_load_folder_perms (seaf->repo_mgr,
repo->id,
FOLDER_PERM_TYPE_USER);
group_perms = seaf_repo_manager_load_folder_perms (seaf->repo_mgr,
repo->id,
FOLDER_PERM_TYPE_GROUP);
if (!is_force_commit) {
if (apply_worktree_changes_to_index (repo, istate, crypt, ignore_list, fset,
user_perms, group_perms) < 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,
user_perms, group_perms) < 0) {
seaf_warning ("Failed to scan worktree for changes.\n");
ret = -1;
}
/* If the index contains unmerged entries, check and remove those entries
* in the end, in cases where they were not completely handled in
* apply_worktree_changes_to_index().
*/
if (handle_unmerged)
handle_unmerged_index_entries (repo, istate, crypt, ignore_list);
seaf_repo_free_ignore_files (ignore_list);
#ifdef WIN32
locked_file_set_free (fset);
#endif
for (ptr = user_perms; ptr; ptr = ptr->next)
folder_perm_free ((FolderPerm *)ptr->data);
g_list_free (user_perms);
for (ptr = group_perms; ptr; ptr = ptr->next)
folder_perm_free ((FolderPerm *)ptr->data);
g_list_free (group_perms);
g_free (crypt);
return ret;
}
/*
* Add the files in @worktree to index and return the corresponding
* @root_id. The repo doesn't have to exist.
*/
int
seaf_repo_index_worktree_files (const char *repo_id,
int repo_version,
const char *modifier,
const char *worktree,
const char *passwd,
int enc_version,
const char *random_key,
char *root_id)
{
char index_path[SEAF_PATH_MAX];
struct index_state istate;
unsigned char key[32], iv[16];
SeafileCrypt *crypt = NULL;
struct cache_tree *it = NULL;
GList *ignore_list = NULL;
memset (&istate, 0, sizeof(istate));
snprintf (index_path, SEAF_PATH_MAX, "%s/%s", seaf->repo_mgr->index_dir, repo_id);
/* Remove existing index. An existing index signifies an interrupted
* clone-merge. Removing it assures that new blocks from the worktree
* get added into the repo again (they're deleted by GC).
*/
seaf_util_unlink (index_path);
if (read_index_from (&istate, index_path, repo_version) < 0) {
g_warning ("Failed to load index.\n");
return -1;
}
if (passwd != NULL) {
if (seafile_decrypt_repo_enc_key (enc_version, passwd,
random_key, key, iv) < 0) {
seaf_warning ("Failed to generate enc key for repo %s.\n", repo_id);
goto error;
}
crypt = seafile_crypt_new (enc_version, key, iv);
}
ignore_list = seaf_repo_load_ignore_files(worktree);
/* Add empty dir to index. Otherwise if the repo on relay contains an empty
* dir, we'll fail to detect fast-forward relationship later.
*/
if (add_recursive (repo_id, repo_version, modifier,
&istate, worktree, "", crypt, FALSE, ignore_list,
NULL, NULL, NULL) < 0)
goto error;
remove_deleted (&istate, worktree, "", ignore_list, NULL, NULL, NULL, FALSE);
it = cache_tree ();
if (cache_tree_update (repo_id, repo_version, worktree,
it, istate.cache, istate.cache_nr,
0, 0, commit_trees_cb) < 0) {
g_warning ("Failed to build cache tree");
goto error;
}
rawdata_to_hex (it->sha1, root_id, 20);
if (update_index (&istate, index_path) < 0)
goto error;
discard_index (&istate);
g_free (crypt);
if (it)
cache_tree_free (&it);
seaf_repo_free_ignore_files(ignore_list);
return 0;
error:
discard_index (&istate);
g_free (crypt);
if (it)
cache_tree_free (&it);
seaf_repo_free_ignore_files(ignore_list);
return -1;
}
gboolean
seaf_repo_is_worktree_changed (SeafRepo *repo)
{
SeafRepoManager *mgr = repo->manager;
GList *res = NULL, *p;
struct index_state istate;
char index_path[SEAF_PATH_MAX];
DiffEntry *de;
int pos;
struct cache_entry *ce;
SeafStat sb;
char *full_path;
if (!check_worktree_common (repo))
return FALSE;
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) {
repo->index_corrupted = TRUE;
g_warning ("Failed to load index.\n");
goto error;
}
repo->index_corrupted = FALSE;
wt_status_collect_changes_worktree (&istate, &res, repo->worktree);
if (res != NULL)
goto changed;
wt_status_collect_untracked (&istate, &res, repo->worktree, should_ignore);
if (res != NULL)
goto changed;
wt_status_collect_changes_index (&istate, &res, repo);
if (res != NULL)
goto changed;
discard_index (&istate);
repo->wt_changed = FALSE;
/* g_debug ("%s worktree is changed\n", repo->id); */
return FALSE;
changed:
g_message ("Worktree changes (at most 5 files are shown):\n");
int i = 0;
for (p = res; p != NULL && i < 5; p = p->next, ++i) {
de = p->data;
full_path = g_build_path ("/", repo->worktree, de->name, NULL);
if (seaf_stat (full_path, &sb) < 0) {
g_warning ("Failed to stat %s: %s.\n", full_path, strerror(errno));
g_free (full_path);
continue;
}
g_free (full_path);
pos = index_name_pos (&istate, de->name, strlen(de->name));
if (pos < 0) {
g_warning ("Cannot find diff entry %s in index.\n", de->name);
continue;
}
ce = istate.cache[pos];
g_message ("type: %c, status: %c, name: %s, "
"ce mtime: %"G_GINT64_FORMAT", ce size: %" G_GUINT64_FORMAT ", "
"file mtime: %d, file size: %" G_GUINT64_FORMAT "\n",
de->type, de->status, de->name,
ce->ce_mtime.sec, ce->ce_size, (int)sb.st_mtime, sb.st_size);
}
for (p = res; p; p = p->next) {
de = p->data;
diff_entry_free (de);
}
g_list_free (res);
discard_index (&istate);
repo->wt_changed = TRUE;
/* g_debug ("%s worktree is changed\n", repo->id); */
return TRUE;
error:
return FALSE;
}
/*
* Generate commit description based on files to be commited.
* It only checks index status, not worktree status.
* So it should be called after "add" completes.
* This way we can always get the correct list of files to be
* commited, even we were interrupted in the last add-commit
* sequence.
*/
static char *
gen_commit_description (SeafRepo *repo, struct index_state *istate)
{
GList *p;
GList *results = NULL;
char *desc;
wt_status_collect_changes_index (istate, &results, repo);
diff_resolve_empty_dirs (&results);
diff_resolve_renames (&results);
desc = diff_results_to_description (results);
if (!desc)
return NULL;
for (p = results; p; p = p->next) {
DiffEntry *de = p->data;
diff_entry_free (de);
}
g_list_free (results);
return desc;
}
gboolean
seaf_repo_is_index_unmerged (SeafRepo *repo)
{
SeafRepoManager *mgr = repo->manager;
struct index_state istate;
char index_path[SEAF_PATH_MAX];
gboolean ret = FALSE;
if (!repo->head)
return FALSE;
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) {
g_warning ("Failed to load index.\n");
return FALSE;
}
if (unmerged_index (&istate))
ret = TRUE;
discard_index (&istate);
return ret;
}
static int
commit_tree (SeafRepo *repo, struct cache_tree *it,
const char *desc, char commit_id[],
gboolean unmerged)
{
SeafCommit *commit;
char root_id[41];
rawdata_to_hex (it->sha1, root_id, 20);
commit = seaf_commit_new (NULL, repo->id, root_id,
repo->email ? repo->email
: seaf->session->base.user_name,
seaf->session->base.id,
desc, 0);
if (repo->head)
commit->parent_id = g_strdup (repo->head->commit_id);
if (unmerged) {
SeafRepoMergeInfo minfo;
/* Don't use head commit of master branch since that branch may have
* been updated after the last merge.
*/
memset (&minfo, 0, sizeof(minfo));
if (seaf_repo_manager_get_merge_info (repo->manager, repo->id, &minfo) < 0) {
seaf_warning ("Failed to get merge info of repo %.10s.\n", repo->id);
return -1;
}
commit->second_parent_id = g_strdup (minfo.remote_head);
commit->new_merge = TRUE;
commit->conflict = TRUE;
}
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
need_handle_unmerged_index (SeafRepo *repo, struct index_state *istate)
{
if (!unmerged_index (istate))
return FALSE;
/* Syncing with an existing directory may require a real merge.
* If the merge produced conflicts, the index will be unmerged.
* But we don't want to generate a merge commit in this case.
* An "index" branch should exist in this case.
*/
if (seaf_branch_manager_branch_exists (seaf->branch_mgr, repo->id, "index"))
return FALSE;
return TRUE;
}
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;
}
char *
seaf_repo_index_commit (SeafRepo *repo, const char *desc, gboolean is_force_commit,
GError **error)
{
SeafRepoManager *mgr = repo->manager;
struct index_state istate;
struct cache_tree *it;
char index_path[SEAF_PATH_MAX];
char commit_id[41];
gboolean unmerged = FALSE;
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) {
g_warning ("Failed to load index.\n");
g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_INTERNAL, "Internal data structure error");
return NULL;
}
if (need_handle_unmerged_index (repo, &istate))
unmerged = TRUE;
if (index_add (repo, &istate, is_force_commit, unmerged) < 0) {
g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Failed to add");
goto error;
}
/* Commit before updating the index, so that new blocks won't be GC'ed. */
char *my_desc = g_strdup(desc);
if (my_desc[0] == '\0') {
char *gen_desc = gen_commit_description (repo, &istate);
if (!gen_desc) {
/* error not set. */
g_free (my_desc);
/* Still need to update index even nothing to commit. */
update_index (&istate, index_path);
discard_index (&istate);
return NULL;
}
g_free (my_desc);
my_desc = gen_desc;
}
it = cache_tree ();
if (cache_tree_update (repo->id, repo->version,
repo->worktree,
it, istate.cache,
istate.cache_nr, 0, 0, commit_trees_cb) < 0) {
g_warning ("Failed to build cache tree");
g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_INTERNAL, "Internal data structure error");
cache_tree_free (&it);
goto error;
}
if (commit_tree (repo, it, my_desc, commit_id, unmerged) < 0) {
g_warning ("Failed to save commit file");
g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_INTERNAL, "Internal error");
cache_tree_free (&it);
goto error;
}
g_free (my_desc);
cache_tree_free (&it);
if (update_index (&istate, index_path) < 0)
goto error;
discard_index (&istate);
g_signal_emit_by_name (seaf, "repo-committed", repo);
return g_strdup(commit_id);
error:
discard_index (&istate);
return NULL;
}
#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 */
int
seaf_repo_checkout_commit (SeafRepo *repo, SeafCommit *commit, gboolean recover_merge,
char **error)
{
SeafRepoManager *mgr = repo->manager;
char index_path[SEAF_PATH_MAX];
struct tree_desc trees[2];
struct unpack_trees_options topts;
struct index_state istate;
gboolean initial_checkout;
GString *err_msgs;
int ret = 0;
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) {
g_warning ("Failed to load index.\n");
return -1;
}
repo->index_corrupted = FALSE;
initial_checkout = is_index_unborn(&istate);
if (!initial_checkout) {
if (!repo->head) {
/* TODO: Set error string*/
g_warning ("Repo corrupt: Index exists but head branch is not set\n");
return -1;
}
SeafCommit *head =
seaf_commit_manager_get_commit (seaf->commit_mgr,
repo->id, repo->version,
repo->head->commit_id);
if (!head) {
seaf_warning ("Failed to get commit %s.\n", repo->head->commit_id);
discard_index (&istate);
return -1;
}
fill_tree_descriptor (repo->id, repo->version, &trees[0], head->root_id);
seaf_commit_unref (head);
} else {
fill_tree_descriptor (repo->id, repo->version, &trees[0], NULL);
}
fill_tree_descriptor (repo->id, repo->version, &trees[1], commit->root_id);
/* 2-way merge to the new branch */
memset(&topts, 0, sizeof(topts));
memcpy (topts.repo_id, repo->id, 36);
topts.version = repo->version;
topts.base = repo->worktree;
topts.head_idx = -1;
topts.src_index = &istate;
/* topts.dst_index = &istate; */
topts.initial_checkout = initial_checkout;
topts.update = 1;
topts.merge = 1;
topts.gently = 0;
topts.verbose_update = 0;
/* topts.debug_unpack = 1; */
topts.fn = twoway_merge;
if (repo->encrypted) {
topts.crypt = seafile_crypt_new (repo->enc_version,
repo->enc_key,
repo->enc_iv);
}
if (unpack_trees (2, trees, &topts) < 0) {
g_warning ("Failed to merge commit %s with work tree.\n", commit->commit_id);
ret = -1;
goto out;
}
#ifdef WIN32
if (!initial_checkout && !recover_merge &&
files_locked_on_windows(&topts.result, repo->worktree)) {
g_debug ("[checkout] files are locked, quit checkout now.\n");
ret = -1;
goto out;
}
#endif
int *finished_entries = NULL;
CheckoutTask *c_task = seaf_repo_manager_get_checkout_task (repo->manager, repo->id);
if (c_task) {
finished_entries = &c_task->finished_files;
}
if (update_worktree (&topts, recover_merge,
initial_checkout ? NULL : commit->commit_id,
commit->creator_name,
finished_entries) < 0) {
g_warning ("Failed to update worktree.\n");
/* Still finish checkout even have I/O errors. */
}
discard_index (&istate);
istate = topts.result;
if (update_index (&istate, index_path) < 0) {
g_warning ("Failed to update index.\n");
ret = -1;
goto out;
}
out:
err_msgs = g_string_new ("");
get_unpack_trees_error_msgs (&topts, err_msgs, OPR_CHECKOUT);
*error = g_string_free (err_msgs, FALSE);
tree_desc_free (&trees[0]);
tree_desc_free (&trees[1]);
g_free (topts.crypt);
discard_index (&istate);
return ret;
}
/**
* Checkout the content of "local" branch to <worktree_parent>/repo-name.
* The worktree will be set to this place too.
*/
int
seaf_repo_checkout (SeafRepo *repo, const char *worktree, char **error)
{
const char *commit_id;
SeafBranch *branch;
SeafCommit *commit;
GString *err_msgs;
/* remove original index */
char index_path[SEAF_PATH_MAX];
snprintf (index_path, SEAF_PATH_MAX, "%s/%s", repo->manager->index_dir, repo->id);
seaf_util_unlink (index_path);
branch = seaf_branch_manager_get_branch (seaf->branch_mgr,
repo->id, "local");
if (!branch) {
g_warning ("[repo-mgr] Checkout repo failed: local branch does not exists\n");
*error = g_strdup ("Repo's local branch does not exists.");
goto error;
}
commit_id = branch->commit_id;
commit = seaf_commit_manager_get_commit (seaf->commit_mgr,
repo->id,
repo->version,
commit_id);
if (!commit) {
err_msgs = g_string_new ("");
g_string_append_printf (err_msgs, "Commit %s does not exist.\n",
commit_id);
g_warning ("%s", err_msgs->str);
*error = g_string_free (err_msgs, FALSE);
seaf_branch_unref (branch);
goto error;
}
if (strcmp(repo->id, commit->repo_id) != 0) {
err_msgs = g_string_new ("");
g_string_append_printf (err_msgs, "Commit %s is not in Repo %s.\n",
commit_id, repo->id);
g_warning ("%s", err_msgs->str);
*error = g_string_free (err_msgs, FALSE);
seaf_commit_unref (commit);
if (branch)
seaf_branch_unref (branch);
goto error;
}
CheckoutTask *task = seaf_repo_manager_get_checkout_task (seaf->repo_mgr,
repo->id);
if (!task) {
seaf_warning ("No checkout task found for repo %.10s.\n", repo->id);
goto error;
}
task->total_files = seaf_fs_manager_count_fs_files (seaf->fs_mgr,
repo->id, repo->version,
commit->root_id);
if (task->total_files < 0) {
seaf_warning ("Failed to count files for repo %.10s .\n", repo->id);
goto error;
}
if (seaf_repo_checkout_commit (repo, commit, FALSE, error) < 0) {
seaf_commit_unref (commit);
if (branch)
seaf_branch_unref (branch);
goto error;
}
seaf_branch_unref (branch);
seaf_commit_unref (commit);
return 0;
error:
return -1;
}
int
seaf_repo_merge (SeafRepo *repo, const char *branch, char **error,
int *merge_status)
{
SeafBranch *remote_branch;
int ret = 0;
if (!check_worktree_common (repo))
return -1;
remote_branch = seaf_branch_manager_get_branch (seaf->branch_mgr,
repo->id,
branch);
if (!remote_branch) {
*error = g_strdup("Invalid remote branch.\n");
goto error;
}
if (g_strcmp0 (remote_branch->repo_id, repo->id) != 0) {
*error = g_strdup ("Remote branch is not in this repository.\n");
seaf_branch_unref (remote_branch);
goto error;
}
ret = merge_branches (repo, remote_branch, error, merge_status);
seaf_branch_unref (remote_branch);
return ret;
error:
return -1;
}
int
checkout_file (const char *repo_id,
int repo_version,
const char *worktree,
const char *name,
const char *file_id,
gint64 mtime,
unsigned int mode,
SeafileCrypt *crypt,
struct cache_entry *ce,
TransferTask *task,
HttpTxTask *http_task,
gboolean is_http,
const char *conflict_head_id,
GHashTable *conflict_hash,
GHashTable *no_conflict_hash,
gboolean download_only)
{
char *path;
SeafStat st, st2;
unsigned char sha1[20];
gboolean path_exists = FALSE;
gboolean case_conflict = FALSE;
gboolean force_conflict = FALSE;
gboolean update_mode_only = FALSE;
#ifndef __linux__
path = build_case_conflict_free_path (worktree, name,
conflict_hash, no_conflict_hash,
&case_conflict,
FALSE);
#else
path = build_checkout_path (worktree, name, strlen(name));
#endif
if (!path)
return FETCH_CHECKOUT_FAILED;
hex_to_rawdata (file_id, sha1, 20);
path_exists = (seaf_stat (path, &st) == 0);
if (path_exists && S_ISREG(st.st_mode)) {
if (st.st_mtime == ce->ce_mtime.sec) {
/* Worktree and index are consistent. */
if (memcmp (sha1, ce->sha1, 20) == 0) {
if (mode == ce->ce_mode) {
/* Worktree and index are all uptodate, no need to checkout.
* This may happen after an interrupted checkout.
*/
seaf_debug ("wt and index are consistent. no need to checkout.\n");
goto update_cache;
} else
update_mode_only = TRUE;
}
/* otherwise we have to checkout the file. */
} else {
if (compare_file_content (path, &st, 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");
goto update_cache;
} 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);
force_conflict = TRUE;
}
}
}
if (update_mode_only) {
#ifdef WIN32
g_free (path);
return FETCH_CHECKOUT_SUCCESS;
#else
chmod (path, mode & ~S_IFMT);
ce->ce_mode = mode;
g_free (path);
return FETCH_CHECKOUT_SUCCESS;
#endif
}
/* Download the blocks of this file. */
int rc;
if (!is_http) {
rc = seaf_transfer_manager_download_file_blocks (seaf->transfer_mgr,
task, file_id);
switch (rc) {
case BLOCK_CLIENT_SUCCESS:
break;
case BLOCK_CLIENT_UNKNOWN:
case BLOCK_CLIENT_FAILED:
case BLOCK_CLIENT_NET_ERROR:
case BLOCK_CLIENT_SERVER_ERROR:
g_free (path);
return FETCH_CHECKOUT_TRANSFER_ERROR;
case BLOCK_CLIENT_CANCELED:
g_free (path);
return FETCH_CHECKOUT_CANCELED;
}
} else {
rc = http_tx_task_download_file_blocks (http_task, file_id);
if (http_task->state == HTTP_TASK_STATE_CANCELED) {
g_free (path);
return FETCH_CHECKOUT_CANCELED;
}
if (rc < 0) {
g_free (path);
return FETCH_CHECKOUT_TRANSFER_ERROR;
}
}
if (download_only) {
g_free (path);
return FETCH_CHECKOUT_SUCCESS;
}
/* The worktree file may have been changed when we're downloading the blocks. */
if (path_exists && S_ISREG(st.st_mode) && !force_conflict) {
seaf_stat (path, &st2);
if (st.st_mtime != st2.st_mtime) {
seaf_message ("File %s is updated by user. "
"Will checkout to conflict file later.\n", path);
force_conflict = TRUE;
}
}
/* then checkout the file. */
gboolean conflicted = FALSE;
if (seaf_fs_manager_checkout_file (seaf->fs_mgr,
repo_id,
repo_version,
file_id,
path,
mode,
mtime,
crypt,
name,
conflict_head_id,
force_conflict,
&conflicted,
is_http ? http_task->email : task->email) < 0) {
seaf_warning ("Failed to checkout file %s.\n", path);
g_free (path);
return FETCH_CHECKOUT_FAILED;
}
/* If case conflict, this file has been checked out to another path.
* Remove the current entry, otherwise it won't be removed later
* since it's timestamp is 0.
*/
if (case_conflict) {
ce->ce_flags |= CE_REMOVE;
g_free (path);
return FETCH_CHECKOUT_SUCCESS;
}
if (conflicted) {
g_free (path);
return FETCH_CHECKOUT_SUCCESS;
}
update_cache:
/* finally fill cache_entry info */
/* Only update index if we checked out the file without any error
* or conflicts. The timestamp of the entry will remain 0 if error
* or conflicted.
*/
seaf_stat (path, &st);
fill_stat_cache_info (ce, &st);
g_free (path);
return FETCH_CHECKOUT_SUCCESS;
}
int
checkout_empty_dir (const char *worktree,
const char *name,
gint64 mtime,
struct cache_entry *ce,
GHashTable *conflict_hash,
GHashTable *no_conflict_hash)
{
char *path;
gboolean case_conflict = FALSE;
#ifndef __linux__
path = build_case_conflict_free_path (worktree, name,
conflict_hash, no_conflict_hash,
&case_conflict,
FALSE);
#else
path = build_checkout_path (worktree, name, strlen(name));
#endif
if (!path)
return FETCH_CHECKOUT_FAILED;
if (!seaf_util_exists (path) && seaf_util_mkdir (path, 0777) < 0) {
g_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) {
g_warning ("Failed to set mtime for %s.\n", path);
}
if (case_conflict) {
ce->ce_flags |= CE_REMOVE;
g_free (path);
return FETCH_CHECKOUT_SUCCESS;
}
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;
}
static void
cleanup_file_blocks (const char *repo_id, int version, const char *file_id)
{
Seafile *file;
int i;
file = seaf_fs_manager_get_seafile (seaf->fs_mgr,
repo_id, version,
file_id);
for (i = 0; i < file->n_blocks; ++i)
seaf_block_manager_remove_block (seaf->block_mgr,
repo_id, version,
file->blk_sha1s[i]);
seafile_unref (file);
}
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)
goto error;
}
ptr = next;
}
expanded = g_list_reverse (expanded);
*results = g_list_concat (*results, expanded);
return 0;
error:
for (ptr = expanded; ptr; ptr = ptr->next)
diff_entry_free ((DiffEntry *)(ptr->data));
return -1;
}
static int
do_rename_in_worktree (DiffEntry *de, const char *worktree,
GHashTable *conflict_hash, GHashTable *no_conflict_hash)
{
char *old_path, *new_path;
gboolean case_conflict;
int ret = 0;
old_path = g_build_filename (worktree, de->name, NULL);
if (seaf_util_exists (old_path)) {
#ifndef __linux__
new_path = build_case_conflict_free_path (worktree, de->new_name,
conflict_hash, no_conflict_hash,
&case_conflict,
TRUE);
#else
new_path = build_checkout_path (worktree, de->new_name, strlen(de->new_name));
#endif
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);
}
g_free (old_path);
return ret;
}
#ifdef WIN32
static void
delete_worktree_dir_recursive_win32 (const char *worktree,
const wchar_t *path_w)
{
WIN32_FIND_DATAW fdata;
HANDLE handle;
wchar_t *pattern;
wchar_t *sub_path_w;
char *path, *sub_path;
int path_len_w;
DWORD error;
path = g_utf16_to_utf8 (path_w, -1, NULL, NULL, NULL);
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());
g_free (path);
g_free (pattern);
return;
}
do {
if (wcscmp (fdata.cFileName, L".") == 0 ||
wcscmp (fdata.cFileName, L"..") == 0)
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);
if (fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
delete_worktree_dir_recursive_win32 (worktree, sub_path_w);
} else {
if (!DeleteFileW (sub_path_w)) {
error = GetLastError();
sub_path = g_utf16_to_utf8 (sub_path_w, -1,
NULL, NULL, NULL);
seaf_warning ("Failed to delete file %s: %lu.\n",
sub_path, error);
g_free (sub_path);
}
}
g_free (sub_path_w);
} while (FindNextFileW (handle, &fdata) != 0);
error = GetLastError();
if (error != ERROR_NO_MORE_FILES) {
seaf_warning ("FindNextFile failed %s: %lu.\n",
path, error);
}
FindClose (handle);
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)
break;
if (++n >= 3)
break;
/* Sleep 100ms and retry. */
g_usleep (100000);
seaf_warning ("Retry remove dir %s.\n", path);
}
g_free (path);
g_free (pattern);
}
#else
static void
delete_worktree_dir_recursive (const char *path)
{
GDir *dir;
const char *dname;
GError *error = NULL;
char *sub_path;
SeafStat st;
dir = g_dir_open (path, 0, &error);
if (!dir) {
seaf_warning ("Failed to open dir %s: %s.\n", path, error->message);
return;
}
while ((dname = g_dir_read_name (dir)) != NULL) {
sub_path = g_build_filename (path, dname, NULL);
if (lstat (sub_path, &st) < 0) {
seaf_warning ("Failed to stat %s.\n", sub_path);
continue;
}
if (S_ISDIR(st.st_mode)) {
delete_worktree_dir_recursive (sub_path);
} else {
/* Delete all other file types. */
if (seaf_util_unlink (sub_path) < 0) {
seaf_warning ("Failed to delete file %s: %s.\n",
sub_path, strerror(errno));
}
}
g_free (sub_path);
}
g_dir_close (dir);
if (g_rmdir (path) < 0) {
seaf_warning ("Failed to delete dir %s: %s.\n", path, strerror(errno));
}
}
#endif /* WIN32 */
static void
delete_worktree_dir (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 (worktree, full_path_w);
g_free (full_path_w);
#else
delete_worktree_dir_recursive(full_path);
#endif
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);
}
#define UPDATE_CACHE_SIZE_LIMIT 100 * (1 << 20) /* 100MB */
int
seaf_repo_fetch_and_checkout (TransferTask *task,
HttpTxTask *http_task,
gboolean is_http,
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;
GHashTable *conflict_hash = NULL, *no_conflict_hash = NULL;
GList *ignore_list = NULL;
LockedFileSet *fset = NULL;
if (is_http) {
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;
} else {
repo_id = task->repo_id;
repo_version = task->repo_version;
is_clone = task->is_clone;
worktree = task->worktree;
passwd = 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) {
g_warning ("Failed to load index.\n");
return FETCH_CHECKOUT_FAILED;
}
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);
ret = FETCH_CHECKOUT_LOCKED;
goto out;
}
} else if (de->status == DIFF_STATUS_RENAMED) {
if (do_check_file_locked (de->name, worktree)) {
seaf_message ("File %s is locked by other program, skip rename.\n",
de->name);
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 {
unsigned char enc_key[32], enc_iv[16];
seafile_decrypt_repo_enc_key (remote_head->enc_version,
passwd,
remote_head->random_key,
enc_key, enc_iv);
crypt = seafile_crypt_new (remote_head->enc_version,
enc_key, enc_iv);
}
}
conflict_hash = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, g_free);
no_conflict_hash = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, NULL);
ignore_list = seaf_repo_load_ignore_files (worktree);
struct cache_entry *ce;
for (ptr = results; ptr; ptr = ptr->next) {
de = ptr->data;
if (de->status == DIFF_STATUS_ADDED || de->status == DIFF_STATUS_MODIFIED) {
if (!is_http)
++(task->n_to_download);
else
++(http_task->n_files);
}
}
#ifdef WIN32
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;
#ifdef WIN32
if (!do_check_file_locked (de->name, worktree)) {
locked_file_set_remove (fset, de->name, FALSE);
delete_path (worktree, de->name, de->mode, ce->ce_mtime.sec);
} else {
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
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;
delete_worktree_dir (worktree, de->name);
/* 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);
do_rename_in_worktree (de, worktree, conflict_hash, no_conflict_hash);
/* 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);
gint64 checkout_size = 0;
int rc;
for (ptr = results; ptr; ptr = ptr->next) {
de = ptr->data;
if (de->status == DIFF_STATUS_ADDED ||
de->status == DIFF_STATUS_MODIFIED) {
seaf_debug ("Checkout file %s.\n", de->name);
gboolean add_ce = FALSE;
gboolean is_locked = FALSE;
char file_id[41];
rawdata_to_hex (de->sha1, file_id, 20);
ce = index_name_exists (&istate, de->name, strlen(de->name), 0);
if (!ce) {
ce = cache_entry_from_diff_entry (de);
add_ce = TRUE;
}
if (!should_ignore_on_checkout (de->name)) {
#ifdef WIN32
is_locked = do_check_file_locked (de->name, worktree);
#endif
if (!is_clone)
seaf_sync_manager_update_active_path (seaf->sync_mgr,
repo_id,
de->name,
de->mode,
SYNC_STATUS_SYNCING);
rc = checkout_file (repo_id,
repo_version,
worktree,
de->name,
file_id,
de->mtime,
de->mode,
crypt,
ce,
task,
http_task,
is_http,
remote_head_id,
conflict_hash,
no_conflict_hash,
is_locked);
/* Even if the file failed to check out, still need to update index.
* But we have to stop after transfer errors.
*/
if (rc == FETCH_CHECKOUT_CANCELED) {
seaf_debug ("Transfer canceled.\n");
ret = FETCH_CHECKOUT_CANCELED;
if (add_ce)
cache_entry_free (ce);
if (!is_clone)
seaf_sync_manager_delete_active_path (seaf->sync_mgr,
repo_id,
de->name);
goto out;
} else if (rc == FETCH_CHECKOUT_TRANSFER_ERROR) {
seaf_warning ("Transfer failed.\n");
ret = FETCH_CHECKOUT_TRANSFER_ERROR;
if (add_ce)
cache_entry_free (ce);
if (!is_clone)
seaf_sync_manager_delete_active_path (seaf->sync_mgr,
repo_id,
de->name);
goto out;
}
if (!is_locked) {
cleanup_file_blocks (repo_id, repo_version, file_id);
if (!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);
}
} else {
#ifdef WIN32
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. */
#endif
}
}
if (!is_http)
++(task->n_downloaded);
else
++(http_task->done_files);
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;
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);
}
/* 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) {
seaf_debug ("Save index file.\n");
update_index (&istate, index_path);
checkout_size = 0;
}
} else if (de->status == DIFF_STATUS_DIR_ADDED) {
seaf_debug ("Checkout empty dir %s.\n", de->name);
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;
}
checkout_empty_dir (worktree,
de->name,
de->mtime,
ce,
conflict_hash,
no_conflict_hash);
seaf_sync_manager_update_active_path (seaf->sync_mgr,
repo_id,
de->name,
de->mode,
SYNC_STATUS_SYNCED);
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;
}
}
update_index (&istate, index_path);
out:
discard_index (&istate);
seaf_branch_unref (master);
seaf_commit_unref (master_head);
seaf_commit_unref (remote_head);
for (ptr = results; ptr; ptr = ptr->next)
diff_entry_free ((DiffEntry *)ptr->data);
g_free (crypt);
if (conflict_hash)
g_hash_table_destroy (conflict_hash);
if (no_conflict_hash)
g_hash_table_destroy (no_conflict_hash);
if (ignore_list)
seaf_repo_free_ignore_files (ignore_list);
#ifdef WIN32
locked_file_set_free (fset);
#endif
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) {
if (seaf_wt_monitor_unwatch_repo (seaf->wt_monitor, repo->id) < 0) {
g_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) {
if (seaf_wt_monitor_watch_repo (seaf->wt_monitor, repo->id, repo->worktree) < 0) {
g_warning ("failed to watch repo %s.\n", repo->id);
}
}
}
static int
compare_repo (const SeafRepo *srepo, const SeafRepo *trepo)
{
return g_strcmp0 (srepo->id, trepo->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 */
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;
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);
return mgr;
}
int
seaf_repo_manager_init (SeafRepoManager *mgr)
{
if (checkdir_with_mkdir (mgr->index_dir) < 0) {
g_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);
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) {
if (seaf_wt_monitor_watch_repo (seaf->wt_monitor, repo->id, repo->worktree) < 0) {
g_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.
*/
}
}
}
}
int
seaf_repo_manager_start (SeafRepoManager *mgr)
{
watch_repos (mgr);
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) {
g_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);
#ifdef WIN32
snprintf (sql, sizeof(sql), "DELETE FROM LockedFiles WHERE repo_id = '%s'",
repo_id);
sqlite_query_exec (mgr->priv->db, sql);
#endif
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);
out:
pthread_mutex_unlock (&mgr->priv->db_lock);
}
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);
if (pthread_rwlock_wrlock (&mgr->priv->lock) < 0) {
g_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);
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) {
g_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) {
g_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 gboolean
get_token (sqlite3_stmt *stmt, void *data)
{
char **token = data;
*token = g_strdup((char *)sqlite3_column_text (stmt, 0));
/* There should be only one result. */
return FALSE;
}
char *
seaf_repo_manager_get_repo_lantoken (SeafRepoManager *manager,
const char *repo_id)
{
char sql[256];
char *ret = NULL;
pthread_mutex_lock (&manager->priv->db_lock);
snprintf (sql, sizeof(sql),
"SELECT token FROM RepoLanToken WHERE repo_id='%s'",
repo_id);
if (sqlite_foreach_selected_row (manager->priv->db, sql,
get_token, &ret) < 0) {
g_warning ("DB error when get token for repo %s.\n", repo_id);
pthread_mutex_unlock (&manager->priv->db_lock);
return NULL;
}
pthread_mutex_unlock (&manager->priv->db_lock);
return ret;
}
int
seaf_repo_manager_set_repo_lantoken (SeafRepoManager *manager,
const char *repo_id,
const char *token)
{
char sql[256];
sqlite3 *db = manager->priv->db;
pthread_mutex_lock (&manager->priv->db_lock);
snprintf (sql, sizeof(sql), "REPLACE INTO RepoLanToken VALUES ('%s', '%s');",
repo_id, token);
if (sqlite_query_exec (db, sql) < 0) {
pthread_mutex_unlock (&manager->priv->db_lock);
return -1;
}
pthread_mutex_unlock (&manager->priv->db_lock);
return 0;
}
int
seaf_repo_manager_verify_repo_lantoken (SeafRepoManager *manager,
const char *repo_id,
const char *token)
{
int ret = 0;
if (!token)
return 0;
char *my_token = seaf_repo_manager_get_repo_lantoken (manager, repo_id);
if (!my_token) {
if (memcmp (DEFAULT_REPO_TOKEN, token, strlen(token)) == 0)
ret = 1;
} else {
if (memcmp (my_token, token, strlen(token)) == 0)
ret = 1;
g_free (my_token);
}
return ret;
}
char *
seaf_repo_manager_generate_tmp_token (SeafRepoManager *manager,
const char *repo_id,
const char *peer_id)
{
char sql[256];
sqlite3 *db = manager->priv->db;
int now = time(NULL);
char *token = gen_uuid();
pthread_mutex_lock (&manager->priv->db_lock);
snprintf (sql, sizeof(sql),
"REPLACE INTO RepoTmpToken VALUES ('%s', '%s', '%s', %d);",
repo_id, peer_id, token, now);
if (sqlite_query_exec (db, sql) < 0) {
pthread_mutex_unlock (&manager->priv->db_lock);
g_free (token);
return NULL;
}
pthread_mutex_unlock (&manager->priv->db_lock);
return token;
}
int
seaf_repo_manager_verify_tmp_token (SeafRepoManager *manager,
const char *repo_id,
const char *peer_id,
const char *token)
{
int ret;
char sql[512];
if (!repo_id || !peer_id || !token)
return 0;
pthread_mutex_lock (&manager->priv->db_lock);
snprintf (sql, 512, "SELECT timestamp FROM RepoTmpToken "
"WHERE repo_id='%s' AND peer_id='%s' AND token='%s'",
repo_id, peer_id, token);
ret = sqlite_check_for_existence (manager->priv->db, sql);
if (ret) {
snprintf (sql, 512, "DELETE FROM RepoTmpToken WHERE "
"repo_id='%s' AND peer_id='%s'",
repo_id, peer_id);
sqlite_query_exec (manager->priv->db, sql);
}
pthread_mutex_unlock (&manager->priv->db_lock);
return ret;
}
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) {
g_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) {
g_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)
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) {
g_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) {
g_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 SeafRepo *
load_repo (SeafRepoManager *manager, const char *repo_id)
{
char sql[256];
SeafRepo *repo = seaf_repo_new(repo_id, NULL, NULL);
if (!repo) {
g_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) {
g_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 {
g_warning ("[repo-mgr] Can not find commit %s\n",
branch->commit_id);
repo->is_corrupted = TRUE;
}
seaf_branch_unref (branch);
} else {
g_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->relay_id = load_repo_property (manager, repo->id, REPO_RELAY_ID);
if (repo->relay_id && strlen(repo->relay_id) != 40) {
g_free (repo->relay_id);
repo->relay_id = NULL;
}
value = load_repo_property (manager, repo->id, REPO_NET_BROWSABLE);
if (g_strcmp0(value, "true") == 0) {
repo->net_browsable = 1;
}
g_free (value);
repo->email = load_repo_property (manager, repo->id, REPO_PROP_EMAIL);
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_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);
#ifdef WIN32
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);
#endif
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);
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) {
g_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) {
g_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);
}
inline static gboolean is_peer_relay (const char *peer_id)
{
CcnetPeer *peer = ccnet_get_peer(seaf->ccnetrpc_client, peer_id);
if (!peer)
return FALSE;
gboolean is_relay = string_list_is_exists(peer->role_list, "MyRelay");
g_object_unref (peer);
return is_relay;
}
int
seaf_repo_manager_set_repo_relay_id (SeafRepoManager *mgr,
SeafRepo *repo,
const char *relay_id)
{
if (relay_id && strlen(relay_id) != 40)
return -1;
save_repo_property (mgr, repo->id, REPO_RELAY_ID, relay_id);
g_free (repo->relay_id);
if (relay_id)
repo->relay_id = g_strdup (relay_id);
else
repo->relay_id = NULL;
return 0;
}
static char *
canonical_server_url (const char *url_in)
{
char *url = g_strdup(url_in);
int len = strlen(url);
if (url[len - 1] == '/')
url[len - 1] = 0;
return url;
}
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;
seaf_wt_monitor_watch_repo (seaf->wt_monitor, repo->id,
repo->worktree);
repo->last_sync_time = 0;
} else {
repo->auto_sync = 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_NET_BROWSABLE) == 0) {
if (g_strcmp0(value, "true") == 0)
repo->net_browsable = 1;
else
repo->net_browsable = 0;
}
if (strcmp(key, REPO_RELAY_ID) == 0)
return seaf_repo_manager_set_repo_relay_id (manager, repo, value);
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;
}
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->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_set_merge (SeafRepoManager *manager,
const char *repo_id,
const char *remote_head)
{
char sql[256];
pthread_mutex_lock (&manager->priv->db_lock);
snprintf (sql, sizeof(sql), "REPLACE INTO MergeInfo VALUES ('%s', 1, '%s');",
repo_id, remote_head);
int ret = sqlite_query_exec (manager->priv->db, sql);
pthread_mutex_unlock (&manager->priv->db_lock);
return ret;
}
int
seaf_repo_manager_clear_merge (SeafRepoManager *manager,
const char *repo_id)
{
char sql[256];
pthread_mutex_lock (&manager->priv->db_lock);
snprintf (sql, sizeof(sql), "UPDATE MergeInfo SET in_merge=0 WHERE repo_id='%s';",
repo_id);
int ret = sqlite_query_exec (manager->priv->db, sql);
pthread_mutex_unlock (&manager->priv->db_lock);
return ret;
}
static gboolean
get_merge_info (sqlite3_stmt *stmt, void *vinfo)
{
SeafRepoMergeInfo *info = vinfo;
int in_merge;
in_merge = sqlite3_column_int (stmt, 1);
if (in_merge == 0)
info->in_merge = FALSE;
else
info->in_merge = TRUE;
/*
* Note that compatibility, we store remote_head in the "branch" column.
*/
const char *remote_head = (const char *) sqlite3_column_text (stmt, 2);
memcpy (info->remote_head, remote_head, 40);
return FALSE;
}
int
seaf_repo_manager_get_merge_info (SeafRepoManager *manager,
const char *repo_id,
SeafRepoMergeInfo *info)
{
char sql[256];
/* Default not in_merge, if no row is found in db. */
info->in_merge = FALSE;
pthread_mutex_lock (&manager->priv->db_lock);
snprintf (sql, sizeof(sql), "SELECT * FROM MergeInfo WHERE repo_id='%s';",
repo_id);
if (sqlite_foreach_selected_row (manager->priv->db, sql,
get_merge_info, info) < 0) {
pthread_mutex_unlock (&manager->priv->db_lock);
return -1;
}
pthread_mutex_unlock (&manager->priv->db_lock);
return 0;
}
typedef struct {
char common_ancestor[41];
char head_id[41];
} CAInfo;
static gboolean
get_common_ancestor (sqlite3_stmt *stmt, void *vinfo)
{
CAInfo *info = vinfo;
const char *ancestor = (const char *) sqlite3_column_text (stmt, 0);
const char *head_id = (const char *) sqlite3_column_text (stmt, 1);
memcpy (info->common_ancestor, ancestor, 40);
memcpy (info->head_id, head_id, 40);
return FALSE;
}
int
seaf_repo_manager_get_common_ancestor (SeafRepoManager *manager,
const char *repo_id,
char *common_ancestor,
char *head_id)
{
char sql[256];
CAInfo info;
memset (&info, 0, sizeof(info));
pthread_mutex_lock (&manager->priv->db_lock);
snprintf (sql, sizeof(sql),
"SELECT ca_id, head_id FROM CommonAncestor WHERE repo_id='%s';",
repo_id);
if (sqlite_foreach_selected_row (manager->priv->db, sql,
get_common_ancestor, &info) < 0) {
pthread_mutex_unlock (&manager->priv->db_lock);
return -1;
}
pthread_mutex_unlock (&manager->priv->db_lock);
memcpy (common_ancestor, info.common_ancestor, 41);
memcpy (head_id, info.head_id, 41);
return 0;
}
int
seaf_repo_manager_set_common_ancestor (SeafRepoManager *manager,
const char *repo_id,
const char *common_ancestor,
const char *head_id)
{
char sql[256];
pthread_mutex_lock (&manager->priv->db_lock);
snprintf (sql, sizeof(sql),
"REPLACE INTO CommonAncestor VALUES ('%s', '%s', '%s');",
repo_id, common_ancestor, head_id);
int ret = sqlite_query_exec (manager->priv->db, sql);
pthread_mutex_unlock (&manager->priv->db_lock);
return ret;
}
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) {
g_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;
}
typedef struct {
SeafRepo *repo;
CheckoutTask *task;
CheckoutDoneCallback done_cb;
void *cb_data;
} CheckoutData;
static void
checkout_job_done (void *vresult)
{
if (!vresult)
return;
CheckoutData *cdata = vresult;
SeafRepo *repo = cdata->repo;
SeafBranch *local = NULL;
if (!cdata->task->success)
goto out;
seaf_repo_manager_set_repo_worktree (repo->manager,
repo,
cdata->task->worktree);
local = seaf_branch_manager_get_branch (seaf->branch_mgr, repo->id, "local");
if (!local) {
seaf_warning ("Cannot get branch local for repo %s(%.10s).\n",
repo->name, repo->id);
return;
}
/* Set repo head to mark checkout done. */
seaf_repo_set_head (repo, local);
seaf_branch_unref (local);
if (repo->auto_sync) {
if (seaf_wt_monitor_watch_repo (seaf->wt_monitor, repo->id, repo->worktree) < 0) {
seaf_warning ("failed to watch repo %s(%.10s).\n", repo->name, repo->id);
return;
}
}
out:
if (cdata->done_cb)
cdata->done_cb (cdata->task, cdata->repo, cdata->cb_data);
/* g_hash_table_remove (mgr->priv->checkout_tasks_hash, cdata->repo->id); */
}
static void *
checkout_repo_job (void *data)
{
SeafRepoManager *mgr = seaf->repo_mgr;
CheckoutData *cdata = data;
SeafRepo *repo = cdata->repo;
CheckoutTask *task;
task = g_hash_table_lookup (mgr->priv->checkout_tasks_hash, repo->id);
if (!task) {
seaf_warning ("Failed to find checkout task for repo %.10s\n", repo->id);
return NULL;
}
repo->worktree = g_strdup (task->worktree);
char *error_msg = NULL;
if (seaf_repo_checkout (repo, task->worktree, &error_msg) < 0) {
seaf_warning ("Failed to checkout repo %.10s to %s : %s\n",
repo->id, task->worktree, error_msg);
g_free (error_msg);
task->success = FALSE;
goto ret;
}
task->success = TRUE;
ret:
return data;
}
int
seaf_repo_manager_add_checkout_task (SeafRepoManager *mgr,
SeafRepo *repo,
const char *worktree,
CheckoutDoneCallback done_cb,
void *cb_data)
{
if (!repo || !worktree) {
seaf_warning ("Invaid args\n");
return -1;
}
CheckoutTask *task = g_new0 (CheckoutTask, 1);
memcpy (task->repo_id, repo->id, 41);
g_return_val_if_fail (strlen(worktree) < SEAF_PATH_MAX, -1);
strcpy (task->worktree, worktree);
g_hash_table_insert (mgr->priv->checkout_tasks_hash,
g_strdup(repo->id), task);
CheckoutData *cdata = g_new0 (CheckoutData, 1);
cdata->repo = repo;
cdata->task = task;
cdata->done_cb = done_cb;
cdata->cb_data = cb_data;
ccnet_job_manager_schedule_job(seaf->job_mgr,
(JobThreadFunc)checkout_repo_job,
(JobDoneCallback)checkout_job_done,
cdata);
return 0;
}
CheckoutTask *
seaf_repo_manager_get_checkout_task (SeafRepoManager *mgr,
const char *repo_id)
{
if (!repo_id || strlen(repo_id) != 36) {
seaf_warning ("Invalid args\n");
return NULL;
}
return g_hash_table_lookup(mgr->priv->checkout_tasks_hash, repo_id);
}
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;
}
int
seaf_repo_manager_update_repo_relay_info (SeafRepoManager *mgr,
SeafRepo *repo,
const char *new_addr,
const char *new_port)
{
GList *ptr, *repos = seaf_repo_manager_get_repo_list (seaf->repo_mgr, 0, -1);
SeafRepo *r;
for (ptr = repos; ptr; ptr = ptr->next) {
r = ptr->data;
if (g_strcmp0(r->relay_id, repo->relay_id) != 0)
continue;
char *relay_addr = NULL;
char *relay_port = NULL;
seaf_repo_manager_get_repo_relay_info (seaf->repo_mgr, r->id,
&relay_addr, &relay_port);
if (g_strcmp0(relay_addr, new_addr) != 0 ||
g_strcmp0(relay_port, new_port) != 0) {
seaf_repo_manager_set_repo_relay_info (seaf->repo_mgr, r->id,
new_addr, new_port);
}
g_free (relay_addr);
g_free (relay_port);
}
g_list_free (repos);
return 0;
}
int
seaf_repo_manager_update_repos_server_host (SeafRepoManager *mgr,
const char *old_host,
const char *new_host,
const char *new_server_url)
{
GList *ptr, *repos = seaf_repo_manager_get_repo_list (seaf->repo_mgr, 0, -1);
SeafRepo *r;
for (ptr = repos; ptr; ptr = ptr->next) {
r = ptr->data;
char *relay_addr = NULL;
char *relay_port = NULL;
seaf_repo_manager_get_repo_relay_info (seaf->repo_mgr, r->id,
&relay_addr, &relay_port);
if (g_strcmp0(relay_addr, old_host) == 0) {
seaf_repo_manager_set_repo_relay_info (seaf->repo_mgr, r->id,
new_host, relay_port);
seaf_repo_manager_set_repo_property (
seaf->repo_mgr, r->id, REPO_PROP_SERVER_URL, new_server_url);
}
g_free (relay_addr);
g_free (relay_port);
}
g_list_free (repos);
return 0;
}
/*
* 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[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, 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);
/* first check the path is a reg file or a dir */
if (seaf_stat(str, &st) < 0) {
g_free(str);
return FALSE;
}
if (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);
}