使用DecSync和vdirsyncer在多设备多服务间同步日历等

renyuneyun 2022年02月05日(周六) 1 mins

和许多当代人一样,我有多个电子设备,需要许多信息在多台设备之间同步。然而同时我对一般意义上的「云服务」这种中心化的服务并不喜欢,其中最主要的就是担心单点失效导致全部崩溃——从简单的暂时性服务失常到更严重的数据损坏,我都不太愿意接受。另一个原因是在部分网络情况下,一些服务访问会比较糟心——连接性和速度。

是的,英国也有类似墙的状况。和国内一样的是,官方对此不做评;和国内不一样的是,民众普遍对此没有任何认识。而由于民众没有认识,我无法确认它究竟是稳定的偶发性网络配置错误,还是刻意为之的限制。再叠加上我国确实存在墙,我也无法确认该行为是否也有墙的干扰。

这是我使用Syncthing来代替网盘的原因(介绍参见近期折腾一文),也是我去尝试在本文将要介绍的DecSyncvdirsyncer的动机。毕竟Syncthing只能同步目录/文件,而许多信息并不明确具有文件结构存储能力;而且Syncthing对冲突的解决能力也很弱,然而像日历、待办事项和联系人这些东西,会时不时修改,其在各次修改间的冲突须要被妥善解决。这就是为什么需要DecSync和vdirsyncer这类专门为日历等信息设计的软件。

本文会首先简单介绍一下两者,对比它们的特性和使用现实,最后介绍我最终为何需要以及如何联合使用它们。

当然,说是联合使用,其实也只是正常的使用两者,只不过配置下来会让数据在两者间流动,以在不同设备和服务间同步。

DecSync和vdirsyncer的基本介绍

DecSync

DecSync是39aldo39个人开发的一套工具(及标准),其开发目的即是它的名称:「去中心化同步」(Decentralized Synchronization)。该工具支持同步日历、联系人、待办事项以及RSS Feed。

它的基本工作模式即是将欲同步的数据扁平化为键值对,然后按照规定的格式存储到文件系统中。它的存储格式经过特意设计,可以完成多设备间的冲突解决。 需要注意的是,DecSync主要提供数据到文件的双向转换和冲突解决。它本身不带「同步」这半截,而是需要依托类似Syncthing(或网盘,但这和我的初衷相悖)的工具来完成这部分。

于是理所当然的,DecSync所支持的软件需要经过专门设计,以支持通过libdecsync(它的库)来读取数据。作者提供了许多说明,来解释各个部分是如何设计和工作的,以降低支持它的门槛。目前有一些软件已经支持DecSync(许多是作者自己适配的),参见仓库中的读我档案/README

设计良好的软件的存储部分应当都是模块化的,所以支持起来不会太过复杂。然而现实中由于DecSync是个人项目,没有资金或情绪推动,支持它的工具暂时并没有大范围铺开。但至少我在乎的平台都有(Linux、Android),状况也算是还行。

除此以外,DecSync的存储方式也有不足支持——因为要支持冲突解决,而且待同步设备的个数是未知的,于是存储中的旧项目不会自动删除,导致它会越来越大。作者说过用户可以通过自行删除旧项目(目录),只保留最新的来解决这个问题。但该方案始终需要手动干预——虽说存储上涨很慢,但也总要提根弦。

另外,DecSync只是一个个人项目,仍有一些问题——虽然暂时没发现数据丢失问题,但有一个烦人的小细节让我最终选择了本文的复杂方案。这个细节就是Android端的DecSyncCC经常会报错,说存储有问题,但又不告诉我错误究竟在哪。报了这个错就导致Android端无法同步,这就失去了使用它的本来意义。

vdirsyncer

另一边,vdirsyncer是一个设计来在多种服务和文件系统间同步的工具。它是pimutils的一部分——pimutils是一套简单风格的进行个人日常信息(即日历、待办事项、通讯录)管理的工具。

