Skip to content

Add Evernote-to-Notion migration CLI with Thrift binary protocol - #10

Open
pollenjp wants to merge 7 commits into
mainfrom
claude/evernote-to-notion-migration-AX2CV
Open

Add Evernote-to-Notion migration CLI with Thrift binary protocol#10
pollenjp wants to merge 7 commits into
mainfrom
claude/evernote-to-notion-migration-AX2CV

Conversation

@pollenjp

@pollenjp pollenjp commented May 2, 2026

Copy link
Copy Markdown
Member

Summary

This PR introduces a complete Evernote-to-Notion migration tool written in Rust. The implementation includes a custom Apache Thrift Binary Protocol implementation to communicate with Evernote's API, ENML-to-Notion block conversion, and rate-limited API clients for both services.

Key Changes

  • Custom Thrift Binary Protocol Implementation (src/thrift/)

    • Minimal BinaryProtocolWriter and BinaryProtocolReader using bytes::{Buf, BufMut}
    • Supports message framing, struct/field serialization, and exception handling
    • HttpTransport for POST-based Thrift calls over HTTPS
  • Evernote Client (src/evernote/)

    • Direct API calls to UserStore and NoteStore without external SDK
    • Methods: getUser(), findNotesMetadata(), getNote(), getResource()
    • Exception handling for EDAMUserException, EDAMSystemException, EDAMNotFoundException
    • Rate limit handling with configurable backoff
  • Notion Client (src/notion/)

    • Database query for duplicate detection via Evernote URL property
    • Page creation with rich text blocks
    • Two-step file upload API integration
  • ENML Converter (src/enml/)

    • Converts Evernote's ENML (HTML subset) to intermediate ConvertedBlock representation
    • Supports headings, paragraphs, lists, code blocks, and media references
    • Media references resolved to Notion file uploads via hash mapping
  • Migrator (src/migrator.rs)

    • Orchestrates the migration workflow
    • Idempotent via Evernote URL tracking in Notion
    • Batch processing with configurable limits and dry-run mode
  • Rate Limiting (src/rate_limit.rs)

    • Token bucket implementation for concurrent API calls
    • Respects Evernote's rateLimitDuration from system exceptions
  • CLI & Configuration

    • main.rs with migrate and dry-run subcommands
    • Environment-based configuration for API tokens and endpoints
    • Structured logging with tracing
  • Build & Testing

    • Cargo.toml with dependencies: tokio, reqwest, serde, quick-xml, thiserror
    • mise.toml for task automation (build, test, lint, fmt)
    • GitHub Actions CI workflow
    • Integration test skeleton with wiremock support
  • Documentation

    • ADR-01 documenting architectural decisions
    • Agent log detailing implementation rationale
    • README with architecture diagram and usage instructions

Notable Implementation Details

  • No external Thrift crate dependency; protocol implemented from scratch to minimize dependencies and maintain control over error handling
  • Evernote exceptions decoded from Thrift result struct fields (IDs 1-3) following standard Thrift conventions
  • ENML parsing uses quick-xml for robustness against malformed input
  • Notion file uploads use multipart form data with signed URLs
  • Concurrent rate limiting via shared TokenBucket across Evernote (2 RPS) and Notion (3 RPS) clients

https://claude.ai/code/session_014DFRXwuWKj8x5HVGUSf71F

claude added 7 commits May 2, 2026 04:31
- Cargo / mise / rust-toolchain / .gitignore で Rust 1.88 + mise tasks (build/test/lint/fmt/check) を整備
- 環境変数を集約する Config と、Notion/Evernote 用に共通利用するトークンバケット式 TokenBucket を追加
- TokenBucket は tokio::sync::Mutex で共有 + acquire().await でレート上限を尊重する
…/UserStore クライアントを追加

- src/thrift/protocol.rs に Apache Thrift Binary Protocol (Strict version) の最小 Writer/Reader を実装
  - i32/i64/string/binary/struct/list と message/field begin/end をサポート
  - 未知フィールドを安全に読み飛ばす skip(ty) を提供
