【网盘项目日志】20210420:Seafile 锁系统开发日志(2)

注:本日志创作时,日志系统已经完成开发和调试,本日志为补档。

锁系统具体实现方式思考

有了前后交互的框架,接下来我们就需要考虑一下如何实现数据存储了。

其实从前面的 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(无人加锁)。
  • 判断锁拥有者和判断者,若二者相同,返回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 小时的锁生效了。

至此,测试完毕,锁功能需求完成。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值