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