它也支持文件系统存储,通过自己的vdir(虚拟目录)标准来完成。于是,它也可以和Syncthing联用,以完成在多设备间的同步。然而由于它只有Python实现,并没有对Android进行支持,所以该方案仅能达成在两个(或多个)Linux设备(Windows之类也可以)间进行同步。

然而需要注意的是,vdirsyncer不建议通过Syncthing或网盘等方式进行同步,而要使用vdirsyncer进行同步——这是因为vdirsyncer的存储不具备冲突解决设计,于是使用Syncthing等文件同步工具的用户需要自己手动解决冲突与合并问题。官方说道可以使用git来管理,因为git比vdirsyncer现有的冲突解决机制更优秀。

同DecSync类似,vdirsyncer相关的应用软件也需要专门设计,以支持从它的存储中读取。这就是pimutils存在的原因——提供一系列实现来支持vdirsyncer的意义。

至此为止,似乎vdirsyncer比DecSync差了一截。然而vdirsyncer不止于此,它还支持CardDAV/CalDAV(日历等信息的万维网访问标准)的同步。这和前面的文件同步是叠加关系,于是两两组合就有了四种同步方案:文件到文件,DAV到文件,文件到DAV,和DAV到DAV。

这样的机制给了vdirsyncer更多选择,也是本文联合使用DecSync和vdirsyncer的基础。

小结

这两个软件(DecSync和vdirsyncer)都号称自己可以进行设备间同步,一般也意味着去中心化。然而DecSync明确列出自己是要去中心化,而vdirsyncer不是。造成的结果是DecSync的存储格式支持冲突解决,可以直接通过任意工具同步;而vdirsyncer不支持,不能这样做。已支持DecSync的有多种设备上的多种应用软件,而vdirsyncer方面只有官方的pimutils。然而vdirsyncer支持CalDAV/CardDAV这些标准,可以有更多用法,而DecSync完全不支持。

联合使用DecSync和vdirsyncer

对于简单的场景,只使用DecSync或vdirsyncer已经能满足需求,比如下面第一节将要描述的场景就是我以前的使用模式。然而后来我发现这一模式不够,于是经过一番探索,改为联合使用DecSync和vdirsyncer,目前使用更加顺心。下面先介绍原先的场景和用法,同时解释DecSync如何在该场景下工作;然后介绍后来的方案及我为什么需要联合使用两者。

下面仅用日历来做代表,各场景实际均支持日历、待办事项和联系人。

过去的简单场景——仅DecSync用例

基本场景是在我的两台电脑与一台Android手机之间进行同步。两台电脑均是我有完全权限的Linux系统,手机也是自己的Android。

这个场景下,设置起来很理所当然:三台设备均通过Syncthing同步一个目录(比如叫decsync-dir),然后分别安装对应的DecSync服务,配置使用该decsync-dir目录作为存储后端;最后在上层用相应可以使用的软件即可。

细节来说,我在Linux上使用的是radicale这个日历服务,并安装了它对应的radicale-decsync存储后端,然后在thunderbird上使用CalDAV/CardDAV(通过TbSync和CalDAV/CardDAV Provider提供)访问对应的radicale存储。在Android上简单一些,安装DecSyncCC来存取日历,直接同步到系统日历和联系人接口(content provider)上;使用OpenTasks来管理待办事项。 各个部分的关系基本是这个样子的:

  • Linux:decsync-dir -- radicale-decsync -- radicale (CaldDAV/CardDAV) -- CalDAV/CardDAV Provider for TbSync -- TbSync -- Thunderbird
  • Android:decsync-dir -- DecSyncCC -- 系统日历/联系人(content provider) / OpenTasks(待办事项)
  • decsync-dir目录通过Syncthing同步:decsync-dir -- Syncthing -- decsync-dir

更普适的复杂场景

