同步算法:处理云端事件

同步原理:处理本地事件 一文介绍了Obsidian中本地文件事件的特点以及处理方式,今天这篇文章介绍如何识别云端文件事件并进行适当的处理。

在正式开始本文前,我们先考虑一个问题:

假如此时云端文件也发生了变化,需要如何处理?本文从云端事件的识别、冲突的识别和处理方面回答这个问题。

带着这个问题,我们开始介绍 Sync Vault项目 的自动同步算法。
在同步中,我们关心一个文件的创建、修改、删除和移动事件,对于云端的文件也是这样。由于云端的事件和云服务相关,一般有以下两种情况。

  • 第一种:云服务本身提供查询文件变化的能力,比如onedrive就有delta接口可以查询距离上次查询发生的变化。甚至有webhook接口通知文件变化,这就极大的简化可客户端的同步机制的实现。
  • 第二种:云服务只是提供了文件存储功能。

我们看下第二种:只有文件存储功能的云服务如何进行同步。比如百度网盘、阿里云盘等众多网盘都是这样的。

文件表示

先用一种数据结构来表示文件对象。如下所示,其中的fsid很关键,用于唯一表征一个文件,也就是说不管文件发生了修改还是移动,这个id是不变的。

interface FileEntry {
	path: string; // absolute file path
	isdir: boolean;
	fsid: string;
	parentFileId?: string;
	ctime: number; /* 秒 */
	mtime: number; /* 秒 */
	size: number; /* 字节 */
}

任意时刻云端快照表示

通过一个Snapshot类型表示,由FileEntry构成。简单示意如下:

文件快照

识别云端变化

获取两次快照,这两次快照之间的差异就是云端文件变化。cloudChanges = diff(currentCloudSnapshot, prevCloudSnapshot)

具体的事件识别方法如下表所示:

文件事件识别方法:针对前后两次snapshot
文件移动fsid相同,path不同
文件删除前一次的FileEntry所在的路径,在当前snapshot中不存在FileEntry
文件修改path相同,云端哈希不同(理想情况),或者最近修改时间、size不同(非理想情况)

这里有一种特别的情况:云端文件的创建。

我们可以不用比较两次snapshot,而是将当前snapshot直接和本地的文件做比对,如果云端文件对应的路径在本地不存在文件,那么这个文件就是云端新建的。但是也有可能云端的新文件是通过移动旧文件产生的,这时候和下文的事件识别和解决有机结合就可以发现真正的新建文件。因此我们先不关心云端文件创建事件。

处理云端事件

提供一个apply方法,用于处理云端事件。在[[处理本地事件]]中,我们已经获取了预处理的 本地文件事件:historyEvents,它是FileEvent的集合。

interface FileEvent {
	type: FileEventTypeEnum; /* 对应文件创建、修改、删除、移动事件 */
	targetPath: string;
	isDir: boolean;
	oldPath?: string; /* rename有oldPath */
	timestamp: number;
	mark: ResolveStatus; /* 用于冲突解决 */
}

同步状态初始化

现在我们已经获取了本地文件事件和云端文件事件,在每一次同步开始的时候,先生成本地文件和云端文件的并集,这个时候还不知道云端发生了什么事件。本地文件的初始同步状态为LocalCreated,云端文件的初始同步状态为RemoteCreated,针对本地和云端都存在的文件,通过checkFileNodeSyncStatus(localFile, remoteFile)方法,生成初始同步状态。

冲突识别

云端事件和本地事件最终都集合成各自的Map,以文件路径为key,事件为Value。通过遍历事件来识别不同的冲突,伪代码如下:resolve(localEvent, remoteEvent): Resolution
其中的Resolution指示当前冲突应该如何处理本地文件,对应有5中类型,如下所示。

不同冲突触发的Resolution如下表所示(应对多端文件同步的方法):

本地事件\云端事件文件修改文件移动文件删除
文件修改MergeMoveLocal, UploadUpload
文件移动MoveRemote,DownloadDownload, Upload[^1]Upload[^2]
文件删除Download[^3]Download无冲突

冲突处理

在冲突处理中更新文件的同步状态。

Resolution类型处理方式
Upload文件上传,本地文件状态置为已同步
Download下载文件
Merge获取云端文件内容,合并文件
MoveLocal移动本地文件,被移动文件状态置为已同步
MoveRemote移动云端文件,被移动文件状态置为已同步

经过冲突处理后,依然保留LocalCreatedRemoteCreated状态的为真正的新建文件。可以直接上传或者下载。

同步流程总结

最终从上面的分析和设计,得到了如下的同步流程,分成4个步骤:

  • 预处理:获取云端快照,初始化同步状态。
  • 快照处理:识别并解决冲突,
  • 后处理:同步未同步的文件。
  • 终结:清理现场,准备下次同步。

依靠这一套冲突解决机制,我们就可以在文件存储服务器上应对多文件冲突。但是这种冲突解决依然只能是文件级别的,如果需要更细粒度的冲突解决方式,甚至协同编辑,就需要一些更加高级的方法,比如CRDT,Sync Vault项目中也实现了CRDT,可以让Obsidian具备协同编辑功能,将在后续博文中进行介绍,敬请关注。

思考

  1. 如果文件对象没有表示唯一性的fsid,会发生什么?
  2. 如果在冲突处理的过程中,云端文件又发生了变化,会发生什么?
  3. 文件下载到本地和用户修改本地文件都会触发Obsidian的modify事件,两者如何区分?

基于本文介绍的事件识别和冲突处理方法,可以尝试回答一下第一个问题和第二个问题。至于第三个问题:后续会介绍sync vault中的event manager,它用一种巧妙的方法对事件进行了区别,敬请关注😘。