Skip to content

Backend less Cloud Sync

a.ito edited this page Jul 15, 2026 · 1 revision

Backend-less Cloud Sync(バックエンドレス・クラウド同期)

The Boosters の同期スキームの完全なロジック記述です。アプリはサーバを持たず、 ノートをローカルのプレーンファイルとして保存し、その保存フォルダを OS のクラウド 同期クライアントに任せる、という設計です。これは新規構想ではなく 既に実装・ 文書化済みの現行アーキテクチャで、docs/ONEDRIVE-DESKTOP-SETUP.md に OneDrive 向けの手順があり、設定画面(Storages タブ)からリンクされています。


1. 全体像

  Device A                          Device B
 ┌────────────┐                    ┌────────────┐
 │ The Boosters│                    │ The Boosters│
 │ (Electron)  │                    │ (Electron)  │
 └─────┬──────┘                    └──────┬─────┘
       │ read/write local files            │
       ▼                                    ▼
 ┌──────────────────────────────────────────────┐
 │  クラウド同期フォルダ内の storage/            │
 │   ├─ boostnote.json     (folders[], version) │
 │   ├─ notes/<key>.cson   (1 note = 1 file)    │
 │   └─ attachments/<noteKey>/...  (画像等)      │
 └──────────────────────────────────────────────┘
       ▲                                    ▲
       └──── OneDrive / Dropbox / iCloud ───┘
              Google Drive などが実ファイルを双方向同期

アプリの責務は「ローカルフォルダを読み書きする」ことだけ。ネットワーク通信・ 認証・サーバは一切ありません。同期の実体は OS のクラウドクライアントです。

補足: リポジトリ内で手順が明文化されているのは OneDrivedocs/ONEDRIVE-DESKTOP-SETUP.md)。 Dropbox / iCloud Drive / Google Drive も 同一の仕組み(任意のフォルダ同期 クライアント)で同じように動きますが、個別手順の明文化は OneDrive のみです。


2. ディスク上のデータモデル

2.1 ストレージ(フォルダ)

1 つの「ストレージ」= ディスク上の 1 フォルダ。フォルダのメタデータは <storagePath>/boostnote.json に入ります。

// <storagePath>/boostnote.json  (CSON で読み書き)
{
  "folders": [
    { "key": "", "name": "Default", "color": "#E10051" }
  ],
  "version": "1.0"
}
  • 参照実装: resolveStorageData.js(存在しなければ { folders: [], version: '1.0' } を自動生成)
  • 書き込みは _.pick(storage, ['folders','version']) なので、この 2 キーだけが保存される。

2.2 ノート(1 ノート = 1 ファイル)

各ノートは <storagePath>/notes/<key>.cson。serialize/parse は @rokt33r/season(CSON)。

# <storagePath>/notes/<uuid>.cson   (MARKDOWN_NOTE の例)
createdAt: '2026-07-16T00:00:00.000Z'
updatedAt: '2026-07-16T00:00:00.000Z'
type: 'MARKDOWN_NOTE'
folder: '<folderKey>'
title: 'First note'
tags: [ 'work', 'idea' ]
isStarred: false
isTrashed: false
content: '# First note\n\n本文…'
linesHighlighted: []
  • keystorage はファイルに書かれない。読み込み時に ファイル名<key>.cson)と 親ストレージから復元される(resolveStorageNotes.jsdata.key = basename(file,'.cson')data.storage = storage.key を注入)。生の .cson を開いても key が無いのはこのため。
  • SNIPPET_NOTE の場合は content/linesHighlighted の代わりに descriptionsnippets[](各 { name, mode, content, linesHighlighted })を持つ。
  • キー生成: ノートは keygen(true) = uuid v4、ストレージは keygen() = 10 バイト hex。

2.3 添付ファイル

<storagePath>/attachments/<noteKey>/<filename>。本文中では 相対プレースホルダ :storage/<noteKey>/<filename> で参照し、描画時にのみ実 file://…/attachments/… に 書き換える(attachmentManagement.jsfixLocalURLS)。

重要: 参照が相対プレースホルダなので、フォルダごと移動・同期してもリンクは 壊れません。逆に言うと、同期は notes/attachments/boostnote.json3 点セットで運ぶ必要があります。


3. 読み書きフロー

3.1 起動時ロード(ディスク → メモリ、1 回だけ)

