前言
本篇文章用於講解如何把本地的數據內容和Evernote的數據進行同步。大部分內容翻譯自《Evernote Synchronization via EDAM v1.0.5》,就文檔而言應該是Evernote官網上比較新的,是去年三月才出的。但是因為各種原因文檔中的部分內容已經過時,或者和API並不是完全符合,也有部分缺陷。所以在這不是一篇完全的翻譯文,中間會加入我個人的部分理解,也有所刪除修改。本文內容主要是文檔中的第三部分。另外附有四五部分的,助於理解,因為就兩張圖,翻譯成中文后反倒不方便。文檔的前兩部分主要是一些簡介,閱讀本文必須擁有一些必要的預備知識(如EvernoteAPI的基本使用),文檔的前兩部分就沒有太大意思。有必要的地方我會盡量也寫出來。關於文檔的最后一部分,有關鏈接筆記的內容,因為我個人暫時沒有需要,所以就沒有翻譯了,而且關於鏈接筆記平時在使用Evernote的時候我也沒有太多使用,沒有什么認識,就不敢亂寫。最重要的是,在這篇文章之后我會在發一篇《Evernote Sync Via EDAM (代碼篇)》,講解我在程序中實現的同步功能。不幸的是真的實現代碼和這一篇的理論有很大脫節,有很多原因,具體我在這篇的總結和下一篇文章中會具體提到。
最后給出《Evernote Synchronization via EDAM》的下載地址http://dev.evernote.com/media/pdf/edam-sync.pdf
預備知識
清楚Evernote中的基本元素概念,包括筆記(Note)筆記本(Notebook)標簽(Tag)資源(Resource)搜索記錄(SavedSearch)等等。
使用基本的EvernoteAPI,清楚UserSrore和NoteStroe,這個部分自然是參考http://dev.evernote.com/documentation/cloud/和http://dev.evernote.com/documentation/reference/,翻看實例代碼是個很不錯的途徑
USN(Update Sequence Number)這個是整個同步系統中最重要的東西。他用於標識賬戶中的每一次修改。每次修改后賬戶的USN就會+1.每一個對象(筆記本,筆記,標簽,等等所有的東西)都會有一個USN,標識着一個對象最后一次被修改時的賬戶USN。這邊有點不好理解。舉一個例子。在某一個時刻賬戶的USN是100.我添加一個筆記Note1,那么賬戶的USN會變成101,Note1的USN也是101.然后我再添加一個筆記Note2,這時賬戶的USN會變成102,Nnote2的USN也是102,Note1的還是101.這樣一來我們每次同步后記錄一下當時賬戶的USN保存為LastUSN,下次同步的時候,如果賬戶的USN>LastUSN,說明賬戶中有東西被修改了。對於每一個對象,比如Note1的USN>LastUSN說明服務器端的Note1被修改了。
幾個名詞的解釋
刪除(delete)在指把筆記移動到回收站(Trash),還是可以獲取的筆記的只不過獲取到的筆記的Active屬性為False
消除(Expunge)也就是永久性刪除,對於筆記有移動到回收站和永久性刪除的區別,而其他對象都只有永久性刪除。
同步操作的偽代碼
一下的偽代碼用於闡述客戶端與服務端同步的部分
服務端變量
updateCount 當前賬戶最新(最大)的USN
fullSyncBefore 客戶端執行增量同步或者完全同步的緩存截止時間。就是一個時間戳,這個變量的值通常是有東西被從賬戶永久性刪除的時間點,或者是非法客戶端USN造成一些服務器問題的時間點。
客戶端變量
lastUpdateCount 上次同步獲取的服務器端的updateCount變量
lastSyncTime 上次同步的時間(這個時間是從服務器上獲取的,也就是服務器時間,關於時間的表示問題,建議看一下http://dev.evernote.com/documentation/reference/Types.html#Typedef_Timestamp,特別是在.NET平台下開發的朋友,他的時間表示和DateTime不同。處理方法會在下一篇文章中給出。)
認證
這邊應該有個第1步的。但是這個部分坑爹了,文檔中的UserStroe.authenticate函數在前幾個月剛剛被廢除,也就是說現在必須使用OAuth進行認證獲取Token,因為我暫時還沒有弄好這個問題。所以我現在還是在使用DeveloperToken,具體內容可以參考http://blog.evernote.com/tech/2012/04/24/security-enhancements-for-third-party-authentication/
同步狀態
2 如果客戶端從來沒有和服務器同步過,就跳轉到完全同步
3 使用NoteStrore.getSyncState(…)獲取服務器端的updateCount和fullSyncBefore的值
a 如果fullSyncBrfore > lastSyncTime 跳轉到完全同步
b 如果 updateCount = lastUpdateCount 說明服務器端沒有更新過,跳轉到 發送改變
c 不然 就跳轉到增量同步
完全同步
4 使用NoteStore.getSyncChunk(…,afterUSN=0,maxEntries),從服務器獲取第一塊數據。這里要解釋兩個東西。一個是數據,這里數據只所有的類型對象,但是針對於想筆記,資源這種大對象,這里值返回一些元數據,元數據包括Guid,Title等,而不包括Content,binary resources這種很大的字段。如果是標簽(Tag)搜索記錄(SavedSearches)等等這種小對象,那么就會返回完整的對象。還有被永久性刪除的對象只返回GUID。二是關於兩個參數的解釋,服務器會返回USN>agterUSN的對象,但是最多返回maxEntries個。這就意味着我們可能需要通過多次獲取數據,並合並才能獲取到全部的數據。下一小步就是完成這個工作。
a 如果上一步返回的Chunk對象的chunkHighUSN小於Chunk對象的updateCount.保存一下現在這個Chunk對象,並且請求下一個Chunk,通過反復執行NoteStroe.getSyncChunk(…,afterUSN=cunkHighUSN,..)
5 按順序數據保存的多個Chunk對象(我們把這多個Chunk對象集合稱為同步塊),來構建當前服務器的狀態
a 為同步塊里服務器端的標簽(tags)建立一個列表(以GUID為唯一標示符),搜索同步塊,按順序把標簽添加到列表,從列表中移除被標記為永久性刪除(expunged)的標簽(通過看guid)
- i 如果一個標簽在服務器列表,但是不在客戶端,那么把它添加到客戶端的數據庫。
- ii 如果有同名標簽,但是GUID不同,按以下步驟處理
1 如果已存在的標簽有臟標記(在本地被修改過),那么說明用戶在服務器創建了一個標簽,在客戶端也離線的時候創建了一個同名的標簽。這個時候需要把他們合並, 或者報告沖突,讓用戶決定如何處理
2 不然就把客戶端的tag重命名一下
iii 如果一個標簽在客戶端,但是不在服務端
1 如果標簽沒有臟標記,或者如果它之前已經有被上傳到服務器過,就把它從客戶端刪除
2 不然就說明這個是客戶端新建的,我們一會會上傳它的
iv 如果一個標簽在客戶端和服務器兩邊都存在
1 如果他們有相同的USN 並且沒有臟標記,那么他們是已經同步了的
2 如果他們有相同的USN,但是客戶端的有臟標記,那么他一會會被上傳到服務器的
3 如果服務器端的標簽有比較高的USN並且客戶端沒有臟標記,那就把客戶端的標簽更新成服務端的樣子(注意要處理同名沖突)
4 如果服務端有更高的USN,並且客戶端有臟標記。說明它被兩端都修改過了,可以嘗試合並或者報告沖突讓用戶決定吧
b 對搜索記錄(SavedSearches)實現相同的算法
c 對筆記本(Notebook)實現相同的算法,如果在客戶端刪除一個筆記本,那么要把它所有的筆記(Notes)和資源(Resources)也都刪除掉
d 對鏈接筆記本(LinkedNotesbooks)實現相同算法
e 對筆記(Note)實現相同算法,注意上面提到過的我們只獲取到了沒有Content的元數據,所以還需要使用NoteSrore.getNoteContent(...)來獲取筆記的完整數據。另外筆記的Title是允許重名的,所以就不用擔心同名沖突
6 完成了和服務器的數據合並,把服務器變量updateCount保存到lastUpdateCount,還有吧服務器的當前時間(currenttime)保存到lastSyncTime
7 轉去發送改變
增量同步
8 用第4步的方式獲取同步塊,但是afterUSN要設置成lastUpdateCount
9 處理同步塊中的列表在客戶端添加或者更新數據
a 為同步塊里服務器端的標簽(tags)建立一個列表(以GUID為唯一標示符),搜索同步塊,按順序把標簽添加到列表,從列表中移除被標記為永久性刪除(expunged)的標簽(通過看guid)
- i 如果一個標簽在服務器列表,但是不在客戶端,那么把它添加到客戶端的數據庫。
- ii 如果有同名標簽,但是GUID不同,按以下步驟處理
1 如果已存在的標簽有臟標記(在本地被修改過),那么說明用戶在服務器創建了一個標簽,在客戶端也離線的時候創建了一個同名的標簽。這個時候需要把他們合並, 或者報告沖突,讓用戶決定如何處理
2 不然就把客戶端的tag重命名一下
iii 如果一個標簽在客戶端和服務器兩邊都存在
3 如果客戶端沒有臟標記,那就把客戶端的標簽更新成服務端的樣子(注意要處理同名沖突)
4 如果客戶端有臟標記。說明它被兩端都修改過了,可以嘗試合並或者報告沖突讓用戶決定吧
b 對資源實現相同的算法
c 對搜索記錄(SavedSearches)實現相同的算法
d 對筆記本(Notebook)實現相同的算法
e 對鏈接筆記本(LinkedNotesbooks)實現相同算法
f 對筆記(Note)實現相同算法,使用NoteSrore.getNoteContent(...)來獲取筆記的完整數據。
10 按順序處理需要從客戶端刪除的數據
a 從同步匯集所有被永久刪除(Expunge)的GUID(筆記的),按照匯集到的GUID,從客戶端刪除
b 對筆記本做相同處理,注意刪除筆記本時要刪除所有的筆記和資源
c 對搜索記錄做相同處理
d 對標簽做相同處理
e 對鏈接筆記多相同處理
11 完成了和服務器的數據合並,把服務器變量updateCount保存到lastUpdateCount,還有吧服務器的當前時間(currenttime)保存到lastSyncTime
12 轉去發送改變
發送改變
13 對每一個本地有臟標記的標簽進行如下處理
a 如果標記是新的(本地的USN沒有被設置過),通過NoteStore.createTag(..)把它添加到服務器。如果服務器報告了一個沖突,客戶端必須在本地處理這個沖突。會產生沖突的原因是Evernote並不對同步提供鎖,所以我可以再你獲取同步塊之后,另外有一個客戶端對服務器的內容作了部分修改。不過這個概率是在很小。如果服務端報告GUID重復了,那就在本地換一個GUID把(這個概率就更小了)
b 如果標簽被修改過的(本地的USN被設置過)使用NoteStore.updateTag(....)把服務器的內容更新。要處理同名沖突
c 不論是上面哪種情況,都要做一下的USN的驗證
i 如果 USN = lastUpdateCount +1 說明客戶端同步成功,把lastUpdateCount修改成新的USN
ii 如果USN > lastUpdateCount +1 那么久說明同步不成功,那就回去在做一個增量同步吧
14 對有臟標記的搜索記錄執行同樣的算法
15 對有臟標記的筆記本執行同樣的算法
16 對有臟標記的筆記執行同樣的算法,注意客戶端是使用NoteStore.createNote()必須傳送有完整數據(包括ContentPresenter resource data 等)的筆記(這里注意使用createNote添加到服務器端添加的Note的GUID和本地的會不一樣,就好添加成果后進行修補),在使用NoteStore.updateNote(..)的時候只需要傳送有修改的字段就可以了。
完全同步的實例
增量同步實例
總結
完全同步和增量同步的幾個區別
1 最主要的區別就是獲取同步塊的時候,afterUSN參數設置的不同。增量同步認為不用處理lastUpdateCount之前的所有數據
2 完全同步時候不會刪除本地多余的數據
整個同步模型,我覺得稍顯復雜,但是可能在需要把整個賬戶的數據全部同步的時候,就必須把完全同步和增量同步分開把。對於大部分只是用固定某些筆記本,或者某些固定標簽的應用來說應該不需要這么復雜的同步模型
還有一點致命的問題就是,本地刪除的數據,沒法被同步到服務端。也就是我在本地刪除了一個數據,沒法通過同步吧服務端的數據也刪除。這一點是我在實際代碼中沒有使用這個模型的原因。在下一篇的文章中我會展示一份同步某一個特定筆記本中所有筆記的代碼。