如前面所说,我在使用DecSyncCC的时候,时不时会出现DecSyncCC报错,提示存储有问题,进而导致Android方面的日历、待办事项等无法同步。 而后来,我又去折腾了一下旧手机,装了SailfishOS。而DecSync并没有提供SailfishOS的软件,我也没弄明白如何将一个普通Linux软件打包成SailfishOS的软件。但SailfishOS提供了标准的CalDAV/CardDAV同步功能。

整体来说,这次的场景变成了:有两台具有完全权限控制的Linux,一台可能可以装DecSyncCC的Android,以及一台不能装DecSync的Sailfish。 然而我其实使用了公网服务器(运行nextCloud)来降低要求,场景实际上和这个不太一样,所以需要重新描述一下:

如何同时在一些设备上使用DecSync,并将日历等信息同步到一个公网服务器上?

对于多数人来说,直接使用公网服务器作为中心化存储终端即可满足需求。然而我的初衷正是不要这种中心化,所以需要像这样的更复杂的方案。

联合使用方案

经过一定时间折腾,弄出了联合使用DecSync和vdirsyncer的方案。其中DecSync还是之前的功能,即在各个DecSync设备之间同步;而vdirsyncer负责将DecSync这边的数据同步到公网服务器上。理论上说,这种方案可以支持更多服务方,比如同时再支持一个Google Calendar之类的。但vdirsyncer对Google的API的支持有一些问题,所以暂时作罢。

也可以说,这个方案是vdirsyncer正常使用方案的变种。毕竟DecSync对应部分在我的Linux设备上同时也是一个CalDAV/CardDAV服务,所以就是vdirsyncer在多个CalDAV/CardDAV之间同步。

和DecSync不同的是,vdirsyncer只在一个设备(作为同步的中枢)上设置一份。对于我来说,这个设备就是我的个人电脑。理论上为了保险(比如Syncthing同步了但vdirsyncer没同步,我却把电脑挂起了),我还可以在另一台电脑上也配置它。但由于最近一直都是remote working,所以暂时没有必要。

对于vdirsyncer,配置需要写专门的配置文件。文件存在$HOME/.vdirsyncer/config,是一个分节的键值对,且多数节有ID。

其实官方文档写了配置文件应该如何写,但官方的说明在我看来有些别扭,费了一些工夫才找到和理解我需要的各个部分。所以在这里我重新简单介绍一下它的配置文件结构和内容。各个部分会给一点例子来辅助解释,而最后会给我的配置文件,以供参考。

配置文件结构

配置文件主要分为三部分:

  1. general节,即基础配置
  2. pair节,即对同步的配置,需要小节名
  3. storage节,即对存储服务的配置,需要小节名

基础配置没什么说的,只有一项设置,即在哪存储状态信息。所以直接按这样写就行:

[general]
status_path = "~/.vdirsyncer/status/"

在general下面的pair和storage都可以随自己需要写无限个。而pair节需要使用storage节中定义的项目,所以需要先讲storage。

storage小节

每个storage小节都声明了一个存储对象,比如一个CalDAV服务。理所当然地,我们可以按自己的需求声明任意多个storage。当然,声明的这些storage要在下面的pair中用到,不然就没有意义了。

在storage节上,节后需要一个名称,作为该节的ID,比如[storage some_name]就是一个叫some_name的节。

然后内容就是对该节的一些细节声明了,比如类型是什么,地址是什么,用户名密码是什么等等。可以参考后文中我的配置,也可以参考官方文档的说明。注意每个storage只能声明一种类型,所以CardDAV和CalDAV需要分开,写成两个。

其中特殊的一点是,密码明文写在配置文件中虽然被支持,但由于不安全所以不推荐。更推荐的做法是通过password.fetch来调用外部方式获取密码,比如我的方案是调用密码管理器(pass)来获取密码:password.fetch= ["command", "pass", "PATH_TO_YOUR_PASSWORD"]官方文档对应部分有更多描述及更多用法。

pair小节

在定义了所需要的storage之后,就可以定义pair了。每个pair定义了如何在一对storage之间同步数据。

