99行のコードで Onetab のクラッシュと元データのバックアップを解決
中文からAI翻訳 · 原文を見る
この記事はインターネットアーカイブのスナップショットから復元しました。一部の画像は失われている可能性があります。
はじめに
大学時代、Chrome とほぼ同時に Onetab を知りました。しかし学生としてネットサーフィンにはあまり負担がなく、ほとんど使っていませんでした。
仕事を始めてからは、ブラウザで 100 個のタブを開くのが日常茶飯事になり、上司や同僚から資料について聞かれることも頻繁に起こるようになりました。そんなとき、Onetab はウェブページの記録帳として、Ctrl+f でキーワードを検索でき、簡単で使いやすいものです。
なぜブックマークを使わないのか?
筆者がブックマークを使った最終的な結果は、ブックマークバーを展開すると画面全体を覆ってしまうことでした。
なぜ履歴を使わないのか?
履歴には他にも無関係な情報が含まれますし、そもそも Chrome の履歴は信頼できません。
なぜ他の知識管理ツールを使わないのか?
- ノート系のアプリは重すぎます
- onetab に似たアプリは、先入観による使用習慣のため、十分に軽量でより効率的な代替品を見つけられませんでした
- onetab はダークモードが追加され、さらに前方/後方/現在のタブのキャッシュが本当に実用的です
状況
5 日前、Windows の更新による再起動後に Edge を開き直すと、Onetab はずっと loading 状態のままで、しばらくするとページがクラッシュし、Out of Memory エラーが報告されました。
- 筆者は 2020 年初頭に新しい Edge が登場して以来、継続して Onetab を使用しており、6 万件のウェブページが記録されていて、システムを再インストールしたことはありません。
/upload/2022/07/QQ%E6%88%AA%E5%9B%BE20220727131653.png三つの手段
- すべてのウェブページと拡張機能を閉じ、Edge を再起動し、Onetab を開き直す。
失敗し、Out of Memory エラーが報告される。 - コンピュータを再起動し、バックグラウンドが空の状態で Edge を再起動し、Onetab を開き直す。
失敗し、Out of Memory エラーが報告される。 - 「C:\User」フォルダ (Windows のユーザーデータディレクトリ) を完全にコピーし、新規に Windows10 をインストールした PC に復元する。
失敗し、Out of Memory エラーが報告される。
この時点で、ほぼデータ量が多すぎることが原因で拡張機能がクラッシュしたと断定できます。
- 実は以前、5 万件のデータがあったときから開くのがかなり遅く、30 秒以上かかっていました。
- ただ、メモリ消費の激しい Chrome が、自らの配下のメモリ使用量にも制限をかけているとは思いませんでした。

目標
これで問題は見つかりました。幸いなことにデータは失われていませんでしたが、完全性は不明です。
2 年以上蓄積されたデータは日常的な使用頻度も高いため、必ず復元しなければなりません。
- Onetab のデータを可能な限り復元し、機械と人の両方が認識できるデータ形式でエクスポートする
-
- を基に、Onetab を完全にバックアップする自動化ツールを実装する
Onetab 標準のエクスポートは純粋な txt ファイルで、手動でコピーする必要があり、さらにタブグループの作成時間のフィールドが欠けている
- を基に、Onetab を完全にバックアップする自動化ツールを実装する
- Onetab 互換の App を実装し、データ検索機能を強化し、1. でエクスポートしたデータ形式と Onetab 本来のデータ形式に互換性を持たせる
対応
原因と目標がすでに分かったので、実装も当てもなく迷うことはなくなりました。
1\. 先人の経験を Google で検索
「onetab data location」を検索する
すぐに onetab、あるいは chromium 系ブラウザ拡張機能のデータ保存場所を見つけました
/upload/2022/07/QQ%E5%9B%BE%E7%89%8720220727175536.pngさらに「onetab data export」を検索すると、Go ベースの先人のオープンソースのソリューションも見つかりました。
これで終わりだと思い、CV の道を歩むつもりでしたが、issue に無効になったという記録が一つあり、筆者には Go のデバッグ経験もありませんでした。コードを見ると 100 行未満だったため、自分で実装してみることにしました。
2\. leveldb について知る
- Google によってオープンソース化された NoSQL データベース
- シングルスレッドで高性能、ローカルインスタンスのみ
- 実行中にメモリを大量に使って高速化する
標準的な leveldb データベースインスタンスはフォルダの形式をとる
/upload/2022/07/QQ%E5%9B%BE%E7%89%8720220727190212.png- LOCK データベースロック
- MANIFEST ヘッダーファイル
- \*.ldb メインデータファイル
オープンソースのソリューションの紹介から、onetab が leveldb のソリューションでユーザーデータを保存していることが分かります。
筆者自身も Chrome の拡張機能を開発したことがあり、ローカル化には localStorge と indexDB の 2 つの API しか使えないと記憶しています。なぜなら、ブラウザ上で動作する拡張機能にはファイルの読み書きや tcp 接続といった能力がないからです。
そのうち localStorge は最大 5M で、5 万件の比較的長い JSON レコードはどうしても保存できません。onetab は indexDB を使ってデータを保存しているはずなのに、この leveldb とは何なのか、しかも貢献者も Google です。
検索してみると、leveldb は indexDB の下位層の実装であり、indexDB は実は上位層の API であることが分かりました (名前が誤解を招くシリーズ)。
Google Chrome は Local Storage の一部のデータを SQLite 形式(.localstorage ファイル)でフォルダに保存します。
IndexedDB フォルダに保存されるその他のデータ(各プロファイルごと)は LevelDB 形式を採用しています。これは Google によって開発されたオープンソースのキーバリューストア形式で、GitHub 上でホストされています。
Chrome の外部で LevelDB 形式のファイルを変更するのは簡単なプロセスではありません。なぜなら、Chrome の Indexed DB leveldb インスタンスを確認するために互換性のあるコンパレータを実装する必要があるからです。
3\. leveldb の GUI ツール
経緯を理解した後、まず~確(tou)認(lan)~データ状況を確認する目的で、筆者はまず leveldb の GUI ツールを探しました。
主流の Navicat と DataGrip が対応していないのは間違いないので、引き続き GitHub をサーフィンしました。
ある electron の GUI 実装、コンパイル失敗
別の electron の GUI 実装、データを読み取れない
ある C++/Qt の GUI 実装、データの読み取りに成功
/upload/2022/07/QQ%E6%88%AA%E5%9B%BE20220727183055.pngそして、onetab が使用しているキー名をはっきりと確認できます。
- 注: このソフトウェアは GPL v3 に基づいてオープンソース化されていますが、バイナリ版は有料で、新規ユーザーは試用できます。筆者がこの記事を書いている時点では公式サイトがダウンしており、登録できませんでした。旧バージョンをダウンロードして実行できます。登録やログインは不要です。
4\. コード実装 node.js 16.15 ベース
先の 2 つの 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()
結果
最後に、筆者は最近とても人気のある Tauri を基に、バイナリのデスクトップアプリをパッケージ化しました。
Tauri をまだ知らない?
こちらを見てください Electron を捨てて、Rust ベースの Tauri を受け入れよう
実装した機能
- Onetab 本来のスタイルで、エクスポートしたすべてのデータを表示する
- 大量データに対応するためページネーションを追加した
- 日付とキーワードのフィルタリングを追加した
今後追加予定
- rust による自動バックアップの実装、node を追加インストールする必要なし
- クラウド同期 webDAV またはその他
- データ分析 ワードクラウド NLP