# 99行代码解决 Onetab 的崩溃 & 原始数据备份

> 前言 大学时, 几乎是与Chrome一起知道的Onetab; 但作为一个学生, 冲浪并没有什么负担, 用的很少. 工作之后, 浏览器打开100个tab变成了家常便饭, 领导/同事询问资料也很频繁的发生, 此时Onetab作为网页记录本, 配合Ctrl+f 查找关键字, 简单易用.…

- 发布于: 2022-07-27
- 标签: Chrome, edge, nodejs, onetab, 浏览器, 疑难杂症
- 查看 HTML 版本: https://bilibili.cat/posts/onetabrestore

---

## 前言

大学时, 几乎是与Chrome一起知道的Onetab; 但作为一个学生, 冲浪并没有什么负担, 用的很少.  
工作之后, 浏览器打开100个tab变成了家常便饭, 领导/同事询问资料也很频繁的发生, 此时Onetab作为网页记录本, 配合Ctrl+f 查找关键字, 简单易用.

### **为什么不使用收藏?**

*笔者使用收藏的最后结果是书签栏展开之后会盖住整个屏幕.*

### **为什么不使用历史记录?**

*历史记录里还会有别的无关信息, 而且Chrome的历史记录并不可靠.*

### **为什么不使用其他知识管理工具?**

1.  *笔记类的应用太重了*
2.  *类onetab的应用由于先入为主的使用习惯, 并没能找到一款足够轻量且更高效的替代品*
3.  *onetab更新了黑暗模式, 并且缓存向前/向后/当前标签真的很实用*

## Situation

五天前, 在Windows更新重启后, 重新打开edge, onetab一直处于loading状态, 大概一段时间后页面崩溃, 报Out of Memory错误.

-   笔者从20年初新Edge上线起, 持续使用onetab, 记录有六万条网页, 未重装过系统.

<figure class="missing-asset"><figcaption>图片「Crash」已丢失：原站与互联网档案馆中都没有找到。<br><code>/upload/2022/07/QQ%E6%88%AA%E5%9B%BE20220727131653.png</code></figcaption></figure>

### 三板斧

1.  关闭所有网页和拓展, 重新启动edge, 重新打开onetab.  
    <ins><em>失败, 报Out of Memory错误.</em></ins>
2.  重启计算机, 后台为空情况下重新启动edge, 重新打开onetab.  
    <ins><em>失败, 报Out of Memory错误.</em></ins>
3.  将"C:\\User"文件夹 (Windows的用户数据目录) 完全拷贝, 恢复到一台新安装Windows10的PC上.  
    <ins><em>失败, 报Out of Memory错误.</em></ins>

此时基本可以断定是数据量太大导致的扩展崩溃.

-   其实之前五万条数据的时候打开就很慢了, 需要超过30s.
-   只是没想到内存狂魔Chrome居然也对手下的内存使用量做了限制.