Main.js: dataApi.init()  ──▶  INIT_ALL { storages, notes } を dispatch
   ├─ fetchStorages(): localStorage['storages'] を読む
   │     └─ fs.existsSync(path) が false のストレージは除外(!)
   ├─ resolveStorageData(): 各 boostnote.json から folders/version を読む
   └─ fetchNotes(): resolveStorageNotes() が notes/*.cson を全読み込み
  • dataApi.init() の呼び出しは 起動時の 1 回のみMain.js)。
  • 以降、Redux ストアは アプリ自身の書き込みの戻り値でのみ更新される。 セッション中に notes ディレクトリを再スキャンしない。

3.2 書き込み(メモリ → ディスク)

  • createNote: 衝突しない uuid を選び、_.omit(noteData, ['key','storage']) を CSON で書く。
  • updateNote: 既存 .cson を読み、Object.assign(…, input, { updatedAt: new Date() }) で上書き。
  • deleteNote: sander.unlinkSync で削除 → attachments/<noteKey>/ も削除。

いずれも書き込み後、戻り値のノートオブジェクトで UPDATE_NOTE 等を dispatch し、 UI に反映する。ディスクを再読込しないのがポイント。


4. 何が同期され、何が同期されないか

データ 場所 同期される?
ノート本文+メタ <storagePath>/notes/*.cson ✅(クラウドフォルダ内なら)
フォルダ定義・version <storagePath>/boostnote.json
添付ファイル <storagePath>/attachments/**
ストレージ一覧(開く場所) localStorage['storages'] ❌ マシンローカル
アプリ設定・テーマ・ホットキー・AI キー localStorage['config'] + ~/.boostnoterc ❌ マシンローカル

帰結: 新デバイスでは、クラウド同期フォルダ内の storage を **一度手動で「ストレージを 追加」**する必要がある(同期されるのはフォルダの中身であって「どのフォルダを開くか」 の設定ではない)。テーマ・ホットキー・AI キーも各デバイスで個別設定になる。


5. 競合と限界(設計上の割り切り)

このスキームには アプリ内の競合解決が存在しません。意図的な割り切りです。

  • ライブ監視なし: notes ディレクトリのファイル変更を監視する watcher は無い (唯一の fs.watchFile は dev ビルドバンドル用で、ノートとは無関係)。 → 他デバイスがクラウド経由で書き込んでも、アプリ再起動まで反映されない
  • 競合解決なし: updatedAt 比較やマージは行わない。2 台でオフライン同時編集すると、 クラウドクライアント側がファイル単位の競合コピー(例: name-PC名.cson)を作る。 ユーザーが手動でどちらを残すか決める。
  • Last-Write-Wins のサイレント損失の可能性(オンライン同時上書き時)。
  • 設定は非同期(§4)。

実務上の推奨運用

  1. 1 台での編集を終えてから、クラウド同期の完了を待って別デバイスを開く。
  2. 別デバイスで編集する前に アプリを再起動して最新をロードする。
  3. 競合コピーができたら手動で解消する(片方を残す)。
  4. ストレージフォルダは常に存在させる(起動時 fs.existsSync で消えたパスは一覧から除外される)。

6. なぜこの設計か(トレードオフ)

得られるもの 代償
サーバ運用ゼロ・コストゼロ・認証情報ゼロ リアルタイム同時編集は不可
プレーンファイルなので Git 連携・grep・バックアップが容易 競合解決は手動
プレーンテキストが自分の管理下から出ない(プライバシー) セッション中の外部変更を自動反映しない
実装が最小(クラウド同期は OS に委譲) 新デバイス登録・設定は手動

将来リアルタイム多デバイス編集が必須要件になった場合は、Yjs CRDT + self-hosted Hocuspocus への移行が別トラックで検討されている(docs/MODERNIZATION-2026-stack-selection.md)。 現行のフォルダ同期はそのベースライン。


7. モダン化時の落とし穴(実装者向け)

  • ESM/CJS × Vite: deleteNote.js / moveNote.js / deleteFolder.js は必ず export default を使う。import を含むファイルに module.exports = を書くと、 Vite ビルドで 黙って空モジュールになる(jest/ava は緑のまま)。CommonJS に 「整理」してはいけない。
  • key/storage はファイルに無い(§2.2)。ファイル名と親ストレージから復元される。
  • 起動時の存在チェック: 一時的に未マウント / クラウドで「容量解放」されたパスは 起動時に一覧から落ちる。フォルダ自体は常に存在させる。

関連ページ: 技術スタックHome リポジトリ内資料: docs/ONEDRIVE-DESKTOP-SETUP.md, docs/MODERNIZATION-2026-stack-selection.md

Clone this wiki locally