前文在storage中所声明的ID即是用在这里。而每个pair也需要一个ID,具体作用不详,但至少在日志和报错中有用。

每个pair的基本设置就是两个存储对象a和b,分别是一个storage的ID,以及它们的冲突解决机制(留空则不解决,会报错)。如前面提到的那样,vdirsyncer的冲突解决机制很简单乃至简陋,要么是a赢或b赢(一者覆盖另一者),要么是调用外部命令解决。

而稍微复杂一点,还可以声明要同步哪些项目,以及要同步什么额外的元信息。

同步的项目通过collections来声明。它的值是一个:code`[][3]`的二维数组,每一行声明了一个/一对要同步的组目:

  1. 在vdirsyncer中该组目的ID(似乎没有特殊用途)
  2. 在a存储中,该组目的ID
  3. 在b存储中,该组目的ID

这个机制很有用,因为在不同的CalDAV服务中,每个日历的ID很可能不一样(在我这里确实不一样,而且连格式都不一样)。通过该机制,可以将对应的日历连接起来。例子比如:

collections = [
        ["SOME_ID1", "SOME_ID2", "SOME_ID3"],
        ["SOME_ID4", "SOME_ID5", "SOME_ID6"]
        ]

注意最后不能有多余的逗号,否则不识别。

而同步的元信息很好理解,就是哪些额外信息要同步。其主要的就是日历的名称(显示名称)以及日历的颜色。

这样,设置多组pair,组合不同的storage,就可以完成不同的目标。

我的配置

有了上面的解释,就可以展示我的配置来做个示例了。理所当然地,我隐去了比较敏感的部分。

在这里DecSync用radicale做服务,所以其URL是radicale的URL,即localhost:5232。而公网服务器的nextCloud也是走CalDAV,配置类似。

[general]
status_path = "~/.vdirsyncer/status/"

[storage radicale_contacts]
type = "carddav"
url = "http://localhost:5232"
username = "YOUR_USER_NAME"
password = "YOUR_PASSWORD"
read_only = false

[storage radicale_calendars]
type = "caldav"
url = "http://localhost:5232"
username = "YOUR_USER_NAME"
password = "YOUR_PASSWORD"
read_only = false

[storage some_remote_contacts]
type = "carddav"
url = "URL_TO_DAV_SERVICE"
username = "YOUR_USER_NAME2"
password.fetch = ["command", "pass", "PATH_TO_YOUR_PASSWORD"]
read_only = false

[storage some_remote_calendars]
type = "caldav"
url = "URL_TO_DAV_SERVICE"
username = "YOUR_USER_NAME2"
password.fetch = ["command", "pass", "PATH_TO_YOUR_PASSWORD"]
read_only = false

[pair contacts_radicale_remote]
a = "some_remote_contacts"
b = "radicale_contacts"
collections = [["SOME_ID1", "SOME_ID2", "SOME_ID3"]]
conflict_resolution = "b wins"
metadata = ["color", "displayname"]

[pair calendars_radicale_remote]
a = "disroot_calendars"
b = "some_remote_calendars"
collections = [
        ["SOME_ID4", "SOME_ID5", "SOME_ID6"]
        ]
conflict_resolution = "a wins"
metadata = ["color", "displayname"]

这样,就配置好了各个项目。于是启动vdirsyncer的服务,日历就会在各个DecSync设备间通过Syncthing同步,以及从我的电脑到远程的nextCloud进行同步。如果需要,还可以继续增加另一个nextCloud实例到storage中,并增加几个pair指挥我的电脑或现有nextCloud到那个nextCloud之间进行同步——对,我们可以指挥两个远程实例之间同步,毕竟vdirsyncer并没有规定要有「本地」。最终,即使有一些设备无法访问,剩余的设备也可以轻易互相同步。


Related posts:

您可以在Hypothesis上的該群組內進行評論,或使用下面的Disqus評論。