前言

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

为什么不使用收藏?

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

为什么不使用历史记录?

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

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

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

Situation

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

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

Crash

三板斧

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

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

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

Chrome meme

Target

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

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

Actions

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

1. Google前人经验

搜索"onetab data location"
很快找到了onetab, 或者说是chromium类浏览器扩展的数据保存位置
QQ图片20220727175536
再搜索"onetab data export", 又找到了一位前辈的开源方案, 基于Go.
jianyuan/onetab-export-to-json
原本以为到此结束, 走上CV的道路, 不过issue里有一条记录说失效了, 笔者也没有Go的debug经验, 看了下代码不到100行, 遂决定自己实现一遍.

2. 了解一下leveldb

google/leveldb

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

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

leveldb实例

  • 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

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

klausgao/electron-linvodb-manager

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

fastogt/fastonosql

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

软件截图
并且能清楚看到onetab所使用的键名.

  • 注: 此软件基于GPL v3 开源, 但二进制版本需要付费, 新用户可试用. 笔者写作此篇文章时官网炸了, 无法注册, 可下载旧版本运行, 无需注册登录.

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

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

  • 此处笔者最先想使用python实现, 但 python 的 level 依赖需要在 linux 下编译安装, 遂作罢.
$ npm install level
$ npm install mongodb
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

它实现了以下的功能

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

即将添加

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

仓库地址

endcloud/Onetab Re