Add Evernote-to-Notion migration CLI with Thrift binary protocol - #10
Open
pollenjp wants to merge 7 commits into
Open
Add Evernote-to-Notion migration CLI with Thrift binary protocol#10pollenjp wants to merge 7 commits into
pollenjp wants to merge 7 commits into
Conversation
- 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 を追加
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/)BinaryProtocolWriterandBinaryProtocolReaderusingbytes::{Buf, BufMut}HttpTransportfor POST-based Thrift calls over HTTPSEvernote Client (
src/evernote/)getUser(),findNotesMetadata(),getNote(),getResource()EDAMUserException,EDAMSystemException,EDAMNotFoundExceptionNotion Client (
src/notion/)ENML Converter (
src/enml/)ConvertedBlockrepresentationMigrator (
src/migrator.rs)Rate Limiting (
src/rate_limit.rs)rateLimitDurationfrom system exceptionsCLI & Configuration
main.rswithmigrateanddry-runsubcommandstracingBuild & Testing
Cargo.tomlwith dependencies: tokio, reqwest, serde, quick-xml, thiserrormise.tomlfor task automation (build, test, lint, fmt)Documentation
Notable Implementation Details
quick-xmlfor robustness against malformed inputTokenBucketacross Evernote (2 RPS) and Notion (3 RPS) clientshttps://claude.ai/code/session_014DFRXwuWKj8x5HVGUSf71F