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

何涵育
2023-12-01

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

需求原因

锁机制在涉及到资源管理的系统中,尤其是涉及到多进程、多任务等需求的系统中尤为常见,主要是确保资源处理单位在其任务周期内对资源的独占。

在 Seafile 中,由于支持文件分享,且支持文件的在线编辑,那么就必然会涉及到一个文件被多人同时修改的多人协作情况。为了防止多人同时修改文件导致文件内容版本出现错误,Seafile Pro 版本设计了一个“锁”系统,能够允许用户在需要进行文件修改的时候先把文件“锁”起来,编辑完以后再释放锁,保证同一时间仅有一个用户编辑文件。

对系统情况确认

其实,Seafile 社区版就是从 Pro 版阉割下来的。尤其是 Seahub 中,许多 Pro 版本相关的功能界面仅仅是通过一个 is_pro() 函数判断的,这给我们的开发带来了不少便利。

根据研究、推论可以总结出,锁机制在 Seafile Pro 中实现方式和步骤:

确认文件锁状态

  • 用户访问对应文件,进行预览;
  • Seahub 向服务端发送 RPC 请求,检视文件状态;
  • Seafile-server 返回对应文件的锁状态,包括是否被锁、是否为自己锁的。

获取文件锁信息

  • 用户访问对应文件,进行预览;
  • Seahub 向服务端发送 RPC 请求,请求获取锁信息;
  • Seafile-server 返回对应文件的锁状态,包括是否被锁、锁拥有者、锁定时间、锁过期时间。

锁定文件

  • 用户点击锁定按钮,需要锁定文件;
  • Seahub 向服务端发送 RPC 请求,确认文件锁状态;
  • Seafile-server 返回对应文件的锁状态,如果:
    • 文件已经被锁:锁定失败;
    • 文件未被锁定:Seahub 向服务端发送 RPC 请求,请求锁定文件;
  • Seafile-server 在数据库创建锁信息。

解锁文件

  • 用户点击锁定按钮,需要锁定文件;
  • Seahub 向服务端发送 RPC 请求,确认文件锁状态;
  • Seafile-server 返回对应文件的锁状态,如果:
    • 文件已经被锁:Seahub 向服务端发送 RPC 请求,获取文件锁信息;
    • Seafile-server 返回对应文件的锁信息,如果:
      • 文件是自己锁的:Seahub 向服务端发送 RPC 请求,请求解锁文件;
      • 文件是别人锁的:报告权限不足,解锁失败;
    • 文件未被锁定:解锁失败;
  • Seafile-server 在数据库移除锁信息。

在 Seahub 中,几乎所有与锁机制相关的代码全部都使用 is_pro() 函数进行了限制。不过,在 Seafile-server,也就是这个服务实现的底层就完全不一样了。全文搜索 is_pro ,甚至没有发现任何相似的字眼,看来是把后端代码删的一干二净了。那么,要想把原本 pro 中的锁机制恢复出来,主要有以下几个任务:

  • 恢复 Seafile-server 在 python/seaservapi.py 中定义的 RPC API;
  • 恢复 Seafile-server 中 C 代码的定义;
  • 实现上述 C 代码。

研究:Seafile Pro

要想恢复 Pro 的功能,还得从 Pro 下手,欸嘿。

在前面的研究中,我们知道,Seafile-server 和 Seahub 之间的连接是通过 Searpc 来实现的。这个连接有个特点,就是需要一个 Python 的 api 文件,为对应的 Python RPC 客户端提供可用的函数列表。那么就意味着,这个接口文件一定是未经加密、直接以 Python 源代码的形式出现的。

在 Seafile-pro 的 Docker 控制台中运行如下命令:

find / -name api.py

果不其然,得到了系统的输出:

/opt/seafile/seafile-pro-server-7.1.14/seahub/thirdpart/requests/api.py
/opt/seafile/seafile-pro-server-7.1.14/seahub/thirdpart/django/contrib/messages/api.py
/opt/seafile/seafile-pro-server-7.1.14/seafile/lib/python3.6/site-packages/seaserv/api.py
/usr/local/lib/python3.6/dist-packages/cffi/api.py
/usr/local/lib/python3.6/dist-packages/aliyunsdkcore/vendored/requests/api.py
/usr/local/lib/python3.6/dist-packages/requests/api.py
/usr/local/lib/python3.6/dist-packages/oss2/api.py
/usr/local/lib/python3.6/dist-packages/sqlalchemy/ext/declarative/api.py
/usr/local/lib/python3.6/dist-packages/sqlalchemy/event/api.py
/usr/local/lib/python3.6/dist-packages/django/contrib/messages/api.py
/usr/local/lib/python3.6/dist-packages/pip/_vendor/requests/api.py

通过对社区版的了解,我们知道,/opt/seafile/seafile-pro-server-7.1.14/seafile/lib/python3.6/site-packages/seaserv/api.py 中的内容是最有可能的。vim 打开这个文件,果然发现了蕴藏在其中的接口定义:

 # file lock
    def get_locked_files(self, repo_id):
        """
        Return a list of FileLock objects (lib/repo.vala)
        """
        return seafserv_threaded_rpc.get_locked_files(repo_id)

    def lock_file(self, repo_id, path, user, expire):
        return seafserv_threaded_rpc.lock_file(repo_id, path, user, expire)

    def unlock_file(self, repo_id, path):
        return seafserv_threaded_rpc.unlock_file(repo_id, path)

    FILE_LOCKED_BY_OTHERS = 1
    FILE_LOCKED_BY_ME = 2

    def check_file_lock(self, repo_id, path, user):
    	"""
        Returns:
        * 0: if file is not locked
        * 1: if file is locked by others
        * 2: if file is locked by me
        """
        return seafserv_threaded_rpc.check_file_lock(repo_id, path, user)

    def refresh_file_lock(self, repo_id, path):
        """
        Returns:
        * 0: success
        * -1: error
        * -2: the file is not locked
        """
        return seafserv_threaded_rpc.refresh_file_lock(repo_id, path)

    def get_lock_info(self, repo_id, path):
        """
        Return filelock object
        """
        return seafserv_threaded_rpc.get_lock_info(repo_id, path);