![Chrome meme](https://i.pinimg.com/originals/95/11/f8/9511f847798db5ec08a51a3fa597d4c5.jpg)

## Target

此时问题已经找到, 万幸的是数据并没有丢失, 虽然完整性未知.  
两年多累积的数据日常使用频率也很高, 必须复原.

1.  尽可能地恢复Onetab的数据, 且导出为机器和人可以识别的数据格式
2.  在1.的基础上实现一个完整备份Onetab的自动化工具  
    ***Onetab自带的导出是纯txt文件, 还需要手动复制, 并且缺少tab组创建时间的字段***
3.  实现一个Onetab的兼容App, 增强数据查找的功能, 兼容1.导出的数据格式和Ontab原有的数据格式

## Actions

已经有了原因和目标了, 实现起来也就不是无头苍蝇了.

### 1\. Google前人经验

搜索"onetab data location"  
很快找到了onetab, 或者说是chromium类浏览器扩展的数据保存位置  

<figure class="missing-asset"><figcaption>图片「QQ图片20220727175536」已丢失：原站与互联网档案馆中都没有找到。<br><code>/upload/2022/07/QQ%E5%9B%BE%E7%89%8720220727175536.png</code></figcaption></figure>

  
再搜索"onetab data export", 又找到了一位前辈的开源方案, 基于Go.  
[![jianyuan/onetab-export-to-json](https://opengraph.githubassets.com/e99b6d5fccc2c9ee0d3a4d515f8b337bd98f88bbb64bf7cb3102d0ec121f84f5/jianyuan/onetab-export-to-json)](https://github.com/jianyuan/onetab-export-to-json)  
原本以为到此结束, 走上CV的道路, 不过issue里有一条记录说失效了, 笔者也没有Go的debug经验, 看了下代码不到100行, 遂决定自己实现一遍.

### 2\. 了解一下leveldb

[![google/leveldb](https://opengraph.githubassets.com/e99b6d5fccc2c9ee0d3a4d515f8b337bd98f88bbb64bf7cb3102d0ec121f84f5/google/leveldb)](https://github.com/google/leveldb)

-   由Google开源的NoSQL数据库
-   单线程, 高性能, 仅有本地实例
-   运行过程中大量使用内存加速

#### 一个标准的levedb数据库实例是一个文件夹的形式

<figure class="missing-asset"><figcaption>图片「leveldb实例」已丢失：原站与互联网档案馆中都没有找到。<br><code>/upload/2022/07/QQ%E5%9B%BE%E7%89%8720220727190212.png</code></figcaption></figure>

-   LOCK 数据库锁
-   MANIFEST 头文件
-   \*.ldb 主数据文件

从开源方案的介绍中可以得知, onetab使用leveldb的方案存储用户数据.  
笔者自己也开发过Chrome的扩展, 犹记得只能使用localStorge和indexDB两种API进行本地化, 因为运行在浏览器的扩展是没有文件读写和tcp连接这种能力的.  
其中localStorge最大5M, 五万条比较长JSON记录无论如何也存不下, onetab应当是使用indexDB存储数据的才对, 这leveldb是什么情况, 而且贡献者也是Google.  
一番搜索得知, leveldb就是indexDB的底层实现, indexDB其实是上层的API (名字误导系列).

> 谷歌浏览器Local Storage以 SQLite 格式（.localstorage文件）存储在文件夹中的一些数据。  
> 存储在IndexedDB文件夹中的一些其他数据（对于每个配置文件）采用LevelDB 格式。它是一种由 Google 开发的开源键值存储格式，托管在GitHub 上。  
> 要在 Chrome 之外修改 LevelDB 格式的文件，这不是一个简单的过程，因为您需要实现一个兼容的比较器来检查 Chrome 的 Indexed DB leveldb 实例。

### 3\. leveldb的GUI工具

了解原委之后, 本着先~确(tou)认(lan)~下数据情况的目的, 笔者先找了找leveldb的GUI工具.  
主流的Navicat和DataGrip肯定是不支持的, 继续GitHub冲浪.

[![heapwolf/levelui](https://opengraph.githubassets.com/e99b6d5fccc2c9ee0d3a4d515f8b337bd98f88bbb64bf7cb3102d0ec121f84f5/heapwolf/levelui)](https://github.com/heapwolf/levelui)

#### 一个electron的GUI实现, 编译失败

[![klausgao/electron-linvodb-manager](https://opengraph.githubassets.com/e99b6d5fccc2c9ee0d3a4d515f8b337bd98f88bbb64bf7cb3102d0ec121f84f5/klausgao/electron-linvodb-manager)](https://github.com/klausgao/electron-linvodb-manager)

#### 另一个electron的GUI实现, 读取不到数据

[![fastogt/fastonosql](https://opengraph.githubassets.com/e99b6d5fccc2c9ee0d3a4d515f8b337bd98f88bbb64bf7cb3102d0ec121f84f5/fastogt/fastonosql)](https://github.com/fastogt/fastonosql)

#### 一个C++/Qt的GUI实现, 成功读取到数据

<figure class="missing-asset"><figcaption>图片「软件截图」已丢失：原站与互联网档案馆中都没有找到。<br><code>/upload/2022/07/QQ%E6%88%AA%E5%9B%BE20220727183055.png</code></figcaption></figure>

  
并且能清楚看到onetab所使用的键名.

-   *注: 此软件基于GPL v3 开源, 但二进制版本需要付费, 新用户可试用. 笔者写作此篇文章时官网炸了, 无法注册, 可[下载旧版本](https://wwc.lanzouf.com/idgFq08i16va)运行, 无需注册登录.*

### 4\. 代码实现 基于 node.js 16.15

虽然之前两个electron实现的GUI无功而返, 但是也发现了他们依赖的 level 包, 提供了非常便捷的API, 开箱即用.

-   *此处笔者最先想使用python实现, 但 python 的 level 依赖需要在 linux 下编译安装, 遂作罢.*

```shell
$ npm install level
$ npm install mongodb
```

```JavaScript
const {promises: {writeFile, mkdir}} = requir('fs');
const {Level} = requir('level')
const MongoClient = requir('mongodb').MongoClient
// halo有点bug, 出现require会崩溃

const USER_HOME = (process.env.HOME || process.env.USERPROFILE).replaceAll("\\", "/")

const chrome = {
    name: "Google/Chrome",
    uuid: "chphlpgkkbolifaimnlloiipkdnihall"
}
const edge = {
    name: "Microsoft/Edge", // Edge || Edge Beta || Edge Dev || Edge Canary
    uuid: "hoimpamkkoehapgenciaoajfkfkpgfop"
}
// const firefox = "" // 火狐的扩展和目录结构与以上两位好兄弟不太一样, 无使用经验, 请自行修改

const winPath = `${USER_HOME}/AppData/Local/${edge.name}/User Data/Default/Local Extension Settings/${edge.uuid}`
// 以下两条为 Copilot 智能添加的代码, 请自行核对路径是否正确
// const macPath = `${USER_HOME}/Library/Application Support/${edge.name}/User Data/Default/Local Extension Settings/${edge.uuid}`
// const linuxPath = `${USER_HOME}/AppData/Local/${edge.name}/User Data/Default/Local Extension Settings/${edge.uuid}`

const ldb = new Level(winPath)
const url = "mongodb://alpha:123456aB@localhost:27017/?authSource=admin"

// 异步读取onetab的leveldb中的数据
const getOnetabData = async () => {
    await ldb.open()
    const str = await ldb.get('state')
    return JSON.parse(JSON.parse(str))
}

// 异步处理数据, 获得一个每条记录的数组
const getItems = async (onetabs) => {
    const items = []
    onetabs.forEach((ele, index) => {
        console.log(ele.id)
        ele.tabsMeta.map(ele => items.push(ele))
    })
    return items
}

// 异步连接mongo
const getMongoDB = async () => {
    const db = await MongoClient.connect(url)
    return await db
}

// 异步写入tab组
const insertGroups = async (db, onetabs) => {
    const dbo = await db.db("onetab")
    await dbo.collection("onetab_groups").insertMany(onetabs)
}

// 异步写入每条onetab记录
const insertItems = async (db, items) => {
    const dbo = db.db("onetab")
    await dbo.collection("onetab_items").insertMany(items)
}

// 生成用于备份的json数据文件和方便查看的CSV文件
const genExtFiles = async (items, onetabs) => {
    /*
     * 生成表头，\ufeff 是防止乱码
     * csv中以 `,` 换列，`\n`换行
     */
    let title = Object.keys(items[1])
    let csvContent = '\ufeff' + title.join(',') + '\n'

    // 添加表体
    items.forEach((item, index) => {
        let c = Object.values(item).join(',') + '\n'
        csvContent += c
    })
    // 生成文件夹
    await mkdir('onetab')

    // 生成csv文件
    await writeFile('./onetab/tab_items.csv', csvContent)

    // 生成JSON
    await writeFile('./onetab/tab_ori.json', JSON.stringify(onetabs))
    console.log('数据文件生成完毕, 请打开当前目录下的onetab文件夹查看.')
}

// 主函数
const main = async () => {
    const json = await getOnetabData()
    const db = await getMongoDB()
    const items = await getItems(json.tabGroups)
    await insertItems(db, items)
    await insertGroups(db, json.tabGroups)
    await genExtFiles(items, json)

    await db.close()
    console.log("写入MongoDB完成, 程序执行完毕. ")
}

main()
```

## Result

最后, 笔者基于最近很火的Tauri封装了一个二进制的桌面应用.

> 还不知道Tauri?  
> 看这里 [扔掉 Electron，拥抱基于 Rust 开发的 Tauri](https://segmentfault.com/a/1190000041440043)

### 它实现了以下的功能

-   以Onetab原有的样式查看导出的所有数据
-   增加了分页来应对大量数据
-   增加了日期和关键字筛选

### 即将添加

-   rust实现自动化备份, 无需额外安装node
-   云同步 webDAV或其他
-   数据分析 词云 NLP

### 仓库地址

[![endcloud/Onetab Re](https://opengraph.githubassets.com/e99b6d5fccc2c9ee0d3a4d515f8b337bd98f88bbb64bf7cb3102d0ec121f84f5/endcloud/onetab_restore_tauri)](https://github.com/endcloud/onetab_restore_tauri)