- src/thrift/transport.rs で application/x-thrift POST 用の薄い HTTP transport を提供
- src/evernote/client.rs で getUser / findNotesMetadata / getNote / getResource を実装
  - Thrift result struct のフィールド ID 0 を成功値、1..=3 を EDAMUserException / EDAMSystemException / EDAMNotFoundException としてマッピング
  - rateLimitDuration を EvernoteError::System に保持し handle_rate_limit で sleep 待機
  - TokenBucket を call ごとに acquire().await
- src/evernote/url.rs で evernote:///view/<userId>/s<shard>/<guid>/<guid> 形式を生成
- 各モジュールに wiremock + 自前リプライバイト列を使った単体テスト
- src/enml/converter.rs に quick-xml ベースの ENML → ConvertedBlock 変換を実装
  - p / h1-h6 / ul / ol / li / pre / code / en-media (hash 参照) を扱う
  - <en-media> はハッシュごと MediaRef として残し、後段で file_upload に解決させる
- src/notion/blocks.rs で ConvertedBlock を Notion ブロック JSON に変換
  - MediaResolver trait + HashMapResolver でハッシュ → file_upload id を解決
  - MIME ヒントから image / video / audio / pdf / file を選択
- src/notion/client.rs に Notion REST クライアントを実装
  - find_page_by_evernote_url で Evernote URL プロパティの一致検索 (冪等性の根拠)
  - create_page は children を 100 件にチャンクし、超過分を append_block_children で送信
  - すべての API 呼び出しで TokenBucket を acquire
- src/notion/upload.rs で file_uploads → upload_url multipart の 2 段階アップロードを実装
- 各モジュールに wiremock を使った単体テスト
- src/migrator.rs に Evernote → Notion の同期オーケストレーションを実装
  - getUser → findNotesMetadata (offset でページング) → getNote → 添付 file_upload → create_page
  - find_page_by_evernote_url の結果でスキップして冪等にする
  - rateLimitDuration エラー時は EvernoteClient::handle_rate_limit で sleep 後にリトライ
- src/main.rs で clap subcommand `migrate` / `dry-run` を提供
  - tracing-subscriber + EnvFilter でログ初期化
  - Notion 用に capacity=3 / refill=3 RPS、Evernote 用に 2 RPS のトークンバケットをデフォルト化
- tests/integration.rs を追加し、公開サーフェスがリンクすることを E2E で確認
…s ワークフローを追加

- .github/workflows/evernote-to-notion-2026-05-02-ci.yml で mise run fmt:check / lint / test を CI 化
- README.md に概要 (mermaid 図含む) と mise タスク一覧、利用手順を記載
- adr/2026-05/..._01_evernote-to-notion-architecture.md でアーキ決定事項 (Thrift 自前実装 / Developer Token / file_upload / レートリミタ / 冪等性) を記録
- agent-logs/2026-05/..._01_evernote-to-notion-bootstrap.md に今回の経緯と詰まり所を記録
- Cargo.lock を含めることで CI を再現可能にする
…directory を渡す

CI の check ジョブが exit 1 で失敗していた原因は、リポジトリ直下に mise.toml が無いため jdx/mise-action が rust 1.88 を install せず、後続の `mise run fmt:check` で cargo が見つからないこと。

- jdx/mise-action@v2 の `working_directory` を `evernote-to-notion-2026-05-02` に指定し、project 内の mise.toml に基づいて install を実行
- 不要な `experimental: true` を削除 (jdx/mise-action@v2 では未定義の input)
- 切り分け用に `mise --version` / `mise ls` / `rustc --version` / `cargo --version` を表示する step を追加
- cache key に Cargo.lock も含めて再現性を強化
mise.toml の `[tools] rust = "1.88.0"` を経由した install が CI 上で安定せず check ジョブが失敗していた。Rust toolchain は dtolnay/rust-toolchain で確実に install し、mise はタスクランナーとしてのみ使う構成に変更する。

- dtolnay/rust-toolchain@master で 1.88.0 + rustfmt + clippy を install
- jdx/mise-action は install:false / cache:false でタスクランナーのみ提供
- 切り分け step に cargo fmt --version / cargo clippy --version を追加
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants