99줄 코드로 Onetab 충돌 및 원본 데이터 백업 해결

· 47 조회

中文에서 AI 번역 · 원문 보기

이 글은 인터넷 아카이브 스냅샷에서 복원되었습니다. 일부 이미지는 유실되었을 수 있습니다.

서문

대학 시절, Chrome과 거의 같은 시기에 Onetab을 알게 되었습니다; 하지만 학생으로서 인터넷 서핑에는 별다른 부담이 없어 사용을 거의 하지 않았습니다.
취업 후에는 브라우저에 100개의 탭을 여는 것이 일상다반사가 되었고, 상사/동료가 자료를 묻는 일도 자주 발생했습니다. 이때 Onetab은 웹 페이지 기록장으로서 Ctrl+f로 키워드를 찾는 것과 함께 간단하고 사용하기 쉬웠습니다.

왜 즐겨찾기를 사용하지 않습니까?

필자가 즐겨찾기를 사용한 최종 결과는 북마크 바를 펼치면 화면 전체를 가리는 것이었습니다.

왜 방문 기록을 사용하지 않습니까?

방문 기록에는 다른 무관한 정보도 남아 있고, Chrome의 방문 기록은 신뢰할 수 없습니다.

왜 다른 지식 관리 도구를 사용하지 않습니까?

  1. 노트류 앱은 너무 무겁습니다
  2. onetab류 앱은 기존 사용 습관에 익숙해진 탓에 충분히 가볍고 더 효율적인 대체품을 찾지 못했습니다
  3. onetab은 다크 모드를 업데이트했고, 앞/뒤/현재 탭 캐시가 정말 실용적입니다

상황

닷새 전, Windows 업데이트 후 재부팅하고 edge를 다시 열었더니 onetab이 계속 loading 상태에 있었고, 얼마 후 페이지가 충돌하여 Out of Memory 오류가 발생했습니다.

  • 필자는 2020년 초 새로운 Edge가 출시된 때부터 onetab을 계속 사용했으며, 6만 개의 웹 페이지가 기록되어 있고 시스템을 재설치한 적이 없습니다.
이미지 「Crash」는 유실되었습니다: 원본 사이트와 인터넷 아카이브 모두에서 찾지 못했습니다.
/upload/2022/07/QQ%E6%88%AA%E5%9B%BE20220727131653.png

세 가지 시도

  1. 모든 웹 페이지와 확장 프로그램을 닫고 edge를 다시 시작한 뒤 onetab을 다시 열었습니다.
    실패했고, Out of Memory 오류가 발생했습니다.
  2. 컴퓨터를 재부팅하고 백그라운드가 비어 있는 상태에서 edge를 다시 시작한 뒤 onetab을 다시 열었습니다.
    실패했고, Out of Memory 오류가 발생했습니다.
  3. "C:\User" 폴더(Windows의 사용자 데이터 디렉터리)를 완전히 복사하여 새로 Windows10을 설치한 PC에 복원했습니다.
    실패했고, Out of Memory 오류가 발생했습니다.

이 시점에서 기본적으로 데이터 양이 너무 많아 확장 프로그램이 충돌한 것이라고 단정할 수 있었습니다.

  • 사실 이전에 5만 건의 데이터일 때도 여는 것이 매우 느려 30초 이상 걸렸습니다.
  • 다만 메모리 괴물 Chrome이 자신이 관리하는 메모리 사용량에도 제한을 두었을 줄은 몰랐습니다.

Chrome meme

목표

이제 문제는 찾았고, 다행히 데이터는 유실되지 않았습니다. 완전성은 알 수 없습니다.
2년 넘게 축적된 데이터는 일상 사용 빈도도 매우 높기 때문에 반드시 복원해야 합니다.

  1. 가능한 한 Onetab의 데이터를 복구하고, 기계와 사람이 인식할 수 있는 데이터 형식으로 내보냅니다
  2. 1.을 바탕으로 Onetab을 완전히 백업하는 자동화 도구를 구현합니다
    Onetab에 내장된 내보내기는 순수 txt 파일이라 수동으로 복사해야 하고, tab 그룹 생성 시간 필드가 빠져 있습니다
  3. Onetab 호환 App을 구현하여 데이터 검색 기능을 강화하고, 1.에서 내보낸 데이터 형식과 Ontab의 기존 데이터 형식에 호환되도록 합니다

조치

원인과 목표가 이미 있으므로 구현하는 일도 더 이상 무작정 헤매지 않아도 되었습니다.

1. Google에서 선행 사례 검색

"onetab data location"을 검색했습니다.
곧 onetab, 아니면 chromium 계열 브라우저 확장 프로그램의 데이터 저장 위치를 찾았습니다

이미지 「QQ图片20220727175536」은 유실되었습니다: 원본 사이트와 인터넷 아카이브 모두에서 찾지 못했습니다.
/upload/2022/07/QQ%E5%9B%BE%E7%89%8720220727175536.png

다시 "onetab data export"를 검색하자, 또 다른 선행 개발자의 오픈 소스 방안을 찾았습니다. Go 기반이었습니다.
jianyuan/onetab-export-to-json
원래는 여기서 끝나고 CV의 길로 들어서는 줄 알았지만, issue에 더 이상 작동하지 않는다는 기록이 있었고 필자도 Go 디버깅 경험이 없었습니다. 코드를 보니 100줄이 채 되지 않아 직접 한 번 구현하기로 했습니다.

2. leveldb 알아보기

google/leveldb

  • Google이 오픈 소스로 공개한 NoSQL 데이터베이스
  • 단일 스레드, 고성능, 로컬 인스턴스만 존재
  • 실행 과정에서 메모리를 대량으로 사용해 가속합니다

표준 levedb 데이터베이스 인스턴스는 폴더 형태입니다

이미지 「leveldb实例」는 유실되었습니다: 원본 사이트와 인터넷 아카이브 모두에서 찾지 못했습니다.
/upload/2022/07/QQ%E5%9B%BE%E7%89%8720220727190212.png
  • LOCK 데이터베이스 잠금
  • MANIFEST 헤더 파일
  • *.ldb 주 데이터 파일

오픈 소스 방안의 소개에서 알 수 있듯이, onetab은 leveldb 방안을 사용해 사용자 데이터를 저장합니다.
필자도 Chrome 확장 프로그램을 개발한 적이 있는데, 브라우저에서 실행되는 확장 프로그램은 파일 읽기/쓰기나 tcp 연결 같은 능력이 없기 때문에 localStorge와 indexDB 두 가지 API만 사용해 로컬화할 수 있었다는 기억이 납니다.
그중 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을 뒤졌습니다.

heapwolf/levelui

하나의 electron GUI 구현, 컴파일 실패

klausgao/electron-linvodb-manager

또 다른 electron GUI 구현, 데이터를 읽지 못함

fastogt/fastonosql

하나의 C++/Qt GUI 구현, 데이터 읽기 성공

이미지 「软件截图」는 유실되었습니다: 원본 사이트와 인터넷 아카이브 모두에서 찾지 못했습니다.
/upload/2022/07/QQ%E6%88%AA%E5%9B%BE20220727183055.png

또한 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()

결과

마지막으로 필자는 최근 매우 인기 있는 Tauri를 기반으로 바이너리 데스크톱 애플리케이션을 패키징했습니다.

아직 Tauri를 모르십니까?
여기를 보십시오 Electron을 버리고 Rust 기반으로 개발된 Tauri를 받아들이십시오

다음 기능을 구현했습니다

  • Onetab의 기존 스타일로 내보낸 모든 데이터를 조회합니다
  • 대량 데이터에 대응하기 위해 페이지네이션을 추가했습니다
  • 날짜 및 키워드 필터를 추가했습니다

곧 추가할 예정

  • rust로 자동 백업을 구현하여 별도로 node를 설치할 필요가 없습니다
  • 클라우드 동기화 webDAV 또는 기타
  • 데이터 분석, 워드 클라우드, NLP

저장소 주소

endcloud/Onetab Re

댓글