使用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評論。