Evernote Sync Via EDAM (理论篇)
前言
本篇文章用于讲解如何把本地的数据内容和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 完全同步时候不会删除本地多余的数据
整个同步模型,我觉得稍显复杂,但是可能在需要把整个账户的数据全部同步的时候,就必须把完全同步和增量同步分开把。对于大部分只是用固定某些笔记本,或者某些固定标签的应用来说应该不需要这么复杂的同步模型
还有一点致命的问题就是,本地删除的数据,没法被同步到服务端。也就是我在本地删除了一个数据,没法通过同步吧服务端的数据也删除。这一点是我在实际代码中没有使用这个模型的原因。在下一篇的文章中我会展示一份同步某一个特定笔记本中所有笔记的代码。