注:本日志创作时,日志系统已经完成开发和调试,本日志为补档。
文章目录
锁系统具体实现方式思考
有了前后交互的框架,接下来我们就需要考虑一下如何实现数据存储了。
其实从前面的 api.py 中可以看出,主要就是对文件锁信息的增删改查,其中锁和文件路径(repo_id, path)应该是一对一的关系。那么,接下来需要考虑如何实现锁。我有两种大概的想法:
- 第一种,粗暴地使用数据库,用数据库承载增删改查;
- 第二种,把数据存储在一个内存中,因为我认为文件锁应该是一个不需要持久化的数据,重启服务以后,丢了就丢了。
但是经过更深层次的考虑发现,这个文件锁信息是需要持久化的,因为一般的锁持续时间都是 12 小时(根据 Pro 中的设置),而且一般服务器重新启动的时间应该很短,不能让用户明显感觉到信息的丢失。
因此,必须将文件锁信息存储到数据库中。
文件锁数据库结构
那么确定了要把文件的锁信息存储到数据库中,接下来就需要思考,它在数据库中的结构应该是怎样的。
首先根据几个增删改查的函数,我认为至少应该包含的信息:
- repo_id,仓库的 ID;
- path,被锁定文件的路径;
- user_name,文件锁的拥有者;
- expire,文件锁的过期时间;
- lock_time,文件锁的创建时间,因为当时输出了 expire=0,而文件锁不可能永远不过期,因此必须通过锁的创建时间来判断它是否有效。
那么,就形成了数据库结构:
sql = "CREATE TABLE IF NOT EXISTS FileLocks (\n"
" id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,\n"
" repo_id CHAR(40) NOT NULL,\n"
" path TEXT NOT NULL,\n"
" user_name VARCHAR(255) NOT NULL,\n"
" lock_time BIGINT,\n"
" expire BIGINT,\n"
" KEY(repo_id)\n"
") ENGINE=INNODB;";
if (seaf_db_query (db, sql) < 0)
return -1;
主要功能的逻辑结构
数据库操作和数据操作
当时写这一部分花了不少功夫。主要难点还是两方面,一个是 Seafile 虽然自己定义了一些 SQL 相关的操作函数,但是真的是一点文档和注释都不给啊啊啊啊啊,很多 SQL 函数的作用、返回值全靠猜,我裂开了。
通过分析了一部分它已经写好的代码以后,我大概明白了它的使用逻辑:
- 通过 sprintf 产生 SQL 语句
- 使用
seaf_db_foreach_selected_row
执行 SQL 语句,在执行的时候需要传入一个 Row 处理函数,函数的模板为gboolean (CcnetDBRow *row, void *data)
,其中,gboolean
类型的返回值应该是决定要不要继续处理下面的行,而void *data
则用于向外传递信息,通常会传入一个GList **
类型,把处理出来的 Row 数据压到里面去。
然后,有关 Glib 的各种使用,网上资料也太少了。。。搜索各种东西,基本上出来的很少有我需要的,而官方文档也很难找到,连菜鸟教程也没有 Glib 的相关使用手册。。。
不过根据我对其他代码的分析,我还是大概了解了 Glib 对象的创建方式,例如下面就是对于 file_lock 对象的创建:
SeafileFileLock *file_lock;
file_lock = g_object_new (SEAFILE_TYPE_FILE_LOCK,
"repo_id", repo_id,
"path", path,
"user", user,
"lock_time", lock_time,
"expire", expire,
NULL);
为什么是这样呢?这事要从前面说起。前面在使用 api.py 里面信息的时候,有过这样一段注释:
def get_locked_files(self, repo_id):
"""
Return a list of FileLock objects (lib/repo.vala)
"""
这里看,提到了 lib/repo.vala
这个文件。首先,根据我们的 Makefile,有一部分 C 源代码文件是通过 Vala 语言生成的。没错,又是一个缺乏文档的陌生语言。。。网上的中文版资源真的太少了吧。。。
总之,只要在这个 Vala 文件中写一些对于类属性的简单定义,我们就可以通过 valac 生成含有 GObject 定义的 C 语言文件。于是,依葫芦画瓢,我在 lib/repo.vala
文件中添加了对于文件锁的定义:
public class FileLock: Object {
public string repo_id { get; set; }
public string path { get; set; }
public string user { get; set; }
public int64 lock_time { get; set; }
public int64 expire { get; set; }
}
然后又对比参考了其他地方使用 g_object_new
生成新的 GObject 的代码,最终确定了这段 Vala 代码的作用:
- 生成一个 C 的 struct:
SeafileFileLock
,依据是 vala 文件顶端的namespace Seafile
,valac 将 namespace 与类名字组合在一起,生成了一个 C 语言的 Struct(格式是:<namespace><class name>
,驼峰命名法); - 生成一个用于
g_object_new
的类型名称:SEAFILE_TYPE_FILE_LOCK
,只要将这个填写在g_object_new
的第一个参数位置上就行了,格式是:<namespace>_TYPE_<class name>
,纯大写加下划线; - 生成一系列用于修改和获取参数的函数,如:
seafile_file_lock_get_user(SeafileFileLock*)
,就是取出给定SeafileFileLock
对象的user
域,格式是:<namespace>_<class name>_get/set_<entry>
,纯小写加下划线。
大概弄清楚了 Glib 和 Vala 语言的交互方式以后,后面就舒服一些了。
check_file_lock
先写最关键的,这个也是其他函数需要用到的。
这个要实现两个功能:
- 去除数据库中的无用信息(已经过期的锁);
- 获取由 (repo_id, path) 确定的文件锁的拥有者。
其中,去除无用信息主要是判断时间戳和当前时间。
在这里需要注意的一点,在数据库中存储的时间戳并没有使用 Timestamp。这是因为在 Seafile 中,时间戳是用 64 位整数变量保存的微秒时间戳。没错,是微秒!!!
经过简单的搜索发现,在 utils.c
中,有专门获取当前微秒时间戳的函数:get_current_time()
。那么很明显,我们不能直接在数据库中用 NOW()
函数做时间的比对。
那么,剩下的就是时效问题。为此,我在网上找到了一篇讲述 Seafile Pro 配置的文档,里面提到了在 Pro 版本中,Seafile 通过 seafile.conf
中的 file_lock.default_expire_hours
设置来定义默认的过期时间。这样,当 lock_file()
传入的 expire = 0 时,就可以使用这个值来计算失效时间。
那么,就形成了如下思路:
- 先从数据库中取得符合由 (repo_id, path) 的文件锁记录,拿到拥有者、过期时间和锁创建时间;
- 根据锁的过期时间判断:
- 如果锁的过期时间为 0,则取得配置中
file_lock.default_expire_hours
定义的过期时间,并计算出过期时间
- 如果锁的过期时间为 0,则取得配置中
- 取得当前时间,与过期时间进行比较:
- 如果过期时间小于当前时间,则已过期,删除该条记录,并返回 0(无人加锁)。
- 判断锁拥有者和判断者,若二者相同,返回2,否则返回1。
lock_file, unlock_file
有了前面的函数以后,这里相对简单一些了,可以先用 check_file_lock
判断当前锁状态,并去除多余数据,然后再根据锁状态,进行数据的增加或者删除。
refresh_file_lock
跟 lock_file, unlock_file 这两个的处理方式很像。先用 check_file_lock
判断当前锁状态,并去除多余数据,然后如果是锁住的,那就把锁的创建时间置为现在。
get_lock_info
这个函数主要是获取一个 FileLock 对象。
这里,难点是如何返回一个锁对象。不过,通过对已经写好的功能模块进行调查,我发现只要用 SeafileFileLock
这个 struct 存储数据,然后用 g_object_new
创造新对象即可。
get_locked_files
这个是获取整个 Repo 中被锁定的文件,返回的是一个锁信息的 GList。
这个跟 get_lock_info 差不多,不过改成了要获取很多数据。
但是注意,别忘了对每条数据判断是否过期。如果过期的话,要把对应记录删除,这一点在最开始的时候忘掉了。
测试过程
第一次测试:段错误
在第一次测试的时候,出现了段错误的情况。错误原因是,在 get_lock_info 函数中,我尝试使用 GList 放三个不同类型的对象,结果导致空指针,进而访问了错误的地址。
通过在处理数据时直接创建一个 GObject,然后将其压入 GList,问题解决。
第二次测试:check_file_lock 结果异常
当时查看数据库,有对应记录,但是日志提示没有锁。后来仔细看日志才发现,前端服务传来的 path
中,有的有前导 /
,有的没有。而数据库是绝对字符匹配,因此一字之差导致了错误。
经过查找发现,utils.c
中定义了一个函数,处理这种情况:format_path()
,它能将 path
的格式标准化。
第三次测试:刷新后锁图标消失,锁状态错误
这个是在文件列表中出的问题,虽然点进文件单独查看,是可以看到锁正常,但是在列表中就看不到。
这种情况下,我打算通过解包的方式研究文件列表的数据获取。经过网络解包,我发现 Seafile Pro 中获取文件列表时,在列表中包含了每个文件的锁信息,而我的版本里则没有。
顺着 urls.py
进行查找,我发现了问题所在:又是 is_pro()
,把三个与锁相关的数据限制住了。
但是,解除了 is_pro()
限制,依然没有出现锁标记。我再次查看数据包,发现虽然对应的键存在了,但是值却不对,都是默认值。我意识到,是 Seafile-server 中缺失了对应数据的传递实现。
于是顺着 RPC 调用的顺序,我找到了处理它的函数:
GList *
seaf_repo_manager_list_dir_with_perm (SeafRepoManager *mgr,
const char *repo_id,
const char *dir_path,
const char *dir_id,
const char *user,
int offset,
int limit,
GError **error)
这个函数能够获取一个目录中的全部文件和文件夹信息,返回一个 Dirent 列表。
找到 dirent.vala
,内容如下所示:
namespace Seafile {
public class Dirent : Object {
......
public bool is_locked { set; get; }
public string lock_owner { set; get; }
public int64 lock_time { set; get; }
public bool is_shared { set; get; }
}
......
} // namespace
可以看到,锁相关的结构居然还在!
那么,我们只要在返回数据之前,把每个文件的锁信息也放进去,应该就可以了。但是这一步花费了我较多的精力,主要就是:对于每个列表中的项目,我们只有它的 id 和名字,并没有它的 path。而我们的锁却是用 path 来区分文件的,那该怎么判断呢?
一时间没有头绪,直到我看到了它对权限信息的处理,我发现了:
if (shared_sub_dirs && S_ISDIR(dent->mode)) {
if (strcmp (dir_path, "/") == 0) {
cur_path = g_strconcat (dir_path, dent->name, NULL);
} else {
cur_path = g_strconcat (dir_path, "/", dent->name, NULL);
}
is_shared = g_hash_table_lookup (shared_sub_dirs, cur_path) ? TRUE : FALSE;
g_free (cur_path);
g_object_set (d, "is_shared", is_shared, NULL);
}
由于这个函数已经接受了文件夹地址,然后又知道每个文件的名字,那我结合一下,不就是 path 了吗?
基于这种想法,结合前面写的 get_locked_files 函数,我很快为文件添加了锁信息:
if (shared_sub_dirs && S_ISREG(dent->mode)) {
if (strcmp (dir_path, "/") == 0) {
cur_path = g_strconcat (dir_path, dent->name, NULL);
} else {
cur_path = g_strconcat (dir_path, "/", dent->name, NULL);
}
seaf_message("REG det, name='%s', path='%s'\n", dent->name, cur_path);
GList *ptr;
for (ptr = file_locks; ptr; ptr = ptr->next)
{
if (strcmp(seafile_file_lock_get_path(ptr->data), cur_path) == 0)
{
g_object_set (d,
"is_locked", TRUE,
"lock_owner", seafile_file_lock_get_user(ptr->data),
"lock_time", seafile_file_lock_get_lock_time(ptr->data),
NULL);
break;
}
}
g_free (cur_path);
}
完成后重新编译,终于可以看到文件夹的锁图标了,所有功能正常。
第四次测试:共享文件夹中锁表现错误
具体表现为:在自己的文件夹里锁能正常显示,但是在共享文件夹里却不能显示。
为了检查,我在 seaf_repo_manager_list_dir_with_perm
函数中添加了中间输出结果,每遇到一个带锁的文件,都输出一条 Message。但是却发现,没有 Message 输出,然后仔细检查刚刚加的代码,发现:
if (shared_sub_dirs && S_ISREG(dent->mode)) {
显然抄的有点过头了,判定条件连看也没看直接抄了。。。
把 shared_sub_dirs
去掉,再次编译,问题解决。
第五次测试:验证默认锁时间是否有效
前一天晚上创建了锁,界面一直保持,第二天起来刷新,锁不消失,到12小时后消失,证明默认 12 小时的锁生效了。
至此,测试完毕,锁功能需求完成。