这就为我们的工作带来了太多便利,因为我们已经知道了要实现锁机制所需要的全部函数。

恢复 api.py

回到我们的工作目录,打开seaserv/api.py

# file lock
    def check_file_lock(self, repo_id, path, user):
        """
        Always return 0 since CE doesn't support file locking.
        """
        return 0

只剩一个 check_file_lock 函数凄惨地矗立在那里,我们把刚才在 Pro 里发现的东西全部都拿来。

俏皮一下:

	def check_file_lock(self, repo_id, path, user):
        """
        Always return 0 since CE doesn't support file locking.
        No, not anymore!	<---- HERE
        Returns:
            0: file is not locked.
            1: file is locked by others.
            2: file is locked by me.
        """
        return seafserv_threaded_rpc.check_file_lock(repo_id, path, user)

按照 api.py,将 RPC 调用的代码恢复

根据前面的代码,我们需要在 seafserv_threaded_rpc 模块中把对应的 RPC 函数调用过程全部推理恢复出来,在文件seafile/rpcclient.py中:

 # file lock
    @searpc_func("objlist", ["string"])
    def seafile_get_locked_files(repo_id):
        pass
    get_locked_files = seafile_get_locked_files
    
	@searpc_func("int", ["string", "string", "string", "int64"])
    def seafile_lock_file(repo_id, path, user, expire):
        pass
    lock_file = seafile_lock_file
    
    #后面的不再展示了

在 Seafile-server 中完成 C 代码中的 RPC 函数注册

根据我们先前对 Searpc 的研究,要想在 C 代码中注册这些函数,必须确保函数的参数签名在文件 rpc_table.py 文件中列出。

根据检查,我们需要在 rpc_table.py 中添加以下条目:

[ "int", ["string", "string", "string", "int64"] ]

代表的是,int 类型返回值,前三个参数为 string,最后一个参数为 gint64,对应的是 seafile_lock_file 函数。

然后在我们的 Docker 容器中运行以下命令:

make clean
make
make install

Seafile-server 的 Makefile 文件中有相应的命令根据 rpc_table.py 文件生成 C 语言的签名。

然后,根据刚才的 api 文件,我们可以比较轻松的把六个C语言的函数注册写出来:

/* file lock */
    searpc_server_register_function ("seafserv-threaded-rpcserver",
                                     seafile_get_locked_files,
                                     "seafile_get_locked_files",
                                     searpc_signature_objlist__string());

    searpc_server_register_function ("seafserv-threaded-rpcserver",
                                     seafile_lock_file,
                                     "seafile_lock_file",
                                     searpc_signature_int__string_string_string_int64());

    searpc_server_register_function ("seafserv-threaded-rpcserver",
                                     seafile_unlock_file,
                                     "seafile_unlock_file",
                                     searpc_signature_int__string_string());

    searpc_server_register_function ("seafserv-threaded-rpcserver",
                                     seafile_check_file_lock,
                                     "seafile_check_file_lock",
                                     searpc_signature_int__string_string_string());

    searpc_server_register_function ("seafserv-threaded-rpcserver",
                                     seafile_refresh_file_lock,
                                     "refresh_file_lock",
                                     searpc_signature_int__string_string());

    searpc_server_register_function ("seafserv-threaded-rpcserver",
                                     seafile_get_lock_info,
                                     "get_lock_info",
                                     searpc_signature_object__string_string());

现在,RPC 相关的东西都定义好了,我们把这里面的 C 函数都实现一个最简单的功能:打印日志。

以锁文件为例:

int
seafile_lock_file (const char *repo_id,
                   const char *path,
                   const char *user,
                   gint64 expire)
{
    /*
     * Locks the file for specific user.
     * Returns:
     * 0: Lock success.
     * -1: File already locked, you cannot lock it.
     * -2: Permission denied.
     */
    seaf_message("-->Lock file(%s, %s, %s, %d)\n", repo_id, path, user, expire);
    return 0;
}

就这样,给六个文件全部先实现一个最简单的打印日志函数,编译后运行服务端。

解除 Seahub 中 is_pro() 限制

这一步比较简单,前面说过,前端大部分与锁相关的代码根本没有被去除,而只是用了 is_pro() 去限制。那么,我们只要在 Seahub 项目中全局查找 “is_pro()”,并把我们认为的与锁相关的全部代码对 pro 判断的语句全部去除就好啦!

在这里就不对它做详细介绍了。

当然,虽然这一步听起来很简单,但是实际上还是有一定难度的,用到 is_pro() 语句的地方多达几百处。

调试一下我们做好的接口

然后,解除了前端的 is_pro() 限制,在前端已经可以看到锁按钮了。

单击锁按钮,在控制台中输出了信息:

-->Check file lock(34151e41-f5bf-4477-aa4b-46b0c4eaafb5, /NewFile.txt, rentenglong@163.com)
-->Lock file(34151e41-f5bf-4477-aa4b-46b0c4eaafb5, /NewFile.txt, rentenglong@163.com, 0)

即证明,前后端的 RPC 交互已经完成,前后的 RPC 系统完全正常。接下来,我们只要把 C 代码里面定义的函数实现了,就万事大吉了。

 类似资料: