seafile/daemon/change-set.c
2022-03-15 16:28:10 +08:00

762 lines
22 KiB
C

/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
#include "common.h"
#include "seafile-session.h"
#include "utils.h"
#include "log.h"
#include "index/index.h"
#include "diff-simple.h"
#include "change-set.h"
struct _ChangeSetDir {
int version;
char dir_id[41];
/* A hash table of dirents for fast lookup and insertion. */
GHashTable *dents;
#if defined WIN32 || defined __APPLE__
/* Case-insensitive hash table. */
GHashTable *dents_i;
#endif
};
typedef struct _ChangeSetDir ChangeSetDir;
struct _ChangeSetDirent {
guint32 mode;
char id[41];
char *name;
gint64 mtime;
char *modifier;
gint64 size;
/* Only used for directory. Most of time this is NULL
* unless we change the subdir too.
*/
ChangeSetDir *subdir;
};
typedef struct _ChangeSetDirent ChangeSetDirent;
/* Change set dirent. */
static ChangeSetDirent *
changeset_dirent_new (const char *id, guint32 mode, const char *name,
gint64 mtime, const char *modifier, gint64 size)
{
ChangeSetDirent *dent = g_new0 (ChangeSetDirent, 1);
dent->mode = mode;
memcpy (dent->id, id, 40);
dent->name = g_strdup(name);
dent->mtime = mtime;
dent->modifier = g_strdup(modifier);
dent->size = size;
return dent;
}
static ChangeSetDirent *
seaf_dirent_to_changeset_dirent (SeafDirent *seaf_dent)
{
return changeset_dirent_new (seaf_dent->id, seaf_dent->mode, seaf_dent->name,
seaf_dent->mtime, seaf_dent->modifier, seaf_dent->size);
}
static SeafDirent *
changeset_dirent_to_seaf_dirent (int version, ChangeSetDirent *dent)
{
return seaf_dirent_new (version, dent->id, dent->mode, dent->name,
dent->mtime, dent->modifier, dent->size);
}
static void
changeset_dir_free (ChangeSetDir *dir);
static void
changeset_dirent_free (ChangeSetDirent *dent)
{
if (!dent)
return;
g_free (dent->name);
g_free (dent->modifier);
/* Recursively free subdir. */
if (dent->subdir)
changeset_dir_free (dent->subdir);
g_free (dent);
}
/* Change set dir. */
static void
add_dent_to_dir (ChangeSetDir *dir, ChangeSetDirent *dent)
{
g_hash_table_insert (dir->dents,
g_strdup(dent->name),
dent);
#if defined WIN32 || defined __APPLE__
g_hash_table_insert (dir->dents_i,
g_utf8_strdown(dent->name, -1),
dent);
#endif
}
static void
remove_dent_from_dir (ChangeSetDir *dir, const char *dname)
{
char *key;
if (g_hash_table_lookup_extended (dir->dents, dname,
(gpointer*)&key, NULL)) {
g_hash_table_steal (dir->dents, dname);
g_free (key);
}
#if defined WIN32 || defined __APPLE__
char *dname_i = g_utf8_strdown (dname, -1);
g_hash_table_remove (dir->dents_i, dname_i);
g_free (dname_i);
#endif
}
static ChangeSetDir *
changeset_dir_new (int version, const char *id, GList *dirents)
{
ChangeSetDir *dir = g_new0 (ChangeSetDir, 1);
GList *ptr;
SeafDirent *dent;
ChangeSetDirent *changeset_dent;
dir->version = version;
if (id)
memcpy (dir->dir_id, id, 40);
dir->dents = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, (GDestroyNotify)changeset_dirent_free);
#if defined WIN32 || defined __APPLE__
dir->dents_i = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, NULL);
#endif
for (ptr = dirents; ptr; ptr = ptr->next) {
dent = ptr->data;
changeset_dent = seaf_dirent_to_changeset_dirent(dent);
add_dent_to_dir (dir, changeset_dent);
}
return dir;
}
static void
changeset_dir_free (ChangeSetDir *dir)
{
if (!dir)
return;
g_hash_table_destroy (dir->dents);
#if defined WIN32 || defined __APPLE__
g_hash_table_destroy (dir->dents_i);
#endif
g_free (dir);
}
static ChangeSetDir *
seaf_dir_to_changeset_dir (SeafDir *seaf_dir)
{
return changeset_dir_new (seaf_dir->version, seaf_dir->dir_id, seaf_dir->entries);
}
static gint
compare_dents (gconstpointer a, gconstpointer b)
{
const SeafDirent *denta = a, *dentb = b;
return strcmp(dentb->name, denta->name);
}
static SeafDir *
changeset_dir_to_seaf_dir (ChangeSetDir *dir)
{
GList *dents = NULL, *seaf_dents = NULL;
GList *ptr;
ChangeSetDirent *dent;
SeafDirent *seaf_dent;
SeafDir *seaf_dir;
dents = g_hash_table_get_values (dir->dents);
for (ptr = dents; ptr; ptr = ptr->next) {
dent = ptr->data;
seaf_dent = changeset_dirent_to_seaf_dirent (dir->version, dent);
seaf_dents = g_list_prepend (seaf_dents, seaf_dent);
}
/* Sort it in descending order. */
seaf_dents = g_list_sort (seaf_dents, compare_dents);
/* seaf_dir_new() computes the dir id. */
seaf_dir = seaf_dir_new (NULL, seaf_dents, dir->version);
g_list_free (dents);
return seaf_dir;
}
/* Change set. */
#define CASE_CONFLICT_PATTERN " \\(case conflict \\d+\\)"
ChangeSet *
changeset_new (const char *repo_id)
{
SeafRepo *repo;
SeafCommit *commit = NULL;
SeafDir *seaf_dir = NULL;
ChangeSetDir *changeset_dir = NULL;
ChangeSet *changeset = NULL;
repo = seaf_repo_manager_get_repo (seaf->repo_mgr, repo_id);
if (!repo) {
seaf_warning ("Failed to find repo %s.\n", repo_id);
return NULL;
}
commit = seaf_commit_manager_get_commit (seaf->commit_mgr,
repo_id,
repo->version,
repo->head->commit_id);
if (!commit) {
seaf_warning ("Failed to find head commit %s for repo %s.\n",
repo->head->commit_id, repo_id);
return NULL;
}
seaf_dir = seaf_fs_manager_get_seafdir_sorted (seaf->fs_mgr,
repo_id,
repo->version,
commit->root_id);
if (!seaf_dir) {
seaf_warning ("Failed to find root dir %s in repo %s\n",
repo->root_id, repo_id);
goto out;
}
changeset_dir = seaf_dir_to_changeset_dir (seaf_dir);
if (!changeset_dir)
goto out;
GError *error = NULL;
GRegex *case_conflict_pattern = g_regex_new(CASE_CONFLICT_PATTERN,
0, 0, &error);
if (error) {
seaf_warning ("Failed to create regex '%s': %s\n",
CASE_CONFLICT_PATTERN, error->message);
goto out;
}
changeset = g_new0 (ChangeSet, 1);
memcpy (changeset->repo_id, repo_id, 36);
changeset->tree_root = changeset_dir;
changeset->case_conflict_pattern = case_conflict_pattern;
out:
seaf_commit_unref (commit);
seaf_dir_free (seaf_dir);
return changeset;
}
void
changeset_free (ChangeSet *changeset)
{
if (!changeset)
return;
changeset_dir_free (changeset->tree_root);
g_regex_unref (changeset->case_conflict_pattern);
g_free (changeset);
}
static gboolean
update_file (ChangeSetDirent *dent,
unsigned char *sha1,
SeafStat *st,
const char *modifier)
{
if (!sha1 || !st || !S_ISREG(st->st_mode))
return FALSE;
dent->mode = create_ce_mode(st->st_mode);
dent->mtime = (gint64)st->st_mtime;
dent->size = (gint64)st->st_size;
rawdata_to_hex (sha1, dent->id, 20);
g_free (dent->modifier);
dent->modifier = g_strdup(modifier);
return TRUE;
}
static void
create_new_dent (ChangeSetDir *dir,
const char *dname,
unsigned char *sha1,
SeafStat *st,
const char *modifier,
ChangeSetDirent *in_new_dent)
{
if (in_new_dent) {
g_free (in_new_dent->name);
in_new_dent->name = g_strdup(dname);
add_dent_to_dir (dir, in_new_dent);
return;
}
char id[41];
rawdata_to_hex (sha1, id, 20);
ChangeSetDirent *new_dent;
new_dent = changeset_dirent_new (id, create_ce_mode(st->st_mode), dname,
st->st_mtime, modifier, st->st_size);
add_dent_to_dir (dir, new_dent);
}
static ChangeSetDir *
create_intermediate_dir (ChangeSetDir *parent, const char *dname, SeafStat *st)
{
ChangeSetDirent *dent;
gint64 mtime = 0;
if (st) {
mtime = st->st_mtime;
}
dent = changeset_dirent_new (EMPTY_SHA1, S_IFDIR, dname, mtime, NULL, 0);
dent->subdir = changeset_dir_new (parent->version, EMPTY_SHA1, NULL);
add_dent_to_dir (parent, dent);
return dent->subdir;
}
#if defined WIN32 || defined __APPLE__
static void
handle_case_conflict (ChangeSet *changeset,
ChangeSetDir *dir,
const char *dname)
{
char *conflict_dname;
ChangeSetDirent *dent;
GError *error = NULL;
if (g_regex_match (changeset->case_conflict_pattern,
dname, 0, NULL)) {
conflict_dname = g_regex_replace_literal (changeset->case_conflict_pattern,
dname, -1, 0, "", 0, &error);
if (!conflict_dname) {
seaf_warning ("Failed to replace regex for %s: %s\n",
dname, error->message);
return;
}
dent = g_hash_table_lookup (dir->dents, conflict_dname);
if (dent) {
remove_dent_from_dir (dir, conflict_dname);
changeset_dirent_free (dent);
}
g_free (conflict_dname);
}
}
#endif
static void
add_to_tree (ChangeSet *changeset,
unsigned char *sha1,
SeafStat *st,
const char *modifier,
const char *path,
ChangeSetDirent *new_dent)
{
char *repo_id = changeset->repo_id;
ChangeSetDir *root = changeset->tree_root;
char **parts, *dname;
int n, i;
ChangeSetDir *dir;
ChangeSetDirent *dent;
ChangeSetDirent *parent_dent = NULL;
SeafDir *seaf_dir;
gboolean changed;
parts = g_strsplit (path, "/", 0);
n = g_strv_length(parts);
dir = root;
for (i = 0; i < n; i++) {
#if defined WIN32 || defined __APPLE__
try_again:
#endif
dname = parts[i];
dent = g_hash_table_lookup (dir->dents, dname);
if (dent) {
if (S_ISDIR(dent->mode)) {
if (i == (n-1))
/* Don't need to update empty dir */
break;
if (!dent->subdir) {
seaf_dir = seaf_fs_manager_get_seafdir(seaf->fs_mgr,
repo_id,
root->version,
dent->id);
if (!seaf_dir) {
seaf_warning ("Failed to load seafdir %s:%s\n",
repo_id, dent->id);
break;
}
dent->subdir = seaf_dir_to_changeset_dir (seaf_dir);
seaf_dir_free (seaf_dir);
}
dir = dent->subdir;
parent_dent = dent;
} else if (S_ISREG(dent->mode)) {
if (i == (n-1)) {
/* File exists, update it. */
changed = update_file (dent, sha1, st, modifier);
// update parent dir mtime when modify files locally.
if (parent_dent && changed) {
parent_dent->mtime = st->st_mtime;
}
break;
}
}
} else {
#if defined WIN32 || defined __APPLE__
/* Only effective for add operation, not applicable to rename. */
if (!new_dent) {
char *search_key = g_utf8_strdown (dname, -1);
dent = g_hash_table_lookup (dir->dents_i, search_key);
g_free (search_key);
if (dent) {
remove_dent_from_dir (dir, dent->name);
g_free (dent->name);
dent->name = g_strdup(dname);
add_dent_to_dir (dir, dent);
goto try_again;
}
handle_case_conflict (changeset, dir, dname);
}
#endif
if (i == (n-1)) {
if (parent_dent && new_dent) {
// update parent dir mtime when rename files locally.
parent_dent->mtime = time(NULL);
} else if (parent_dent && st) {
// update parent dir mtime when add files locally.
parent_dent->mtime = st->st_mtime;
}
create_new_dent (dir, dname, sha1, st, modifier, new_dent);
} else {
dir = create_intermediate_dir (dir, dname, st);
}
}
}
g_strfreev (parts);
}
static ChangeSetDirent *
delete_from_tree (ChangeSet *changeset,
const char *path,
gboolean *parent_empty)
{
char *repo_id = changeset->repo_id;
ChangeSetDir *root = changeset->tree_root;
char **parts, *dname;
int n, i;
ChangeSetDir *dir;
ChangeSetDirent *dent, *ret = NULL;
ChangeSetDirent *parent_dent = NULL;
SeafDir *seaf_dir;
*parent_empty = FALSE;
parts = g_strsplit (path, "/", 0);
n = g_strv_length(parts);
dir = root;
for (i = 0; i < n; i++) {
dname = parts[i];
dent = g_hash_table_lookup (dir->dents, dname);
if (!dent)
break;
if (S_ISDIR(dent->mode)) {
if (i == (n-1)) {
/* Remove from hash table without freeing dent. */
remove_dent_from_dir (dir, dname);
if (g_hash_table_size (dir->dents) == 0)
*parent_empty = TRUE;
ret = dent;
// update parent dir mtime when delete dirs locally.
if (parent_dent) {
parent_dent->mtime = time (NULL);
}
break;
}
if (!dent->subdir) {
seaf_dir = seaf_fs_manager_get_seafdir(seaf->fs_mgr,
repo_id,
root->version,
dent->id);
if (!seaf_dir) {
seaf_warning ("Failed to load seafdir %s:%s\n",
repo_id, dent->id);
break;
}
dent->subdir = seaf_dir_to_changeset_dir (seaf_dir);
seaf_dir_free (seaf_dir);
}
dir = dent->subdir;
parent_dent = dent;
} else if (S_ISREG(dent->mode)) {
if (i == (n-1)) {
/* Remove from hash table without freeing dent. */
remove_dent_from_dir (dir, dname);
if (g_hash_table_size (dir->dents) == 0)
*parent_empty = TRUE;
ret = dent;
// update parent dir mtime when delete files locally.
if (parent_dent) {
parent_dent->mtime = time (NULL);
}
break;
}
}
}
g_strfreev (parts);
return ret;
}
static void
apply_to_tree (ChangeSet *changeset,
char status,
unsigned char *sha1,
SeafStat *st,
const char *modifier,
const char *path,
const char *new_path)
{
ChangeSetDirent *dent, *dent_dst;
gboolean dummy;
switch (status) {
case DIFF_STATUS_ADDED:
case DIFF_STATUS_MODIFIED:
case DIFF_STATUS_DIR_ADDED:
add_to_tree (changeset, sha1, st, modifier, path, NULL);
break;
case DIFF_STATUS_RENAMED:
dent = delete_from_tree (changeset, path, &dummy);
if (!dent)
break;
dent_dst = delete_from_tree (changeset, new_path, &dummy);
changeset_dirent_free (dent_dst);
add_to_tree (changeset, NULL, NULL, NULL, new_path, dent);
break;
}
}
void
add_to_changeset (ChangeSet *changeset,
char status,
unsigned char *sha1,
SeafStat *st,
const char *modifier,
const char *path,
const char *new_path)
{
apply_to_tree (changeset,
status, sha1, st, modifier, path, new_path);
}
static void
remove_from_changeset_recursive (ChangeSet *changeset,
const char *path,
gboolean remove_parent,
const char *top_dir)
{
ChangeSetDirent *dent;
gboolean parent_empty = FALSE;
dent = delete_from_tree (changeset, path, &parent_empty);
changeset_dirent_free (dent);
if (remove_parent && parent_empty) {
char *parent = g_strdup(path);
char *slash = strrchr (parent, '/');
if (slash) {
*slash = '\0';
if (strlen(parent) >= strlen(top_dir)) {
/* Recursively remove parent dirs. */
remove_from_changeset_recursive (changeset,
parent,
remove_parent,
top_dir);
}
}
g_free (parent);
}
}
void
remove_from_changeset (ChangeSet *changeset,
char status,
const char *path,
gboolean remove_parent,
const char *top_dir)
{
remove_from_changeset_recursive (changeset, path, remove_parent, top_dir);
}
static char *
commit_tree_recursive (const char *repo_id, ChangeSetDir *dir)
{
ChangeSetDirent *dent;
GHashTableIter iter;
gpointer key, value;
char *new_id;
SeafDir *seaf_dir;
char *ret = NULL;
g_hash_table_iter_init (&iter, dir->dents);
while (g_hash_table_iter_next (&iter, &key, &value)) {
dent = value;
if (dent->subdir) {
new_id = commit_tree_recursive (repo_id, dent->subdir);
if (!new_id)
return NULL;
memcpy (dent->id, new_id, 40);
g_free (new_id);
}
}
seaf_dir = changeset_dir_to_seaf_dir (dir);
memcpy (dir->dir_id, seaf_dir->dir_id, 40);
if (!seaf_fs_manager_object_exists (seaf->fs_mgr,
repo_id, dir->version,
seaf_dir->dir_id)) {
if (seaf_dir_save (seaf->fs_mgr, repo_id, dir->version, seaf_dir) < 0) {
seaf_warning ("Failed to save dir object %s to repo %s.\n",
seaf_dir->dir_id, repo_id);
goto out;
}
}
ret = g_strdup(seaf_dir->dir_id);
out:
seaf_dir_free (seaf_dir);
return ret;
}
/*
* This function does two things:
* - calculate dir id from bottom up;
* - create and save seaf dir objects.
* It returns root dir id of the new commit.
*/
char *
commit_tree_from_changeset (ChangeSet *changeset)
{
char *root_id = commit_tree_recursive (changeset->repo_id,
changeset->tree_root);
return root_id;
}
gboolean
changeset_check_path (ChangeSet *changeset,
const char *path,
unsigned char *sha1,
guint32 mode,
gint64 mtime)
{
ChangeSetDir *root = changeset->tree_root;
char **parts, *dname;
int n, i;
ChangeSetDir *dir;
ChangeSetDirent *dent;
gboolean ret = FALSE;
char id[41];
rawdata_to_hex (sha1, id, 20);
parts = g_strsplit (path, "/", 0);
n = g_strv_length(parts);
dir = root;
for (i = 0; i < n; i++) {
dname = parts[i];
dent = g_hash_table_lookup (dir->dents, dname);
if (!dent) {
seaf_message ("Changeset mismatch: path component %s of %s not found\n",
dname, path);
break;
}
if (S_ISDIR(dent->mode)) {
if (i == (n-1)) {
if (dent->mode != mode) {
seaf_message ("Changeset mismatch: %s is not a dir\n", path);
break;
} else if (strcmp (dent->id, EMPTY_SHA1) != 0) {
seaf_message ("Changeset mismatch: %s is not a empty dir\n", path);
break;
}
ret = TRUE;
break;
}
if (!dent->subdir) {
seaf_message ("Changeset mismatch: path component %s of %s is not in changeset\n",
dname, path);
break;
}
dir = dent->subdir;
} else if (S_ISREG(dent->mode)) {
if (i == (n-1)) {
if (dent->mode != mode) {
seaf_message ("Changeset mismatch: %s mode mismatch, "
"index: %u, changeset: %u\n",
path, mode, dent->mode);
break;
} else if (dent->mtime != mtime) {
seaf_message ("Changeset mismatch: %s mtime mismatch, "
"index: %"G_GINT64_FORMAT
", changeset: %"G_GINT64_FORMAT"\n",
path, mtime, dent->mtime);
break;
} else if (strcmp (dent->id, id) != 0) {
seaf_message ("Changeset mismatch: %s id mismatch, "
"index: %s, changeset: %s\n",
path, id, dent->id);
break;
}
ret = TRUE;
break;
}
/* We find a file in the middle of the path, this is invalid. */
seaf_message ("Changeset mismatch: path component %s of %s is a file\n",
dname, path);
break;
}
}
g_strfreev (parts);
return ret;
}