正在恢复丢失的漫游笔记

2020-10-31 02:02:09

这篇文章深入探讨了一个可怕的数据丢失场景--我们将涵盖识别数据丢失、调查根本原因以及最终恢复数据的内容。

此错误影响ReadWise用户,他们在10/27导出其亮点(手动和自动)以漫游。如果您是这些用户中的一员,您应该尽快联系漫游支持&;Use My Recovery Code!

漫游是网络思维的笔记工具。它支持各种很酷的东西--这里与之相关的是,它每天都会自动创建一个新页面,那就是你的“每日笔记”(Daily Notes)。最近,我开始使用ReadWise,它吸收Kindle的亮点,并使用间隔重复来帮助你记住所读的内容。ReadWise具有漫游集成功能,可以自动将Kindle亮点添加到漫游中。不幸的是,由于Roam还没有一个公共API,Readwise的集成似乎有效地使用了Selenium-点击元素和粘贴亮点,这在本质上是不可靠的。

昨天,我醒来时没有带前一天的日记。灾难!幸运的是,在漫游松弛小组和来自Readwise的特里斯坦的帮助下,我能够隔离笔记删除的原因,甚至可以恢复我丢失的数据。事情是这样发生的:

Roam将Datascript用于其客户端数据库。与DATOMIC类似,Datascript将数据存储为定义为[e a v tx]的DATOM,或实体、属性、值和事务ID(递增整数)。如果您有兴趣了解更多,Datascript的作者有一个很好的概述。

对我们来说重要的是,Roam与其他web应用的不同之处在于它不会在后端存储所有的状态和历史。取而代之的是,roam的后端只存储一个快照的Datascript数据库(据我所知每天更新)和自上次快照以来的事务列表。如果我们可以在漫游下一个快照之前下载这两项内容,我们就有两个恢复的面包屑了:1.我们可以找到删除“我的每日笔记”页的事务2.我们还可以重建我们的Datascript数据库,在删除之前重新播放事务,并从中恢复我们的“每日笔记”!

我们的第一步是存储漫游的数据库快照和事务列表。Roam使用WebSocket连接将这些调用发送到其Web客户端,而不是REST API调用。这让我们的事情变得复杂起来:我们需要下载一个HAR文件,而不是仅仅用cURL保存API响应,幸运的是,它包含了更新的Chrome版本的WebSocket流量。HAR文件只是按时间顺序存储的JSON档案-只选择WebSocket流量很容易:

(parse-har[harfile]([json((Harfile)true)ws-message(json:log:entry(#((:_webSocketMessages%)Second:_webSocketMessages)ws-data(:data ws-message)]ws-data)

仔细检查这些数据,似乎漫游的WebSocket消息通常是JSON字符串(偶尔也是数字)。当一封邮件超过16KB时,它会拆分成多封邮件而不进行包装-因此我们需要将这些较大的邮件缝合在一起。检测非拆分消息的一种方法是尝试将其解析为JSON-如果它是有效的,我们可以说它是非拆分的。(这里有一个我们不太可能遇到的边缘情况:如果16KB的数据块碰巧也是有效的JSON,我们就不走运了。)。我们很幸运,我没有碰到这个!)。现在,我们可以按如下方式扩展parse-har:

(parse-har[harfile]([json((Harfile)true)ws-message(json:log:entry(#((:_webSocketMessages%)Second:_webSocketMessages)ws-data(:data ws-message)try-parse#((%true)(Throwable_Nil));;Roam通过WS消息发送一系列JSON对象。;;如果一个对象大于16KB,它会被拆分;;多条消息-因此我们需要将它们缝合在一起。WS-json(([{:keys[Done Partial]}Next]([Potential-json-str(Partial Next)]([json(Potential-json-str)]{:Done(Done Json):Partial";";}{:Done Done:Partial Potential-json-str})){:Done[]:Partial";";}ws-Data)](:Partial WS-json)";";))(:Done ws-json))。

有了我们解析的WebSocket消息,我们可以看到其中许多消息看起来像是事务。其中一个看起来特别可疑的字段有一个名为tx-meta的嵌套字段,值为delete-page!交易如下所示:

{:应用程序版本";0.7.4";,:电子邮件";[email protected]";,:Session-id";uuid95d98efd-c8fa-4412-87a4-e7b7201bee24";,:t 1603947791561,:Time 1603947791542,:tx";[[\";^\";,\";~:Block/uid\";,\";ogCRjInhE\";,\";~:Block/String\";,\";一些文本-此处\";,\";~:编辑/时间\";,1603947791363,\";~:编辑/电子邮件\";,\";[email protected]\";],[\";^\";,\";^0\";,\";4CpSytRnt\";,\";^1\";,\";突出显示首次同步日期为#Readwise 10.28,2020\";,\";^2\";,1603947791364,\";^3\";,\";[email protected]\";],[\";^\";,\";^0\";,\";C-IOsE50G\";,\";^1\";,\";2020年10月28日晚上11:03添加新亮点\";,\";^2\";,1603947791364,\";^3\";,\";,[\";~:db.fn/retractEntity\";,[\";^0\";,\";]],[\";^4\";,[\";^0\";,\";VwD08rqdT\";]],[\";^4\";,[\";^0\";,\";6VWOGgeAd\";],[\";^4\";,[\";^0\";,\";P56-fWN2O\";]],[\";^4\";,[\";^0\";,\";SffV3NfN2\&34;]],[\";^4\";,\";^0\";,\";SffV3NfN2\&34;]],[\";^4\";,[\";^0\";,\";qnZBZCGCv\";]],[\";^4\";,[\";^0\";,\";10-28-2020\";]]";,:tx-META{:Event-id";uuid719b009f-b969-47b6-b2db-41542d10b328";,:Event-Name&34;删除-第"页;,:tx-id";uuid289e80fc-4c27-4d54-9df4-d83ac0ceeaed";,:tx-name";删除页面";}}。

为了节省空间,我省略了大约90%的事务处理--但它们大同小异。这看起来绝对像删除了我的Daily Notes页面的事务:我在事务中看到db.fn/retractEntity和10-28-2020。有趣的是,该事务还捕获了两个ReadWise交互。这不是确凿的证据,但我的页面被神秘删除的同时,Readwise正在操作我的数据库,这一点绝对令人怀疑!

让我们在这里暂停一下,并与漫游松弛小组签到。已经有人开了一个关于数据丢失的帖子了!他们和其他人很快确认,他们也都启用了ReadWise的自动导出功能。再说一次,这并不是说Readwise是罪魁祸首,但这足以让我停止我正在做的事情,并禁用我的Readwise集成!我们还将在松弛线程中分享我们的知识,并要求受影响的用户像我们一样保存他们的roam Har文件。

后来,Readwise的创始人特里斯坦突然进入Slake,并迅速证实,最近的漫游行为变化与Readwise的整合相结合,可能会导致页面被删除。特里斯坦做出完美回应的巨大道具:他对问题进行分类,禁用功能以防止更多用户点击它,并在几个小时内修复并重新启用自动导出!特里斯坦也很善于沟通,承担全部责任,甚至提供退款,尽管我认为,当Roam还没有开放他们的公共API时,这些问题肯定会发生。

再次查看解析后的Har文件,我们会发现似乎是我们的序列化数据库-它是这样存储的:

这看起来像是过境!Transfer是一种类似JSON的格式,用于在应用程序之间发送数据(这篇文章是一个很好的介绍)。Datascript有它自己的传输处理程序集-让我们导入它,看看我们是否得到了一个工作的数据库!当然,我们还需要通过将传输编码的字符串粉碎在一起来组合Split-db。

(';[datcript.Transition:AS DT])(parse-db[parsed-har]([db-str(parsed-har;;数据库嵌套很深!(#(%:D:B:D:Split-db))first:D:B:D:Split-db(";";))](db-str))。

瞧--一个真正的Datascript数据库!我们可以通过查询确认它是我的漫游数据库:

(';[datcript.core:as d])([db(harfile()())conn(Db)](';[:find?e:where[?e:node/title";Daily Template";]@conn));;#{[1855]}。

对于正在运行的漫游数据库,我们的下一步是应用我们拥有的所有事务,直到删除事件。事务是传输编码的,我们必须进行相当多的数据操作才能获得它们的列表。一旦我们有了该列表,我们就可以对交易进行排序并按顺序应用它们:

(Apply-Transaction-Until[db parsed-har Until-time]([要应用的事务(parsed-har(#(%:D:B:D)(seqable?)。(CONCAT)(#((%First Name)";-MK";))(Second)(#((:time%)to-time))(:time)(:tx)(dt/read-transportstr)conn(Db)]([要应用的Tx事务](Conn Tx))(Conn))。

这里,Until-time是删除事务的时间。我们现在就快到了!在我的笔记被删除之前,我们已经成功地实现了我的漫游数据库!我们现在需要做的就是删除那个页面,然后我们就可以完成了!

恢复我删除的页面应该不会太难:我们可以使用Datascript的拉动功能递归地抓取整个页面的内容!

(recover[db]([conn(Db)note-eid((`[:find?e:where[?e:block/uid~Missing-Date]@conn))page(db';[:block/string{:block/Children[:block/order:block/string{:block/Children...}]}]note-eid)]page))(Db);#:block{:Children[#:block{:order 0,;:String";[[体育馆💪🏽]]";,;:儿童[#:Block{:Order 0,;;:String";#[[Weight Room]]";,;:Children[#:Block{:Order 0,:String";Back 5x5x225";};#:Block{:Order 1,:String";Seated row 4x12x100";}]}。

接下来,让我们将此数据结构转换为可以粘贴回Roam中的内容。我们可以通过递归地物化块的子块来物化字符串,在递归时增加缩进:

(物化[{:KEYS[块/字符串]:作为页面}级别]([缩进(级别4)子页面((:块/子页面)(:块/顺序)(#(%(级别)(#(";%s-%s";(str(缩进";";))%)(";\n";)])]((String(Children))(String";\n";Children)(Children)Children String:Else";";)(Recover[db]([conn(Db)note-eid((`[:find?e:where[?e:block/uid~Missing-Date]]@conn))页面(db';[:BLOCK/String{:BLOCK/CHILD[:BLOCK/ORDER:BLOCK/STRING{:BLOCK/CHILD...}]}]备注-eid)](第0页))(Db);-[[体育馆💪🏽]];-#[[重量间]];-工作台5x5x225;-座位行4x12x100。

就是这样!现在我们可以将该字符串复制到漫游中,我们已经完全恢复了丢失的漫游页面结构和所有内容!您可以在这里找到完整的代码。

这是多么好的一种方式来度过一个上午啊!我非常感谢我能找回丢失的笔记。我很幸运,Roam在删除后没有对我的数据库进行快照-这将使这种方法变得不可能。我也感谢来自Readwise的特里斯坦和其他所有在漫游中的人,他们帮助隔离和调试了这个问题。我希望我能够帮助他们中的至少几个人恢复他们的数据。