fix(i18n): overhaul Japanese translations, localise pending UI labels, and update documentation

- Overhaul Japanese translation catalogue and synchronize with current documentation
- Localise pending UI labels across dialogues, panels, and settings
- Add Japanese translation mapping for provisional LiveSync messages
- Update and refine all Japanese documentation files
- Strip weak ETag prefix in ObsHttpHandler for object storage compatibility
This commit is contained in:
Chihiro WATANABE committed 2026-11-04 10:38:55 +09:00
1 parent 0fe7d82f46
commit 5ffa3b76ed
32 files changed
+6049 -1444

No files matched your search

+95 -56
View File
@@ -1,86 +1,125 @@
<!-- For translation: 20240227r0 -->
# Self-hosted LiveSync
[英語版ドキュメント](./README.md) - [中国語版ドキュメント](./README_cn.md).
Obsidianで利用可能なすべてのプラットフォームで使える、CouchDBをサーバに使用する、コミュニティ版の同期プラグイン
Self-hosted LiveSync は、Obsidian 対応のすべてのプラットフォームで利用できる、コミュニティ開発の同期プラグインです。CouchDB や MinIO、S3、R2 などのオブジェクトストレージを利用し、信頼性の高いデータ同期を実現します。
WebRTC によるピアツーピア(P2P)同期にも対応しており、中央のデータ保存サーバーを介さずにデバイス間でノートを交換できます。ピアの検出にはシグナリングリレーが必要です。詳しくは[ピアツーピア同期の仕組み](./docs/p2p.md)をご覧ください。
セットアップガイド、トラブルシューティング、技術資料、設計判断、過去の資料は[ドキュメント一覧](docs/README.md)から確認できます。
![obsidian_live_sync_demo](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif)
※公式のSyncと同期することはできません。
>[!IMPORTANT]
> このプラグインは公式の「Obsidian Sync」と互換性がなく、同サービスとは同期できません。
## 機能
- 高効率・低トラフィックでVault同士を同期
- 競合解決がいい感じ
- 単純な競合なら自動マージします
- OSSソリューションを同期サーバに使用
- 互換ソリューションも使用可能です
- End-to-End暗号化実装済み
- 設定・スニペット・テーマ、プラグインの同期が可能
- [Webクリッパー](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf) もあります
NDAや類似の契約や義務、倫理を守る必要のある、研究者、設計者、開発者のような方に特にオススメです。
- Vault を効率的に、通信量を抑えて同期します。
- 競合する変更を適切に処理します。
- 単純な競合は自動的にマージします。
- サーバーにはオープンソースのソリューションを使用します。
- 互換性のあるソリューションにも対応します。
- エンドツーエンド暗号化に対応します。
- [カスタマイズ同期(高度な設定)](docs/settings_ja.md#6-customisation-sync-advanced)または[非表示ファイル同期](docs/tips/hidden-file-sync.md)で、設定、スニペット、テーマ、プラグインを同期します。
- WebRTC によるピアツーピア同期を行います。
このプラグインは、セキュリティ上の理由からノートを完全に自己管理する必要がある研究者、エンジニア、開発者に特に適しています。ノートが完全にプライベートなままであることに安心感を求める方にも適しています。
>[!IMPORTANT]
> - インストール・アップデート前には必ずVaultをバックアップしてください
> - 複数の同期ソリューションを同時に有効にしないでください(これはiCloudや公式のSyncも含みます)
> - このプラグインは同期プラグインです。バックアップとして使用しないでください
> - このプラグインをインストールまたはアップグレードする前に、Vault をバックアップしてください。
> - iCloud や Obsidian Sync など、同じ Vault に書き込む他の同期ソリューションと同時に有効にしないでください。
> - バックアップには、[Differential ZIP Backup](https://github.com/vrtmrz/diffzip) も利用できます。
## このプラグインの使い方
### 3分セットアップ - CouchDB on fly.io
セルフホストでは、信頼できるサーバーを選び、自分で管理できます。サーバーをセットアップして保守し、安全な状態を保ってください。
**はじめての方におすすめ**
<a id="setup-workflows"></a>
[![LiveSync Setup onto Fly.io SpeedRun 2024 using Google Colab](https://img.youtube.com/vi/7sa_I1832Xc/0.jpg)](https://www.youtube.com/watch?v=7sa_I1832Xc)
### 必要なもの
1. [Fly.ioにCouchDBをセットアップする](docs/setup_flyio.md)
2. [Quick Setup](docs/quick_setup_ja.md)でプラグインを設定する
| 方法 | 必要なもの | あると便利なもの |
| --- | --- | --- |
| CouchDB | CouchDB 互換サーバー | — |
| オブジェクトストレージ | S3 互換サービスまたはサーバー | — |
| ピアツーピア | 不要 | 独自のシグナリングサーバー |
CouchDB 互換サーバーを使う場合は、環境に合ったセットアップガイドを選んでください。
### Manually Setup
- [自分のサーバー](docs/setup_own_server_ja.md): Raspberry Pi など、自分で管理するサーバーで CouchDB を実行します。
- [Docker](docker/README.md): 付属の Docker スタックで CouchDB をデプロイします。
- [Fly.io](docs/setup_flyio.md): ホスト型インフラ上で自分の CouchDB インスタンスを管理します。
- [IBM Cloudant(マネージド)](docs/setup_cloudant_ja.md): IBM が管理するデータベースサービスを利用します。
1. サーバのセットアップ
1. [Fly.ioにCouchDBをセットアップする](docs/setup_flyio.md)
2. [CouchDBをセットアップする](docs/setup_own_server_ja.md)
2. [Quick Setup](docs/quick_setup_ja.md)でプラグインを設定する
各ガイドでセットアップ方法とプロバイダー固有の費用を確認してください。
> [!TIP]
> IBM Cloudantもまだ使用できますが、いくつかの理由で現在はおすすめしていません。[IBM Cloudantのセットアップ](docs/setup_cloudant_ja.md)はまだあります。
<a id="3-minute-setup---couchdb-on-flyio"></a>
Fly.io の動画による手順は、[自動セットアップ](docs/setup_flyio.md#a-very-automated-setup)を参照してください。
P2P では中央のデータ保存サーバーは不要ですが、ピアの検出にはシグナリングリレーが必要です。プロジェクトの公開シグナリングリレーを利用できるため、サーバーを用意する必要はありません。別のデバイスが同期している間は、必要なデータを持つデバイスを少なくとも1台オンラインにしてください。[P2P の通信モデル](./docs/p2p.md)を確認してから、[ピアツーピアのセットアップ](./docs/setup_p2p.md)に進んでください。
デバイス上で P2P のセットアップと接続がどのように動作するかを確認したい場合は、実験的な [WebPeer ブラウザーユーティリティ](https://vrtmrz.github.io/obsidian-livesync/webpeer/)のガイド付き接続チェックを試してください。空の Vault を使用し、ノートの同期はテストしません。範囲と制限は [WebPeer のドキュメント](src/apps/webpeer/README.md)を参照してください。
### 同期を開始する
1. 最初のデバイスで、CouchDB の[クイックセットアップ](docs/quick_setup_ja.md)、[オブジェクトストレージのセットアップ](docs/setup_object_storage.md)、または[ピアツーピアのセットアップ](docs/setup_p2p.md)を使用して Self-hosted LiveSync をインストールおよび設定します。
2. 動作中のデバイスからセットアップ URI を生成し、同じガイドに従って他のデバイスを追加します。
3. テスト用ノートを作成し、双方向に同期されることを確認します。
<a id="third-party-managed-couchdb-hosting"></a>
### コミュニティが紹介するホスティングサービス
> [!NOTE]
> 以下のサービスは第三者によって運営されています。Self-hosted LiveSync の公式サービスではなく、本プロジェクトが推奨または保証するものでもありません。表内の掲載は各プロバイダーから提案されたものです。
自分でサーバーをセットアップして保守したくない場合は、以下のプロバイダーが、このプラグインから接続できるマネージド CouchDB サーバーを提供しています。
| プロバイダー | 提供内容 |
| --- | --- |
| [Zenith Hosting](https://zenith.hosting/host/obsidian-livesync) | マネージド CouchDB ホスティング。 |
同期データはプロバイダーが運営するサーバーに保存されます。Vault にとって許容できるか検討し、利用前にプロバイダーの最新の利用規約、料金、データ取扱方針を確認してください。
## ステータスバーの説明
同期ステータスはステータスバーに、下記のアイコンとともに表示されます
同期ステータスは、デフォルトでステータスバーとエディタ内に表示されます。主な表示は以下のとおりです。
- アクティビティー
- 📲 ネットワーク接続中
- 同期ステータス
- ⏹️ 停止中
- 💤 変更待ち(LiveSync中)
- ⚡️ 同期の進行中
- ⚠ エラー
- 統計情報
- ↑ アップロードしたチャンクとメタデータ数
- ↓ ダウンロードしたチャンクとメタデータ数
- 進捗情報
- 📥 転送後、未処理の項目数
- 📄 稼働中データベース操作数
- 💾 稼働中のストレージ書き込み数操作数
- ⏳ 稼働中のストレージ読み込み数操作数
- 🛫 待機中のストレージ読み込み数操作数
- ⚙️ 隠しファイルの操作数(待機・稼働中合計)
- 🧩 取得待ちを行っているチャンク数
- 🔌 設定同期関連の操作数
| 表示 | 意味 |
| --- | --- |
| ⏹️ | 停止中。 |
| 🌀 | 同期を開始しています。 |
| 💤 | LiveSync モードが変更を待っています。 |
| ⚡️ | 同期中です。 |
| ⏩ | CouchDB の変更履歴を比較しています(ローカルに保持済みの項目も含みます)。 |
| ⚠ | エラーが発生しました。 |
| ↑ / ↓ | アップロード/ダウンロードしたチャンクとメタデータ。 |
| 📲 / 🌐N | 個別のリモート操作を実行中/概算のリモートリクエスト数。 |
データベースやファイルの破損を避けるため、Obsidianの終了は進捗情報が表示されなくなるまで待ってください(プラグインも復帰を試みますが)。特にファイルを削除やリネームした場合は気をつけてください。
<a id="customising-status-items-with-css-snippets"></a>
詳細な進捗カウンターと [CSS のカスタマイズ](docs/tips/status-display.md#customising-status-items-with-css-snippets)については、[ステータス表示ガイド](docs/tips/status-display.md)を参照してください。
## Tips and Troubleshooting
- 2台目以降のセットアップ時に、初期同期をより迅速かつ簡単に行うには、[ファストセットアップガイド](docs/tips/fast-setup_ja.md)をご参照ください。
- 何かこまったら、[Tips and Troubleshooting](docs/troubleshooting.md)をご参照ください。
Obsidian を閉じる前に、保留中のファイル操作が完了するまで待ってください。特にファイルの削除や名前変更の後は注意してください。
## License
## ヒントとトラブルシューティング
- 後続のデバイスをセットアップする際に、より高速で簡単な初期複製を行う場合は、[Fast Setup ガイド](docs/tips/fast-setup_ja.md)を参照してください。
- 通常のノート同期が動作することを確認してから、[非表示ファイル同期](docs/tips/hidden-file-sync.md)を設定してください。
- Obsidian または LiveSync が正常に起動できない場合は、リモートデータベースを変更またはリセットする前に、[復旧とフラグファイル](docs/recovery.md)を確認してください。
- Self-hosted LiveSync 1.0 には Obsidian 1.7.2 以降が必要です。旧バージョンの Obsidian で 1.0 を使用する必要がある場合は、バージョン、プラットフォーム、およびそのバージョンにとどまる理由を添えて [Issue を作成](https://github.com/vrtmrz/obsidian-livesync/issues/new?template=issue-report.md)してください。サポートの拡張が可能かどうかを検討します。標準のコミュニティプラグインインストーラーでは、互換性のある古いプラグインリリースが選択されます。
- プラグインが動作しない場合は、[トラブルシューティング](docs/troubleshooting.md)を参照してください。
Licensed under the MIT License.
## 謝辞
本プロジェクトの継続的な発展は、以下の方々の支援によって支えられています。
- 多くの [コントリビューター](https://github.com/vrtmrz/obsidian-livesync/graphs/contributors)の皆様。
- 多くの [GitHub スポンサー](https://github.com/sponsors/vrtmrz#sponsors)の皆様。
- JetBrains コミュニティプログラム / オープンソースプロジェクト支援 <img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" alt="JetBrains logo" height="24">
貢献してくださった方々のご厚意とご支援に、深く感謝します。
## 開発ガイド
開発環境のセットアップ、テスト基盤、コード規約などについては、[開発ガイド](devs.md)を参照してください。
## ライセンス
MIT License の下で公開されています。
+105 -58
View File
@@ -1,93 +1,140 @@
# Quick setup
このプラグインには、いろいろな状況に対応するための非常に多くの設定オプションがあります。しかし、実際に使用する設定項目はそれほど多くはありません。そこで、初期設定を簡略化するために、「セットアップウィザード」を実装しています。
※なお、次のデバイスからは、`現在の設定をセットアップURIにコピー`と`セットアップURIで接続`を使ってセットアップしてください。
# クイックセットアップ
[英語版](./quick_setup.md) - [中国語版](./quick_setup_cn.md)
## Wizardの使い方
`🧙‍♂️ Setup wizard` から開きます。もしセットアップされていなかったり、同期設定が何も有効になっていない場合はデフォルトで開いています。
このガイドでは、最初のデバイスで通常のノート同期を開始し、その後、別のデバイスを追加します。オプション機能は、この基本的な同期が動作することを確認してから設定してください。
![](../images/quick_setup_1.png)
開始前に、次の準備をしてください。
### Discard the existing configuration and set up
今設定されている内容をいったん全部消してから、ウィザードを始めます。
- 対象となるすべての Vault をバックアップする。
- Obsidian Sync、iCloud 同期、および同じ Vault に書き込む他のサービスを無効にする。
- リモートサービスとセットアップ URI を用意する。
- セットアップ URI とそのパスフレーズを別々に保管する。
### Do not discard the existing configuration and set up
今の設定を消さずにウィザードを開始します。
たとえ設定されていたとしても、ウィザードモードではすべての設定を見ることができません。
この手順では、推奨されるプロビジョニング済み CouchDB の手順を説明します。サーバーとセットアップ URI の準備方法は[こちら](./setup_own_server_ja.md)を参照してください。
いずれかのNextを押すと、設定画面がウィザードモードになります。
## セットアップ URI の内容
### Wizardモード
セットアップ URI は `obsidian://setuplivesync?settings=` で始まります。認証情報を含む接続設定が暗号化されて格納されていますが、暗号化されていても URI 自体を安全に保管してください。
![](../images/quick_setup_2.png)
セットアップ URI 用のパスフレーズは URI を復号するためのものです。同期データを保護する Vault 暗号化パスフレーズとは別のものです。両方を安全に保管し、URI とそのパスフレーズを同じ経路で送らないでください。
順番に設定を行っていきます。
## 1台目のデバイスをセットアップする
## Remote Database configuration
リモートデータベースが新規の場合、またはサーバー全体を再構築する際にこのデバイスを明示的に正本とする場合に限り、この手順を使ってください。
### Remote databaseの設定
セットアップしたデータベースの情報を入力していきます。
1. 対象の Vault に Self-hosted LiveSync をインストールして有効にします。
2. `Welcome to Self-hosted LiveSync` 通知を選び、オンボーディングを開きます。
3. `I am setting this up for the first time` を選び、新しい同期をセットアップすることを確認します。
4. `Connection Method` で `Use a Setup URI (Recommended)` を選びます。
5. セットアップ URI を貼り付け、URI 用のパスフレーズを入力して `Test Settings and Continue` を選びます。
![](../images/quick_setup_3.png)
![暗号化されたセットアップ URI とマスクされたパスフレーズ](../images/quick-setup/guide-quick-setup-first-setup-uri.png)
これらはデータベースをセットアップした際に決めた情報です。
6. `Setup Complete: Preparing to Initialise Server` の内容を確認し、`Restart and Initialise Server` を選びます。
### Test database connectionとCheck database configuration
ここで、データベースへの接続状況と、データベース設定を確認します。
![](../images/quick_setup_5.png)
![1台目のサーバー初期化の確認](../images/quick-setup/guide-quick-setup-first-initialise.png)
#### Test Database Connection
データベースに接続できるか自体を確認します。失敗する場合はいくつか理由がありますが、一度Check database configurationを行ってそちらでも失敗するか確認してください。
7. 最終的な上書き警告をよく読みます。バックアップがあり、リモートデータを置き換える意図があることを確認してから、`I Understand, Overwrite Server` を選びます。
#### Check database configuration
データベースの設定を確認し、不備がある場合はその場で修正します。
![サーバー上書きの最終確認](../images/quick-setup/guide-quick-setup-first-rebuild-confirmation.png)
![](../images/quick_setup_6.png)
8. 新規に用意したデータベースでは、`No Synchronisation Settings Found` と表示される場合があります。新しいデータベースであることが確かな場合は、`Use this device's settings` を選びます。既存の設定があるはずなら、キャンセルして接続先のリモートを確認してください。キャンセルすると、Vault とデータベースへの反映を一時停止した状態で再起動します。リモートを確認するまで LiveSync を無効のままにしてください。`Could Not Read Synchronisation Settings` と表示された場合は再試行するか、このデバイスの設定を使って上書きを続行します。上書きには正常な接続が必要です。
この項目は接続先によって異なる場合があります。上記の場合、みっつのFixボタンを順にすべて押してください。
Fixボタンがなくなり、すべてチェックマークになれば完了です。
![新規データベースで想定されるリモート設定なしの選択肢](../images/quick-setup/guide-quick-setup-missing-remote-configuration.png)
### 機密性設定
9. `All optional features are disabled` の確認に進みます。通常の同期経路を確認するまで、オプション機能は無効のままにしてください。
10. 初期化と、要求された再起動が完了するまで待ちます。LiveSync の進捗表示が消えるまで Obsidian を開いたままにしてください。
![](../images/quick_setup_4.png)
別のデバイスを追加する前に、通常のテストノートを作成し、アップロードが完了するまで待ちます。
意図しないデータベースの暴露に備えて、End to End Encryptionを有効にします。この項目を有効にした場合、デバイスを出る瞬間にノートの内容が暗号化されます。`Path Obfuscation`を有効にすると、ファイル名も難読化されます。現在は安定しているため、こちらも推奨されます。
暗号化には256bitのAES-GCMを採用しています。
これらの設定は、あなたが閉じたネットワークの内側にいて、かつ第三者からアクセスされない事が明確な場合には無効にできます。
## 別のデバイス用のセットアップ URI を作成する
動作中の1台目のデバイスで、追加デバイス用のセットアップ URI を作成します。サーバーのプロビジョニング時に作成した URI を使い回すのではなく、1台目で実際に使われている設定を取り込めます。
![](../images/quick_setup_7.png)
1. 1台目で Obsidian のコマンドパレットを開きます。
2. `Self-hosted LiveSync: Copy settings as a new Setup URI` を実行します。
3. URI を保護する新しいパスフレーズを入力し、`OK` を選びます。
### Next
次へ進みます
![追加デバイス用 URI のマスクされたパスフレーズ](../images/quick-setup/guide-quick-setup-copy-setup-uri-passphrase.png)
### Discard exist database and proceed
すでにRemote databaseがある場合、Remote databaseの内容を破棄してから次へ進みます
4. 生成されたセットアップ URI をコピーし、`OK` を選びます。
![動作中の1台目で生成したセットアップ URI](../images/quick-setup/guide-quick-setup-copy-setup-uri-result.png)
## Sync Settings
最後に同期方法の設定を行います。
新しい URI とそのパスフレーズは別々に保管してください。URI は暗号化されていますが、認証情報と Vault 設定を含むため、引き続き安全に扱ってください。
![](../images/quick_setup_9_1.png)
## 別のデバイスを追加する
Presetsから、いずれかの同期方法を選び`Apply`を行うと、必要に応じてローカル・リモートのデータベースを初期化・構築します。
「All done!」(日本語環境では「完了!」)と表示されれば完了です。自動的に、「現在の設定をセットアップURIにコピー」のダイアログが開き、Setup URIを暗号化するためのパスフレーズを求められます(このパスフレーズはSetup URIを暗号化するためのもので、Vault自体の暗号化キーではありません)。
パスフレーズを入力すると、クリップボードにSetup URIが保存されますので、これを2台目以降のデバイスに何らかの方法で転送してください。
新しい Vault、または別途バックアップした Vault を使ってください。未同期のノートがある本番 Vault は、[Fast Setup の選択肢](./tips/fast-setup_ja.md)を確認せずに使用しないでください。
![](../images/quick_setup_10.png)
1. Self-hosted LiveSync をインストールして有効にします。
2. `Welcome to Self-hosted LiveSync` 通知からオンボーディングを開きます。
3. `I am adding a device to an existing synchronisation setup` を選び、デバイスを追加することを確認します。
4. `Device Setup Method` で `Use a Setup URI (Recommended)` を選びます。
5. 1台目で作成した新しいセットアップ URI を貼り付け、URI 用のパスフレーズを入力して `Test Settings and Continue` を選びます。
6. `Setup Complete: Preparing to Fetch Synchronisation Data` の内容を確認し、`Restart and Fetch Data` を選びます。
お好みのパスフレーズを設定してください。
クリップボードにSetup URIが保存されますので、これを2台目以降のデバイスに何らかの方法で転送してください。
![追加デバイスでのデータ取得確認](../images/quick-setup/guide-quick-setup-second-fetch.png)
# 2台目以降の設定方法
2台目の端末にSelf-hosted LiveSyncをインストールしたあと、コマンドパレットから`Use the copied setup URI (Formerly Open setup URI)`を選択し、転送したsetup URIを入力します。その後、パスフレーズを入力するとセットアップ用のウィザードが開きます。
下記のように答えてください。
7. 新規または空の Vault では `Overwrite all with remote files` を選びます。ローカルの作業がある Vault では操作を止め、[Fast Setup ガイド](./tips/fast-setup_ja.md)から適切な方法を選んでください。
- `Importing LiveSync's conf, OK?` に `Yes`
- `How would you like to set it up?` に `Set it up as secondary or subsequent device`
![Fast Setup のデータ取得方法の選択肢](../images/quick-setup/guide-quick-setup-retrieval-method.png)
これで設定が反映され、レプリケーションが開始されます。
8. 追加のローカルファイルの扱いを尋ねられたら、保守的な選択肢は `Keep local files even if not on remote` です。ローカル Vault を破棄してリモートと完全に同じ状態にする意図がある場合に限り、削除する選択肢を使ってください。
> [!TIP]
> **ファストセットアップ (Fast Setup)**
> 近年のバージョンでは、セットアップURIの読み込みやデータの全取得(Fetch All)を実行した際、より簡単に同期戦略を選択して即座に初期同期を完了できる **ファストセットアップ (Simple Fetch)** フローが利用できます。詳細は [ファストセットアップガイド](./tips/fast-setup_ja.md) をご参照ください。
![追加デバイスでのローカルファイルの扱い](../images/quick-setup/guide-quick-setup-local-file-policy.png)
9. データ取得、ファイルへの反映、および要求された再起動が完了するまで待ちます。LiveSync の進捗表示が消えるまで Obsidian を開いたままにしてください。
1台目で作成したテストノートが変更されずに表示されることを確認します。次に、新しいデバイスで2つ目の通常ノートを作成または編集し、1台目に届くことを確認します。
![プロビジョニング済み URI を使って同期された通常ノート](../images/quick-setup/guide-quick-setup-synchronised-note.png)
## 通常の同期が動作した後
オプション機能は、どの機能が管理するかと初期化の方向を明確にするため、1つずつ追加してください。
- 選択した非表示ファイルやフォルダーには[非表示ファイル同期](./tips/hidden-file-sync.md)を使用します。
- Obsidian のカスタマイズ設定には[カスタマイズ同期](./settings_ja.md#6-customisation-sync-advanced)を使用します。
同じファイルに両方の機能を有効にしないでください。
## 1台目で CouchDB を手動設定する
この方法は CouchDB の準備ができていて、セットアップ URI を利用できない場合に使います。表示されるオンボーディング画面で1台目を設定する手順であり、CouchDB サーバーのプロビジョニングや修復は行いません。後続のデバイスには、認証情報を再入力するのではなく、この1台目で動作確認後に作成したセットアップ URI を使ってください。
1. 対象の Vault に Self-hosted LiveSync をインストールして有効にします。
2. `Welcome to Self-hosted LiveSync` 通知を選び、`I am setting this up for the first time` を選択して、新しい同期をセットアップすることを確認します。
3. `Connection Method` で `Configure a remote manually`、続けて `Proceed with manual configuration` を選びます。
![オンボーディングでのリモート手動設定](../images/couchdb-manual/guide-couchdb-manual-connection-method.png)
4. `End-to-End Encryption` で同期データの保護方法を決めます。
- 通常の新規 Vault では `End-to-End Encryption` を有効にし、強力な Vault 暗号化パスフレーズを入力します。
- リモートドキュメントのプロパティも隠す場合は `Obfuscate Properties` を有効にします。
- Vault 暗号化パスフレーズは安全に保管してください。セットアップ URI を保護するパスフレーズとは別のものです。
![パスフレーズをマスクした CouchDB の Vault 暗号化設定](../images/couchdb-manual/guide-couchdb-manual-encryption.png)
5. `Choose a synchronisation remote` で `CouchDB` を選び、`Continue to CouchDB setup` を選びます。
![同期リモート一覧の CouchDB](../images/couchdb-manual/guide-couchdb-manual-remote-selection.png)
6. CouchDB の完全な URL、ユーザー名、パスワード、データベース名を入力します。
- Obsidian Mobile では HTTPS が必要です。HTTP は、デスクトップから信頼できるローカル接続を行う場合に限り使用できます。
- 選択したデータベースへの接続が許可された認証情報を使います。1台目を設定する際、データベースが存在しなければ作成できる権限も必要です。
![パスワードをマスクした CouchDB 接続情報](../images/couchdb-manual/guide-couchdb-manual-connection-details.png)
7. `Check server requirements` は任意です。表示中の認証情報を Obsidian の内部リクエスト API 経由で設定済みサーバーへ送信します。一部の検査には CouchDB 管理者権限が必要です。最初の検査は読み取り専用です。サーバー変更が提案された場合は、個々の変更内容を確認してから別途承認してください。
![CouchDB サーバー要件の検査成功例](../images/couchdb-manual/guide-couchdb-manual-server-requirements.png)
8. `Create or connect to database and continue` を選びます。オンボーディングを続けるには、この接続テストに成功する必要があります。
9. `Setup Complete: Preparing to Initialise Server` の内容を確認し、`Restart and Initialise Server` を選びます。
10. 最終的な上書き警告を読みます。このデバイスを正本とする意図があり、最新のバックアップがある場合に限り、`I Understand, Overwrite Server` を選びます。
11. 新規作成したデータベースでは `No Synchronisation Settings Found` と表示される場合があります。この新規データベースであることが確かな場合は、`Use this device's settings` を選びます。既存の設定があるはずなら、キャンセルして接続先を確認してください。キャンセル後は Vault とデータベースへの反映を一時停止した状態で再起動するため、リモートを確認するまで LiveSync を無効のままにしてください。`Could Not Read Synchronisation Settings` と表示された場合は再試行するか、このデバイスの設定を使って上書きを続行します。上書きには正常な接続が必要です。
12. `All optional features are disabled` の確認に進み、初期化の進捗が消えるまで Obsidian を開いたままにしてください。
通常のテストノートを作成し、同期されることを確認します。CouchDB に届いたら、[別のデバイス用のセットアップ URI を作成する](#別のデバイス用のセットアップ-uri-を作成する)、続けて[別のデバイスを追加する](#別のデバイスを追加する)に進んでください。これにより、2台目の設定を、1台目で実際に適用されたリモートプロファイルと暗号化設定に合わせられます。
+1434 -312
View File
File diff suppressed because it is too large. Load diff
+16 -15
View File
@@ -10,55 +10,56 @@
1. カタログが開くので、「Cloudant」と検索してください。出てきた選択肢をクリックすると作成画面に進みます。
![step 2](../instruction_images/cloudant_2.png)
1. Liteプランを選択してください。
1. 無料の Lite プランを選択してください。
![step 3](../instruction_images/cloudant_3.png)
1. リージョンと環境を選択します。Lite では Multitenant しか選択できないので、Multitenant を選択してください。デフォルトで選択されています。
リージョンはお好みの場所で作成してください。
![step 4](../instruction_images/cloudant_4.png)
3. "Authentication Method"で「IAM and legacy credentials」を選択します。
1. "Authentication Method"で「IAM and legacy credentials」を選択します。
![step 5](../instruction_images/cloudant_5.png)
4. Liteプランが選択されていることと、Capacityを確認します。
1. Lite プランが選択されていることと、Capacity を確認します。
![step 6](../instruction_images/cloudant_6.png)
5. 確認ができたら、右側のCreateボタンをクリックします。
1. 確認ができたら、右側の Create ボタンをクリックします。
![step 7](../instruction_images/cloudant_7.png)
6. 上記の手順が正常に完了したら、左のメニューから「リソース・リスト」をクリックしてください。リソース・リストが表示され、「サービス及びソフトウエア」に作成したCloudantのインスタンスが表示されます。
1. 上記の手順が正常に完了したら、左のメニューから「リソース・リスト」をクリックしてください。リソース・リストが表示され、「サービスおよびソフトウェア」に作成した Cloudant のインスタンスが表示されます。
インスタンス名をクリックしてください。
![step 8](../instruction_images/cloudant_8.png)
7. ここで、"External Endpoint (preferred)" と記載されているアドレスを控えてください。後ほど使います。<sup>(\*1)</sup>
1. ここで、"External Endpoint (preferred)" と記載されているアドレスを控えてください。後ほどデータベース名と組み合わせて使用します。<sup>(\*1)</sup>
![step 9](../instruction_images/cloudant_9.png)
## データベースの設定
1. 「Launch Dashboard」ボタンをクリックします。そうすると、今度はデータベースのダッシュボードが表示されます。CouchDBには、Fauxtonというインターフェイスがあるのですが、それそのものです。
1. 「Launch Dashboard」ボタンをクリックします。すると、データベースのダッシュボードが表示されます。これは CouchDB の Fauxton インターフェースとほぼ同じものです。
![step 1](../instruction_images/couchdb_1.png)
1. CORS の許可設定を行います。メニューの「Account」をクリックし、「CORS」タブを開きます。
最初は「Restrict to specific domains」が選択されているので、「All domains (\*)」を選択し直します。この反映は即座に行われますが、すぐに戻せるので大丈夫です。
ただし、Cloudant では `app://obsidian.md` を許可する設定は使用できません。
![step 2](../instruction_images/couchdb_2.png)
1. データベースが作成できるか確認します。メニューの「Databases」をクリックし、次に「Create Database」ボタンをクリックします。
右側にパネルが表示されますので、好きな名前を入力し、「Create」ボタンをクリックします。
右側にパネルが表示されますので、好きな名前を入力し<sup>(\*2)</sup>、「Create」ボタンをクリックします。
![step 3](../instruction_images/couchdb_3.png)
1. それっぽいメッセージが表示された後、データベースが表示されていれば、ほとんどセットアップは完了です。今後、ほとんどこの画面は使いません。Self-hosted LiveSyncからデータベースは作成できます。
1. 完了を示すメッセージが表示された後、データベースが表示されていれば、セットアップはほぼ完了です。データベースを作成できることを確認した後は、通常この画面を開く必要はありません。Self-hosted LiveSync からもデータベースを作成できます。
![step 4](../instruction_images/couchdb_4.png)
### 資格情報のセットアップ
### 認証情報のセットアップ
1. IBM Cloud に戻って、「サービス資格情報」をクリックしてください。おそらく何も表示されていないので、「新規資格情報」をクリックします。
![step 1](../instruction_images/credentials_1.png)
1. 資格情報を作成するダイアログが表示されるので、わかりやすい名前を入力します。その後、役割に「管理者」が選択されていることを確認してから、「追加」ボタンをクリックしてください。
1. 認証情報を作成するダイアログが表示されるので、任意の名前を入力するか既定のままにして、「追加」ボタンをクリックしてください。
![step 2](../instruction_images/credentials_2.png)
備考: この「名前」は Self-hosted LiveSync で使用する Username とはまた別のものです。
1. 「サービス資格情報」に戻ると、新しい資格情報が作成されています。~~わかりにくいことに名前は「鍵名」に変わります~~。左側のボタンを押すと詳細が開きます。
1. 「サービス資格情報」に戻ると、新しい認証情報が作成されています。~~わかりにくいことに名前は「鍵名」に変わります~~。左側のボタンを押すと詳細が開きます。
![step 3](../instruction_images/credentials_3.png)
Self-hosted LiveSync から使用する Username と Password は、表示された JSON に記載されているものを使用します。
今回の図で言うと、Username は"apikey-v2-2unu15184f7o8emr90xlqgkm2ncwhbltml6tgnjl9sd5"<sup>(\*3)</sup>、パスワードは"c2c11651d75497fa3d3c486e4c8bdf27"<sup>(\*4)</sup>になります。
@@ -69,11 +70,11 @@
先ほどの設定例から引用すると、
| Items | Value | example |
| ------------------- | -------------------------------- | --------------------------------------------------------------------------- |
| 項目 | 値 | 例 |
| --- | --- | --- |
| URI | (\*1) | https://xxxxxxxxxxxxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud |
| Username | (\*3) | apikey-v2-2unu15184f7o8emr90xlqgkm2ncwhbltml6tgnjl9sd5 |
| Password | (\*4) | c2c11651d75497fa3d3c486e4c8bdf27 |
| Database name | (\*2) | sync-test |
となります。
のとおりです。
+424 -83
View File
@@ -1,93 +1,434 @@
# CouchDBのセットアップ方法
# CouchDB サーバーのセットアップ
## CouchDBのインストールとPCやMacでの使用
CouchDBを構築するには、[Dockerのイメージ](https://hub.docker.com/_/couchdb)を使用するのが一番簡単です。
ただし、インストールしたCouchDBをSelf-hosted LiveSyncから使用するためには、少々設定が必要となります。
具体的には、下記の設定が`local.ini`として必要になります。
## 目次
- [1. CouchDB を準備する](#1-prepare-couchdb)
- [A. Docker を使用する](#a-using-docker)
- [B. Docker Compose を使用する](#b-using-docker-compose)
- [C. CouchDB を直接インストールする](#c-install-couchdb-directly)
- [2. couchdb-init.sh を実行して初期化する](#2-run-couchdb-initsh-for-initialise)
- [3. デバイスから CouchDB に接続できるようにする](#3-expose-couchdb-to-the-internet)
- [4. クライアントのセットアップ](#4-client-setup)
- [手動セットアップ情報](#manual-setup-information)
- [リバースプロキシ](#reverse-proxies)
---
<a id="1-prepare-couchdb"></a>
## 1. CouchDB を準備する
<a id="a-using-docker"></a>
### A. Docker を使用する
<a id="1-prepare"></a>
#### 1. 準備
```bash
# 環境変数を設定する。
export hostname=http://localhost:5984
export username=goojdasjdas # 好きな値に変更してください。
export password=kpkdasdosakpdsa # 好きな値に変更してください。
# 保存データと設定用のディレクトリを作成する。
mkdir couchdb-data
mkdir couchdb-etc
```
<a id="2-run-docker-container"></a>
#### 2. Docker コンテナを実行する
1. 起動確認。
```
$ docker run --name couchdb-for-ols --rm -it -e COUCHDB_USER=${username} -e COUCHDB_PASSWORD=${password} -v ${PWD}/couchdb-data:/opt/couchdb/data -v ${PWD}/couchdb-etc:/opt/couchdb/etc/local.d -p 5984:5984 couchdb
```
> [!WARNING]
> コンテナがエラーで終了した場合は、couchdb-data と couchdb-etc のアクセス権を確認してください。
> CouchDB の起動後、これらのディレクトリの所有者は uid:`5984` になります。その uid に所有者を変更し直してください。
2. バックグラウンドで有効化する
```
$ docker run --name couchdb-for-ols -d --restart always -e COUCHDB_USER=${username} -e COUCHDB_PASSWORD=${password} -v ${PWD}/couchdb-data:/opt/couchdb/data -v ${PWD}/couchdb-etc:/opt/couchdb/etc/local.d -p 5984:5984 couchdb
```
完了したら、[手順 2](#2-run-couchdb-initsh-for-initialise)に進んでください。
<a id="b-using-docker-compose"></a>
### B. Docker Compose を使用する
<a id="1-prepare-1"></a>
#### 1. 準備
```
[couchdb]
single_node=true
max_document_size = 50000000
# 保存データと設定用のディレクトリを作成する。
mkdir couchdb-data
mkdir couchdb-etc
[chttpd]
require_valid_user = true
max_http_request_size = 4294967296
# 所有者をユーザー 5984 に変更する。
chown -R 5984:5984 ./couchdb-data
chown -R 5984:5984 ./couchdb-etc
```
[chttpd_auth]
require_valid_user = true
authentication_redirect = /_utils/session.html
<a id="2-create-a-docker-composeyml-file-with-the-following-added-to-it"></a>
[httpd]
WWW-Authenticate = Basic realm="couchdb"
enable_cors = true
#### 2. 次の内容を追加した `docker-compose.yml` ファイルを作成する
```
services:
couchdb:
image: couchdb:latest
container_name: couchdb-for-ols
user: 5984:5984
environment:
- COUCHDB_USER=<INSERT USERNAME HERE> # 好きな値に変更してください。
- COUCHDB_PASSWORD=<INSERT PASSWORD HERE> # 好きな値に変更してください。
volumes:
- ./couchdb-data:/opt/couchdb/data
- ./couchdb-etc:/opt/couchdb/etc/local.d
ports:
- 5984:5984
restart: unless-stopped
```
<a id="3-run-the-docker-compose-file-to-boot-check"></a>
#### 3. Docker Compose ファイルを実行して起動確認する
```
docker compose up
# 古いバージョンを使用している場合
docker-compose up
```
> [!WARNING]
> コンテナがエラーで終了した場合は、couchdb-data と couchdb-etc のアクセス権を確認してください。
> CouchDB の起動後、これらのディレクトリの所有者は uid:`5984` になります。その uid に所有者を変更し直してください。
<a id="4-run-the-docker-compose-file-in-the-background"></a>
#### 4. Docker Compose ファイルをバックグラウンドで実行する
すべて正常に完了し、エラーが発生しなかった場合は、`CTRL+C` で終了してから次のコマンドを実行します。
```
docker compose up -d
# 古いバージョンを使用している場合
docker-compose up -d
```
完了したら、[手順 2](#2-run-couchdb-initsh-for-initialise)に進んでください。
<a id="c-install-couchdb-directly"></a>
### C. CouchDB を直接インストールする
[公式ドキュメント](https://docs.couchdb.org/en/stable/install/index.html)を参照してください。ただし、完全に設定する必要はありません。管理者を設定するだけで十分です。
<a id="2-run-couchdb-initsh-for-initialise"></a>
## 2. couchdb-init.sh を実行して初期化する
Deno 2 が必要です。CouchDB の接続情報とデータベースの詳細をエクスポートし、プロビジョニング用のラッパーを実行します。
この手順の `username` と `password` は CouchDB の管理者認証情報です。Docker または Docker Compose の場合は、`COUCHDB_USER` と `COUCHDB_PASSWORD` に指定した同じ値を使用します。直接インストールした場合は、CouchDB のセットアップ時に設定した管理者を使用します。ラッパーは、管理者以外の同期用アカウントを別途作成しません。
```
export hostname=http://localhost:5984
export username='<INSERT COUCHDB ADMINISTRATOR USERNAME HERE>'
export password='<INSERT COUCHDB ADMINISTRATOR PASSWORD HERE>'
export database=obsidiannotes
curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | bash
```
> [!TIP]
> fish では `hostname` が読み取り専用で、エクスポートできません。代わりに `env` 経由で値を渡してください: `curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | env hostname='http://localhost:5984' username='<INSERT COUCHDB ADMINISTRATOR USERNAME HERE>' password='<INSERT COUCHDB ADMINISTRATOR PASSWORD HERE>' database=obsidiannotes bash`
次のように表示された場合:
```
CouchDB provisioning completed.
```
ラッパーは、レジストリでバージョンを固定した Commonlib を使用するコードをそのまま実行します。`database` を指定すると、データベースを作成し、Commonlib を通じて LiveSync のデータベースバージョンドキュメントを初期化します。`database` を指定しない場合は、CouchDB サーバーの設定のみを行います。
Docker Compose を使用していて上記のコマンドが動作しない場合、または `ERROR: Hostname missing` と表示される場合は、プレースホルダーを自分の値に置き換えて次のコマンドを実行してください:
```
curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | hostname='http://<YOUR SERVER IP>:5984' username='<INSERT COUCHDB ADMINISTRATOR USERNAME HERE>' password='<INSERT COUCHDB ADMINISTRATOR PASSWORD HERE>' database=obsidiannotes bash
```
<a id="3-expose-couchdb-to-the-internet"></a>
## 3. デバイスから CouchDB に接続できるようにする
- CouchDB を信頼できるローカルネットワーク上のデスクトップデバイスでのみ使用する場合は、この手順を省略できます。
- モバイルデバイスでは、Obsidian は有効な証明書付きの HTTPS を必要とします。HTTPS を使用するために CouchDB を公開する必要はありません。
デバイスから CouchDB サーバーへの接続方法を選択します。自分で運用する[リバースプロキシ](#reverse-proxies)、プライベートネットワーク経由のアクセス、または任意のトンネルサービスを使用できます。[Docker プロファイル](../docker/README.md#profile-details)には例があります。
Cloudflare は任意の選択肢の1つです。次の例では、一時的なテスト用に [Quick Tunnel](https://developers.cloudflare.com/tunnel/get-started/quick-tunnels/) を使用します。ランダムなホスト名が表示され、トンネルを作成するたびに変わります。また、`cloudflared` プロセスを停止すると URL は動作しなくなります。Quick Tunnel はテストと開発用であり、本番用ではありません。継続して使用する場合は、選択した接続方法を設定してください。Cloudflare を選ぶ場合は、[Cloudflare Tunnel ガイド](https://developers.cloudflare.com/tunnel/get-started/)が参考になります。
```
cloudflared tunnel --url http://localhost:5984
```
次の 2024 年の出力は形式の例です。ホスト名は例示のみを目的としています。実際には、自分のトンネルが出力した URL を使用してください。
```
2024-02-14T10:35:25Z INF Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee. If you intend to use Tunnels in production you should use a pre-created named tunnel by following: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
2024-02-14T10:35:25Z INF Requesting new quick Tunnel on trycloudflare.com...
2024-02-14T10:35:26Z INF +--------------------------------------------------------------------------------------------+
2024-02-14T10:35:26Z INF | Your quick Tunnel has been created! Visit it at (it may take some time to be reachable): |
2024-02-14T10:35:26Z INF | https://tiles-photograph-routine-groundwater.trycloudflare.com |
2024-02-14T10:35:26Z INF +--------------------------------------------------------------------------------------------+
:
:
:
```
トンネルが出力した URL をサーバーアドレスとして使用します。クライアントの設定とテストが完了するまで、トンネルプロセスを実行したままにしてください。
<a id="4-client-setup"></a>
## 4. クライアントのセットアップ
> [!TIP]
> 生成されたセットアップ URI は、新しい Vault の現在のデフォルトと、選択したリモートプロファイルを引き継ぐため、推奨される方法です。セットアップ URI を生成できない場合は、[1台目で CouchDB を手動設定する](./quick_setup_ja.md#1台目で-couchdb-を手動設定する)に従い、その後、動作中のデバイスから新しいセットアップ URI を生成して、追加のデバイスごとに使用してください。
<a id="1-generate-the-setup-uri-on-a-desktop-device-or-server"></a>
### 1. デスクトップデバイスまたはサーバーでセットアップ URI を生成する
ここでの `LIVESYNC_USER` と `LIVESYNC_PASS` は、Self-hosted LiveSync がこのデータベースへの通常のアクセス用に保存する認証情報です。手順 2 の管理者認証情報でもかまいません。このデータベースへの必要なアクセス権を持つ CouchDB アカウントを別途設定している場合は、そのアカウントを使用してください。プロビジョニングラッパーもセットアップ URI ジェネレーターも、その別のアカウントを作成しません。
デバイスから CouchDB に接続するときに使用するサーバー URL を使用します。上記の一時的な Quick Tunnel の例では、実行中の自分のトンネルが出力した URL を使用してください。
```bash
export LIVESYNC_HOST='<YOUR COUCHDB SERVER URL>'
export LIVESYNC_DB=obsidiannotes
export LIVESYNC_USER='<INSERT COUCHDB USERNAME FOR LIVESYNC>'
export LIVESYNC_PASS='<INSERT THE COUCHDB PASSWORD>'
export LIVESYNC_PASSPHRASE='<INSERT A STRONG VAULT ENCRYPTION PASSPHRASE>'
export uri_passphrase='<INSERT A SEPARATE SETUP URI PASSPHRASE>' # 任意
deno run --minimum-dependency-age=0 --allow-env https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/setup/generate_setup_uri.ts
```
> [!TIP]
> `LIVESYNC_PASSPHRASE` は、エンドツーエンド暗号化で同期 Vault のデータを保護します。`uri_passphrase` はセットアップ URI のみを保護します。異なる値を使用し、両方を安全に保管し、セットアップ URI とそのパスフレーズを同じ経路で送らないでください。
>
> `uri_passphrase` を省略した場合、ジェネレーターは暗号学的にランダムな値を作成し、一度だけ表示します。
ジェネレーターは、ランダムな ID キーの復旧コードも表示します。同じ Vault のセットアップ URI を再生成する可能性がある場合は、そのコードを保存してください。`id_recovery_code` として渡し直さないと、後で実行したときに異なるキーが作成されます。`id_mode=legacy` は、以前の ID 動作を使用する Vault に接続する場合にのみ設定してください。詳しくは[セットアップユーティリティのリファレンス](../utils/readme.md#setup-uri-generation)を参照してください。
ジェネレーターは、プロビジョニングユーティリティが使用するのと同じ、レジストリでバージョンを固定した Commonlib リリースを使用します。設定済みの CouchDB リモートプロファイルを作成し、新しい Vault の現在のデフォルトを適用して、Commonlib のセットアップ URI 契約でエンコードします。
デフォルトでは、URI は Ephemeral(一時的)であり、ジェネレーターが表示した正確な UTC 時刻までのみ開けます。これは作成から7日後ではなく、固定された7日間の UTC ウィンドウの終了時点です。URI に時間条件が必要ない場合、または既存の暗号化形式をサポートする古いクライアントで読み取る必要がある場合は、コマンドを実行する前に `uri_mode=persistent` を設定してください。
出力は次の形式になります。以下のタイムスタンプと値は例です。
```text
Generated couchdb Setup URI.
Ephemeral: usable until 2026-10-01T00:00:00.000Z (UTC).
Your passphrase for the Setup URI is: H7vX...a-random-32-character-value
This passphrase is never shown again, so store it safely.
ID recovery code: sls-id-v1:<64 lowercase hexadecimal characters>
Use id_recovery_code with this value and reuse the same remote settings when generating another Setup URI for the same Vault.
obsidian://setuplivesync?settings=%5B%22tm2DpsOE74nJAryprZO2M93wF%2Fvg.......4b26ed33230729%22%5D
```
セットアップ URI とそのパスフレーズは別々に保管してください。
<a id="2-setup-self-hosted-livesync-to-obsidian"></a>
### 2. Self-hosted LiveSync を Obsidian にセットアップする
1台目については、[クイックセットアップ](./quick_setup_ja.md#1台目のデバイスをセットアップする)に従ってください。現在のオンボーディング通知、セットアップ URI のインポート、サーバーの初期化、および新しくプロビジョニングされたデータベースで表示される安全確認について説明しています。
通常のノート同期が動作した後、その1台目で[新しいセットアップ URI を生成](./quick_setup_ja.md#別のデバイス用のセットアップ-uri-を作成する)し、[別のデバイスを追加する](./quick_setup_ja.md#別のデバイスを追加する)に従ってください。2台目を、プロビジョニング時に作成した初期セットアップ URI の保持に依存させないでください。オプション機能は、通常の経路が確認された後にのみ設定してください。[非表示ファイル同期には専用のガイド](./tips/hidden-file-sync.md)があります。
---
<a id="manual-setup-information"></a>
## 手動セットアップ情報
<a id="setting-up-your-domain"></a>
### ドメインの設定
ドメインの A レコードをサーバーに向けて設定し、任意の方法でリバースプロキシをホストします。
注: CouchDB をトップディレクトリにマウントすることは推奨しません。
Caddy を使用すると、SSL 付きでサーバーを自動的に提供でき便利です。
Caddy と CouchDB を同時に起動する [docker-compose.yml と ini ファイル](https://github.com/vrtmrz/self-hosted-livesync-server)を公開しています。Traefik を使用している場合は、下の[リバースプロキシ](#reverse-proxies)のセクションを確認してください。
また、サーバーログを必ず確認し、不正なアクセスに注意してください。
<a id="reverse-proxies"></a>
## リバースプロキシ
<a id="traefik"></a>
### Traefik
Traefik を使用している場合、この [docker-compose.yml](https://github.com/vrtmrz/obsidian-livesync/blob/main/docker-compose.traefik.yml) ファイル(下にも掲載)には、適切な CORS パラメーターがすべて設定されています。外部ネットワーク `proxy` が存在することを前提としています。
```yaml
services:
couchdb:
image: couchdb:latest
container_name: obsidian-livesync
user: 1000:1000
environment:
- COUCHDB_USER=username
- COUCHDB_PASSWORD=password
volumes:
- ./data:/opt/couchdb/data
- ./local.ini:/opt/couchdb/etc/local.ini
# Traefik に渡している場合はポートは不要
#ports:
# - 5984:5984
restart: unless-stopped
networks:
- proxy
labels:
- "traefik.enable=true"
# Traefik ネットワーク
- "traefik.docker.network=proxy"
# 'obsidian-livesync.example.org' を自分のドメインに置き換えるのを忘れないでください
- "traefik.http.routers.obsidian-livesync.rule=Host(`obsidian-livesync.example.org`)"
# 'websecure' エントリーポイントは基本的に HTTPS のエントリーポイントです。問題が発生した場合にのみ次のコードスニペットを確認してください。これがリバースプロキシする最初のコンテナでなければ、おそらく Traefik は正しく設定されています。
- "traefik.http.routers.obsidian-livesync.entrypoints=websecure"
- "traefik.http.routers.obsidian-livesync.service=obsidian-livesync"
- "traefik.http.services.obsidian-livesync.loadbalancer.server.port=5984"
- "traefik.http.routers.obsidian-livesync.tls=true"
# 'letsencrypt' を自分の証明書リゾルバーに置き換えてください
- "traefik.http.routers.obsidian-livesync.tls.certresolver=letsencrypt"
- "traefik.http.routers.obsidian-livesync.middlewares=obsidiancors"
# Traefik 2.x で CORS を動作させるために必要な部分はここから
- "traefik.http.middlewares.obsidiancors.headers.accesscontrolallowmethods=GET,PUT,POST,HEAD,DELETE"
- "traefik.http.middlewares.obsidiancors.headers.accesscontrolallowheaders=accept,authorization,content-type,origin,referer"
- "traefik.http.middlewares.obsidiancors.headers.accesscontrolalloworiginlist=app://obsidian.md,capacitor://localhost,http://localhost"
- "traefik.http.middlewares.obsidiancors.headers.accesscontrolmaxage=3600"
- "traefik.http.middlewares.obsidiancors.headers.addvaryheader=true"
- "traefik.http.middlewares.obsidiancors.headers.accessControlAllowCredentials=true"
networks:
proxy:
external: true
```
上記で言及した `traefik.yml` 設定ファイルの抜粋:
```yml
...
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: "websecure"
scheme: "https"
websecure:
address: ":443"
...
```
<a id="nginx"></a>
### Nginx
Nginx を CouchDB のリバースプロキシとして設定する際は、よくある間違いに注意してください。
1. Fast Fetch の進捗が止まり、無期限にフリーズしたように見える場合は、`proxy_buffering` が無効になっていることを確認してください:
```nginx
location / {
proxy_pass http://127.0.0.1:5984;
proxy_buffering off;
...
}
```
2. "413 Entity too large" エラーが発生する場合は、`client_max_body_size` を増やしてください:
```nginx
location / {
proxy_pass http://127.0.0.1:5984;
client_max_body_size 50M; # 必要に応じて値を調整してください
...
}
```
3. "404 Database not found" エラーが発生する場合は、CouchDB をルートロケーションに配置していることを確認してください(推奨):
```nginx
server {
server_name couchdb.domain.com
location / {
proxy_pass http://127.0.0.1:5984;
...
}
}
```
CouchDB をサブディレクトリに配置することも可能ですが、設定をそれに合わせて変更する必要があります:
```nginx
server {
server_name domain.com
location /couchdb {
rewrite ^ $request_uri;
rewrite ^/couchdb/(.*) /$1 break;
proxy_pass http://127.0.0.1:5984$uri;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
...
}
location /_session {
proxy_pass http://127.0.0.1:5984/_session;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
...
}
}
```
4. データベース接続の詳細設定でカスタム HTTP ヘッダーを追加した場合は、Nginx と CouchDB の両方の設定を更新してください:
Nginx:
```nginx
location / {
set $pass 1;
# カスタム HTTP ヘッダーを処理する例
if ($http_x_custom_header != 'foo'){
set $pass 0;
}
# 重要: OPTIONS リクエストにはヘッダーが含まれないため、常に CouchDB にプロキシする必要があります
if ($request_method = 'OPTIONS') {
set $pass 1;
}
if ($pass = 0) {
return 403;
}
proxy_pass http://127.0.0.1:5984;
...
}
```
couchdb-etc/docker.ini:
```ini
...
[cors]
origins = app://obsidian.md,capacitor://localhost,http://localhost
credentials = true
headers = accept, authorization, content-type, origin, referer
methods = GET, PUT, POST, HEAD, DELETE
max_age = 3600
origins = app://obsidian.md,capacitor://localhost,http://localhost
;カスタムヘッダーをリストに追加して、CORS が壊れないようにしてください
headers = accept, authorization, content-type, origin, referer, x-custom-header
```
このファイルを作成し、
```
$ docker run --rm -it -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v .local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb
```
とすると簡単にCouchDBを起動することができます。
備考:このとき、local.iniのオーナーが5984:5984になります。これは、Dockerイメージの制限事項です。編集する場合はいったんオーナーを変更してください。
正常にSelf-hosted LiveSyncからアクセスすることができたら、お好みでバックグラウンドで起動するように編集して起動してください。
例)
```
$ docker run -d --restart always -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v .local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb
```
## モバイルからのアクセス
MacやPCからアクセスする場合は上記の方法で作ったサーバーで問題ありませんが、モバイル端末からアクセスする場合は有効なSSLの証明書が必要となります。
### モバイルからのアクセスのテスト
テストを行う場合は、[localhost.run](http://localhost.run/)などのサービスが便利です。
```
$ ssh -R 80:localhost:5984 nokey@localhost.run
Warning: Permanently added the RSA host key for IP address '35.171.254.69' to the list of known hosts.
===============================================================================
Welcome to localhost.run!
Follow your favourite reverse tunnel at [https://twitter.com/localhost_run].
**You need a SSH key to access this service.**
If you get a permission denied follow Gitlab's most excellent howto:
https://docs.gitlab.com/ee/ssh/
*Only rsa and ed25519 keys are supported*
To set up and manage custom domains go to https://admin.localhost.run/
More details on custom domains (and how to enable subdomains of your custom
domain) at https://localhost.run/docs/custom-domains
To explore using localhost.run visit the documentation site:
https://localhost.run/docs/
===============================================================================
** your connection id is xxxxxxxxxxxxxxxxxxxxxxxxxxxx, please mention it if you send me a message about an issue. **
xxxxxxxx.localhost.run tunneled with tls termination, https://xxxxxxxx.localhost.run
Connection to localhost.run closed by remote host.
Connection to localhost.run closed.
```
このように表示された場合、`https://xxxxxxxx.localhost.run`が一時的なサーバアドレスとして使用できます。
### ドメインを設定してアクセスする。
DNSのAレコードを設定し、お好みの方法でリバースプロキシをホスティングしてください。
備考:トップディレクトリにCouchDBを露出させるのはおすすめしません。
Caddy等でLet's Encryptの証明書を自動取得すると運用が楽になります。
CaddyとCouchDBを同時に立てられる[docker-composeの設定とiniファイル](https://github.com/vrtmrz/self-hosted-livesync-server)を公開しています。
ぜひご利用下さい。
なお、サーバのログは必ず確認し、不正なアクセスに注意してください。
+22 -5
View File
@@ -4,12 +4,29 @@
![Synchronisation](../images/1.png)
1. ノートが更新された際、Obsidianがイベントを発報します。Obsidian-LiveSyncはそれをハンドリングして、ローカルのPouchDBに変更を反映します。
2. PouchDBは、リモートのCouchDBに差分をレプリケーションします。
3. 他のデバイスは、リモートのCouchDBを監視しているので、変更が検出された場合はそのまま差分がダウンロードされます。
4. Self-hosted LiveSyncはPouchDBに転送された変更を、ObsidianのVaultに反映していきます。
1. ノートが更新されると、Obsidian からイベントが通知されます。Self-hosted LiveSync はイベントを処理し、ローカルの PouchDB に変更を反映します。
2. PouchDB は差分をリモートの CouchDB に複製します。
3. 他のデバイスはリモートの CouchDB を監視し、変更を検出すると差分をダウンロードします。
4. Self-hosted LiveSync は、PouchDB が受信した変更を Obsidian の Vault に反映します。
図は2端末での単一方向として描きましたが、実際には双方向に、複数の端末間で実行されます。
図では2台のデバイス間の一方向として示していますが、実際には複数のデバイス間で双方向に同期します。
## ファイルイベントとストレージ書き込み
ファイルイベントは、Vault 内で検出された変更を表します。Commonlib はこれらのイベントをフィルタリングおよび直列化してから、ローカルデータベース内のファイルメタデータを更新します。したがって、キューに入った `DELETE` はデータベースの変更を要求するものであり、それ自体が物理ファイルを削除する命令ではありません。同期対象外へのファイル名の変更も、宛先ファイルがディスク上に残ったままデータベース上は削除扱いとなる場合があります。
逆方向の処理はデータベースのメタデータから始まります。レプリケーションされた変更やフルスキャンによってデータベースからストレージへのハンドラが呼び出され、競合およびコンテンツ保護ルールに従って Vault ファイルの書き込みや削除が行われます。古いファイルイベントによるメタデータの削除を防ぐことと、正当なレプリケーションによる削除を適用することは、別の判断基準として扱われます。
Commonlib の [Storage events and database-to-storage reflection](https://github.com/vrtmrz/livesync-commonlib/blob/main/docs/storage-events-and-reflection.md) には、イベントの境界、Commonlib 0.1.23 で導入された削除の再検証、およびその制限事項が記載されています。特に、削除保護機能は外部フォルダの大文字小文字の変更やパスの表記揺れの完全な収束を保証するものではありません。
## 現在の技術リファレンス
- [データベースのデータ構造 (Database Data Structures)](datastructure.md): 現在のメタデータおよびチャンクの形式、識別子の取り扱い、削除、リモート上の生の表現について解説しています。
- [Replicator のアーキテクチャ (Replicator architecture)](design_docs/replicator_architecture.md): プロバイダーの構成、アクティブな Replicator の公開、終了、および P2P の所有権について解説しています。
- [競合解決とリビジョン履歴 (Conflict resolution and revision provenance)](specs_conflict_resolution.md): 現在のリビジョンツリーおよびファイルの来歴ルールを定義しています。
- [チャンクの取得と待機 (Chunk Retrieval and Waiting)](design_docs/chunk_retrieval_and_waiting.md): 不足しているチャンクの到着と、処理が落ち着くまでの待機の扱いを定義しています。
- [パスコンポーネント長の互換性 (Path component length compatibility)](design_docs/path_component_length_compatibility.md): 255 UTF-8 バイトがパスの妥当性を判断する普遍的な規則ではなく、Android や Linux における互換性警告である理由を解説しています。
- [データ圧縮 (Data Compression)](specs_data_compression.md) および [ガベージコレクション V3 (Garbage Collection V3)](specs_garbage_collection.md): それぞれのストレージとメンテナンスに関する取り決めを説明しています。
## 帯域幅低減のために
+49 -24
View File
@@ -1,30 +1,29 @@
# ファストセットアップ (Fast Setup / Simple Fetch)
ファストセットアップは、2台目以降のデバイスのセットアップやデータベース再構築時のデータ取得・初期化処理を、直感的かつ迅速に行うための簡略化された同期フローです。
ファストセットアップ(Simple Fetch)は、新規または空の2台目以降のデバイスに初期複製する際の推奨フローです。復旧にも使えますが、取得時の選択によってローカルファイルが置き換わることがあります。すでに作業がある Vault で処理方法を選ぶ前に、価値のあるローカルデータとリモートデータを保全してください。正本を特定する方法は[復旧とフラグファイル](../recovery.md#first-choose-the-authoritative-copy)を参照してください。
従来のセットアップウィザードにおける複数の詳細なステップを踏むことなく、同期の基本方針を選択するだけで、データベースのダウンロードとローカルファイルのスキャン・反映を一連のプロセスとして自動的に実行します。
従来のセットアップウィザードにおける複数の詳細なステップを踏むことなく、同期の基本方針を選択するだけで、データベースのダウンロードとローカルファイルのスキャン・反映を一連のプロセスとして自動的に実行します。取得経路は、選択したリモートによって異なります。
---
## 仕組み
2台目以降のデバイスで **セットアップURI** をインポートした場合や、手動でデータの再フェッチ(Vaultルートに `redflag3.md` / `flag_fetch.md` を配置する等)が予約された場合、プラグインはリモートデータベースからのデータ取得スケジュールを設定します。
2台目以降のデバイスで **セットアップ URI** をインポートした場合や、手動でデータの再取得(Vault ルートに `redflag3.md` / `flag_fetch.md` を配置する等)が予約された場合、プラグインはリモートデータベースからのデータ取得スケジュールを設定します。
その後の起動時、プラグインはデータフェッチ予約モードで立ち上がり、**「Data retrieval scheduled(データ取得のスケジュール)」** という簡略化されたダイアログを表示します。
その後の起動時、プラグインはデータ取得予約モードで立ち上がり、**「Data retrieval scheduled(データ取得のスケジュール)」** という簡略化されたダイアログを表示します。
---
## 技術的な特徴
ファストセットアップは、高速かつ安全でクリーンな処理を実現するために、以下の最適化を行っています。
ファストセットアップでは、取得と反映に関して次の動作を行います。
1. **高速なストリーム受信**
- 全データを取得しますが、ストリーム受信によるレプリケーションを使用するため、処理が非常に高速です。
1. **CouchDB 向けのストリームベースの Fast Fetch**
- ストリームベースの Fast Fetch は CouchDB 専用です。リモートのメタデータを段階的に取得します。オブジェクトストレージと P2P は、それぞれ独自の取得経路を使用します。
2. **ストレージ反映の遅延による不要な警告の抑制**
- データのダウンロード中にローカルファイル(ストレージ)への書き出し(反映)処理をあえて遅延させることで、同期途中で発生しがちな「破損データ同期」や「サイズ不一致」などの一時的なエラー警告を抑え込みます。
- すべてのデータダウンロードが完了した後に一括してストレージへ書き出すため、不必要な警告画面でユーザーを混乱させません。
3. **時刻ベース比較の妥当性**
- 初期セットアップやリカバリーの段階(この状態に移行した直後)においては、概ねファイル更新時刻(タイムスタンプ)ベースでの単純比較を行うことで、十分かつ妥当な同期結果を得ることができます。これにより複雑な競合解決の手間を省きます。
- 取得中にローカルファイルへの反映を遅延させることで、リモートデータの取得中に発生する一時的な同期警告を抑制します。
3. **更新日時を比較してファイル全体を選択**
- **更新日時を比較して新しい方を採用** は更新日時を比較し、ファイル全体として新しい方のバージョンを選択します。ファイルの内容をマージする処理ではありません。作業中の Vault にこの方法を使う前に、価値のあるローカルおよびリモートの編集内容を保全し、[復旧とフラグファイル](../recovery.md#first-choose-the-authoritative-copy)を確認してください。
---
@@ -33,30 +32,31 @@
### ステップ 1: データの反映方法の選択
取得したリモートデータを、既存のローカルファイルとどのように統合するかを選択します。
1. **Compare time and take newer (newer-wins)**
- ファイルの更新日時を比較し、より新しい方を採用します。
- **推奨されるケース:** すでに Self-hosted LiveSync を使用しており、複数のデバイスで編集した変更内容をタイムスタンプに基づいて統合したい場合。
2. **Overwrite all with remote files (remote-wins)**
1. **更新日時を比較して新しい方を採用(newer-wins)**
- ファイルの更新日時を比較し、ファイル全体として新しい方のバージョンを採用します。両方のバージョンの編集内容を統合するものではありません。
- **推奨されるケース:** 両方のバージョンを保全した上で、更新日時に基づいてどちらかを意図的に選択したい場合。
- 作業中の Vault にこの方法を使う前に、価値のあるローカルおよびリモートの編集内容を保全し、[復旧とフラグファイル](../recovery.md#first-choose-the-authoritative-copy)を確認してください。
2. **すべてリモートのファイルで上書き(remote-wins)**
- リモートデータベースの内容を正(Source of Truth)として扱います。
- **推奨されるケース:** まったく新しいデバイスをセットアップする場合(空のVaultなど)。
- **推奨されるケース:** 新しい2台目以降のデバイスを、空の Vault でセットアップする場合。
- *警告: ローカルにあるすべてのファイルがリモートの内容で上書きされます。重要なデータがある場合は、事前にバックアップを取得してください。*
3. **Use the detailed flow (legacy)**
3. **詳細フローを使用(legacy)**
- 従来の詳細なセットアップウィザードダイアログに戻ります。
- **推奨されるケース:** データベースの構成オプションをステップバイステップで細かく制御・確認したい場合。
### ステップ 2: 競合および削除ルールの構成
ステップ 1 での選択内容に応じて、ローカルとリモートの不一致をどう処理するかを設定します。
#### 「Compare time and take newer」を選択した場合:
- **Delete local files if they were deleted on remote**
- 他のデバイスで削除済みのファイルをローカルからも削除し、Vaultを同期・クリーンな状態に保ちます。
- **Recreate remote files even if they were deleted on remote**
#### 「更新日時を比較して新しい方を採用」を選択した場合:
- **リモートで削除されている場合はローカルファイルを削除**
- 他のデバイスで削除済みのファイルをローカルからも削除し、ローカル Vault を整理して他のデバイスと揃った状態に保ちます。
- **リモートで削除されていてもローカルファイルを保持**
- 他のデバイスで削除されたファイルであっても、ローカルファイルを維持し、リモートデータベースに再度アップロードします。
#### 「Overwrite all with remote files」を選択した場合:
- **Delete local files if not on remote**
#### 「すべてリモートのファイルで上書き」を選択した場合:
- **リモートに存在しない場合はローカルファイルを削除**
- リモートに存在しないローカル専用ファイルを削除し、ローカルの Vault をリモートデータベースと完全に一致させます。
- **Keep local files even if not on remote**
- **リモートに存在しなくてもローカルファイルを保持**
- リモートに存在しないローカルファイルをそのまま残します。ただし、同期後に重複ファイルが発生する可能性があるため、その場合は手動でクリーンアップを行ってください。
### ステップ 3: 自動同期の実行
@@ -64,3 +64,28 @@
1. リモートデータベースの高速ダウンロードを実行します (`fetchLocalDBFast`)。
2. ローカルファイルへの変更反映のため、フォアグラウンドでフルスキャン (`synchroniseAllFilesBetweenDBandStorage`) を自動的に実行します。
3. 処理が完了すると、プラグインは通常の動作状態へ復帰します。
### ファストセットアップが中断した場合
ファストセットアップは、ドキュメントを保存しながら最後に正常に保存されたリモートの位置を記録します。一時的な接続の中断が発生した場合、その位置から自動的に再試行されます。キャプチャされたリモートの状態が正常にすべて保存された後にのみ、完了が報告されます。
データの復号、リモートレスポンスの読み取り、またはローカルデータベースへの書き込み中のエラーによって処理が停止した場合、LiveSync は Vault スキャンを実行せず、再構築されたローカルデータベースの確定も行いません。保存された位置と取得フラグはそのまま保持されます。保存された有効な選択肢は、再起動時に確認ダイアログで再び利用できます。部分的に再構築されたデータベースが Vault に適用されたり、新しいローカル変更と混ざったりしないよう、ファイル監視とデータベースの反映も一時停止されたままになります。
**ログを表示** で最初のエラー内容を確認し、原因を修正してから Obsidian を再起動して、保持された状態から再試行してください。処理を続行する意思がある間は、取得フラグを削除したり、手動で Scram スイッチを再開したりしないでください。再起動後も同じエラーが解消しない場合は、LiveSync を一時停止したままデータを保護し、[レポートを収集](../troubleshooting.md#collect-a-report) してください。
## 取得フラグに記憶される選択肢
ファストセットアップは、選択肢を `flag_fetch.md`(または取得フラグがそれだけの場合は `redflag3.md`)内の `json` コードブロックに保存します。このファイルは通常の Markdown であり、Obsidian 内で開いて編集できます。選択肢が更新されても、状態ブロックの外側にある説明文は保持されます。フラグファイル全体は一時的なものであり、処理が完了またはキャンセルされたときに削除されます。
中断後に再起動すると、現在のリモートと保存された選択肢が表示されます。それらの選択肢で続行するか、選び直すか、あるいは予約された取得をキャンセルできます。前回の取得が完了したもののフラグを削除できなかった場合も含め、保存された選択肢が確認なしに適用されることはありません。
新しい参加(Join)を開始するか、別の取得を予約すると、`flag_fetch.md` は空のフラグに置き換えられ、新しい選択肢が求められます。両方のファイル名が存在する場合は、空の場合も含めて `flag_fetch.md` が優先され、完了時に両方のエイリアスが削除されます。
生成されるブロックには `type: "livesync-fast-setup"`、`version: 1`、および2つの安定した英語の選択値(`stage1` と `stage2`)が含まれます。接続の認証情報は保存されません。既存の選択肢の認識可能な組み合わせのみが受け入れられます。確認を行うと、P2P 接続設定を変更した後であっても、現在選択されているリモートに選択肢が適用されます。
空のフラグがある場合は、通常の選択ダイアログが起動します。空ではないフラグに対応する有効な選択肢が含まれていない場合、プラグインは新しい選択肢を指定して続行するかどうかを確認します。これには不正な形式の JSON、未対応のバージョン、重複した状態ブロック、不完全な Markdown のコードフェンスなどが含まれます。続行を選択した場合、ファイル内容は変更されず、今回の試行にのみ新しい選択肢が使用されます。再起動すると再度確認が行われます。プラグインが Markdown を修復したり、代替ブロックを追加したりすることはありません。取得を完了またはキャンセルすると、説明文を含めフラグファイル全体が削除されます。
ストレージの読み取りまたは書き込みに失敗した場合は処理が停止し、空のフラグとしては扱われません。処理の最後にフラグの削除に失敗した場合、その問題が解決するまでファイル監視とデータベースの反映は一時停止されたままになります。フラグが残っている場合、再起動時に再度確認が求められます。
以前のバージョンでは、選択肢をデバイスのローカルストレージに個別に保存していました。それらの値は完了した取得の後にも残存している可能性があるため、再利用されなくなりました。中断された処理の実行中に更新した場合は、もう一度選択肢を確認してください。この変更自体によってダウンロード済みのデータや Fast Fetch(ファストフェッチ)のチェックポイントが削除されることはありません。
+1 -1
View File
@@ -26,7 +26,7 @@
"check": "npm run tsc-check && npm run tsc-check:apps && npm run lint && npm run lint:community && npm run lint:community:tools && npm run svelte-check && npm run check:compatibility",
"i18n:bake": "npm run i18n:yaml2json && npm run i18n:bakejson && npm run i18n:format",
"i18n:bakejson": "tsx _tools/bakei18n.ts",
"i18n:format": "prettier --config .prettierrc.mjs --write --log-level error 'src/common/messagesJson/*.json' 'src/common/messages/*.ts'",
"i18n:format": "prettier --config .prettierrc.mjs --write --log-level error \"src/common/messagesJson/*.json\" \"src/common/messages/*.ts\"",
"i18n:json2yaml": "tsx _tools/json2yaml.ts",
"i18n:yaml2json": "tsx _tools/yaml2json.ts",
"test:unit": "vitest run --config vitest.config.unit.ts",
+51 -30
View File
@@ -1,76 +1,97 @@
# Summary
---
title: 'Self-hosted LiveSync: ローカルファースト Obsidian Vault のための検査可能で復旧可能な複製'
tags:
- ローカルファーストソフトウェア
- 同期
- CouchDB
- PouchDB
- WebRTC
- Obsidian
- TypeScript
authors:
- name: 'vorotamoroz'
affiliation: 1
corresponding: true
affiliations:
- name: '独立研究者'
index: 1
date: 2026年9月5日
bibliography: paper.bib
---
# 概要
Self-hosted LiveSync は、ローカルの Markdown ファイルとして文書を保存するノートアプリ Obsidian [@obsidian] 向けのオープンソース同期プラグインである。ユーザーが管理するストレージまたは直接のピアツーピア接続を介し、ノートや添付ファイルを収めたディレクトリー(Vault)をデスクトップとモバイルデバイス間で同期する。
本プラグインにより、ユーザーはオフラインで編集を行い、再接続後に同期できる。2台のオフライン端末で同一ノートを別々に編集した場合のように編集の衝突が生じても、即座の解決を強制したり競合する変更を無条件に上書きしたりすることはない。ファイル形式や設定されたポリシーに応じて、重複しない変更箇所の自動マージや、競合する版を後から比較・解決するための保持が可能である。組み込みの検査ツールは、競合や内容の欠落の調査を支援し、コピーが残っている場合の復旧を支援する。
本プラグインにより、ユーザーはオフラインで編集を行い、再接続後に同期できる。2台のオフラインデバイスで同一ノートを別々に編集した場合のように編集の競合が生じても、即座の解決を強制したり競合する変更を無条件に上書きしたりすることはない。ファイル形式や設定されたポリシーに応じて、重複しない変更箇所の自動マージや、競合する版を後から比較・解決するための保持が可能である。組み込みの検査ツールは、競合や内容の欠落の調査を支援し、コピーが残っている場合の復旧を支援する。
本ソフトウエアは、データの保存先を自ら管理しながら複数デバイスで記録を継続する必要がある研究者、エンジニア、および実務者のワークフローに対応する。
本ソフトウェアは、データの保存先を自ら管理しながら複数デバイスで記録を継続する必要がある研究者、エンジニア、および実務者のワークフローに対応する。
# Statement of Need
# 背景と必要性
研究やエンジニアリングのワークフローは、長期間蓄積されるノート、観察記録、設計上の決定事項、および関連ファイルに依存している。著者の業務では、管理下にある各デバイスの導入ソフトウエアを制御し、業務ファイルを自身の管理下にあるインフラで扱い、運用実績のあるサーバーソフトウエアを採用する必要があった。これらの制約から、デスクトップとモバイル双方の Obsidian 内で直接動作し、外部クライアントデーモンを必要としない同期エンジンを開発した。
研究やエンジニアリングのワークフローは、長期間蓄積されるノート、観察記録、設計上の決定事項、および関連ファイルに依存している。著者の業務では、管理下にある各デバイスの導入ソフトウェアを制御し、業務ファイルを自身の管理下にあるインフラで扱い、運用実績のあるサーバーソフトウェアを採用する必要があった。これらの制約から、デスクトップとモバイル双方の Obsidian 内で直接動作し、外部クライアントデーモンを必要としない同期エンジンを開発した。
オフライン端末で別々に行った編集の競合は、変更を交換した際に認識される。意図しない編集や削除、並行した変更の乖離、あるいはデータベースの状態とは独立した外部ツールによるファイル変更も起こりうる。競合する版を保持せずに単一の版で上書きしてしまうと、ユーザーが変更を確認して判断する前に情報が失われるおそれがある。
オフラインデバイスで別々に行った編集の競合は、変更を交換した際に認識される。意図しない編集や削除、並行した変更の乖離、あるいはデータベースの状態とは独立した外部ツールによるファイル変更も起こりうる。競合する版を保持せずに単一の版で上書きしてしまうと、ユーザーが変更を確認して判断する前に、情報が不可逆的に失われるおそれがある。
フィールドワークやモバイルでの作業中、研究者や実務者は、競合する編集内容をレビューする前であっても、観察の記録とデバイス間でのノート転送を続ける必要がある。Self-hosted LiveSync は、このように記録と競合解決を分けて進める作業を支援する。並行ブランチが未解決のままでも複製を継続でき、競合する編集は、レビューまたは設定されたポリシーによって解決されるまで保護される。
# State of the Field
# 関連分野の現状
ローカルファーストソフトウエアは、ユーザーデータの唯一の所有者としてのホスト型サービスへの依存を避けつつ、ローカルにおける可用性と、複数デバイス間での同期や協調を両立させる [@kleppmann2019localfirst]。Obsidian エコシステム内では、いくつかのツールが異なるアプローチから複数デバイス間の同期に対応している。Obsidian Sync は統合されたホスト型サービスを提供し [@obsidiansync]、Obsidian Git はバージョン管理指向の push/pull ワークフローを提供し [@obsidiangit]、Syncthing はファイルシステム層で動作し [@syncthing]、Remotely Save はクラウドやセルフホスト型ストレージの複数の API に Obsidian を接続する [@remotelysave]。
ローカルファーストソフトウェアは、ユーザーデータの唯一の所有者としてのホスト型サービスへの依存を避けつつ、ローカルにおける可用性と、複数デバイス間での同期や協調を両立させる [@kleppmann2019localfirst]。Obsidian エコシステム内では、いくつかのツールが異なるアプローチから複数デバイス間の同期に対応している。Obsidian Sync は統合されたホスト型サービスを提供し [@obsidiansync]、Obsidian Git はバージョン管理指向の push/pull ワークフローを提供し [@obsidiangit]、Syncthing はファイルシステム層で動作し [@syncthing]、Remotely Save はクラウドやセルフホスト型ストレージの複数の API に Obsidian を接続する [@remotelysave]。
これらのアプローチは、並行する変更の表現方法が異なる。Syncthing は競合コピーを通常のファイルとして他のデバイスへ転送し [@syncthingsync]、Git は分岐した履歴をマージ前に取得できる [@gitfetch]。Obsidian Git はデスクトップおよびモバイル上でこの操作を自動化している [@obsidiangit]。Conflict-free Replicated Data Types(CRDT)でも複数の選択肢を検査でき、Automerge は同じオブジェクトプロパティーへの並行した代入を保持する [@automergeconflicts]。Self-hosted LiveSync は、競合するファイルの各バージョンを、メタデータドキュメントのリビジョンツリー上の末端リビジョン(leaf、各分岐の現在の版)として保持し、設定されたポリシーまたはユーザーによる明示的な操作によって解決されるまで維持する。
これらの既存ツールはそれぞれ異なる運用上の要請に応えている。ホスト型サービスは導入の平易さを重視し、外部のファイル同期ツールは任意のファイルシステムツリーを対象とし、バージョン管理ツールは明示的なコミットワークフローを前提としている。一方、著者の環境では管理対象デバイス上でバックグラウンドクライアントデーモンの実行が制限されており、かつ競合する版の保持と、その版に結び付いたファイル更新を扱うためには、複製処理とローカルファイル操作を直接統合する必要があった。そのため、Self-hosted LiveSync は外部デーモンを介さず Obsidian 内で直接動作するプラグインとして構築され、複数のバックエンドで同一のリビジョンセマンティクスを維持するために、中核ロジックをプラットホーム非依存のエンジンとして分離する構成が採用された。
これらの既存ツールはそれぞれ異なる運用上の要請に応えている。ホスト型サービスは導入の平易さを重視し、外部のファイル同期ツールは任意のファイルシステムツリーを対象とし、バージョン管理ツールは明示的なコミットワークフローを前提としている。一方、著者の環境では管理対象デバイス上でバックグラウンドクライアントデーモンの実行が制限されており、かつ競合する版の保持と、その版に結び付いたファイル更新を扱うためには、複製処理とローカルファイル操作を直接統合する必要があった。そのため、Self-hosted LiveSync は外部デーモンを介さず Obsidian 内で直接動作するプラグインとして構築され、複数のバックエンドで同一のリビジョンセマンティクスを維持するために、中核ロジックをプラットフォーム非依存のエンジンとして分離する構成が採用された。
Self-hosted LiveSync は新しいデータベース複製アルゴリズムを導入するものではない。むしろその貢献は、リビジョン認識可能なデータベースのセマンティクスを、外部から編集できるファイル Vault へ適用した点にある。Content-addressed なチャンク、デバイスローカルな来歴情報、および組み込みの復旧ツールにより、通常のノート作成ワークフローを損なうことなく、競合のレビューを保留しながら編集を継続できるようにしている。
# Software Design
# ソフトウェア設計
共通の複製サービスおよび競合処理サービスは `@vrtmrz/livesync-commonlib` [@commonlib021] として公開されており、Obsidian プラグイン、コマンドラインインターフェース(CLI)、Web アプリケーション、および Web Peer で利用されている。
## Revision-aware Vault representation
## リビジョンを認識する Vault の表現
Vault の各ファイルは、ローカルの PouchDB [@pouchdb] 内で、パス、サイズ、更新日時、および分割されたチャンクドキュメントへの参照を含むメタデータドキュメントとして表現される。チャンクは Content-addressed であり、同一のコンテンツ領域を持つリビジョン間や異なるファイル間で再利用できる。複数デバイス間で並行して更新が行われると、メタデータドキュメントの周囲に競合する複数の leaf(子を持たない末端リビジョン)が形成される。PouchDB はデフォルトの取得対象として決定論的な winner(選出された leaf)を選出するが、この選択は内部的なタイブレークに過ぎず、その winner がより新しい、より安全である、あるいは特定のデバイスの Vault に表示されているバージョンであることを証明するものではない。
Vault の各ファイルは、ローカルの PouchDB [@pouchdb] 内で、パス、サイズ、更新日時、および分割されたチャンクドキュメントへの参照を含むメタデータドキュメントとして表現される。チャンクは Content-addressed であり、同一のコンテンツ領域を持つリビジョン間や異なるファイル間で再利用できる。複数デバイス間で並行して更新が行われると、メタデータドキュメントに複数の競合末端リビジョンが形成される。PouchDB はデフォルトの取得対象となる代表リビジョン(winner)を決定論的に選ぶが、これは内部的なタイブレークに過ぎず、そのリビジョンがより新しい、より安全である、あるいは特定のデバイスの Vault に表示されているバージョンであることを意味しない。
並行する更新によってブランチ $\alpha$ と $\beta$ に分岐した場合、両方の leaf は解決前に他のデバイスへ複製される。自動3方向マージは、両方の leaf と最も近い利用可能な共通祖先についてメタデータ本文およびチャンクが読み取り可能である場合に、Markdown(`.md`)、Canvas(`.canvas`)、および JSON(`.json`)ファイルを対象として適用される(その他の形式は対象外である)。Markdown では、同一オフセットへの並行した挿入は即座に失敗とせず、更新日時に応じて順次連結して統合できる。CouchDB の複製プロトコルは祖先リビジョンの識別子を伝播するものの祖先の内容は取得しないため [@couchdbreplication]、祖先の履歴や内容が欠落している場合、あるいは互換性のない編集衝突が生じた場合、同期エンジンは自動マージを保留する。自動マージが無効または適用不能であり両方の版が読み取り可能である場合、JSON ファイルおよび内容の異なるバイナリーファイルは互換性のための動作として、「常に新しいファイルで上書きする」が無効であっても更新日時によって解決され、このオプションを有効にするとテキストの競合にも当該解決が拡張される。それ以外の場合、テキストの競合は手動解決のために保持され、両方の版が読み取り可能であれば2方向の差分(two-way diff)によって直接比較できる。
並行する更新によってブランチ $\alpha$ と $\beta$ に分岐した場合、両方の末端リビジョンは解決前に他のデバイスへ複製される。自動3方向マージは、両方の末端リビジョンと最も近い利用可能な共通祖先についてメタデータ本文およびチャンクが読み取り可能である場合に、Markdown(`.md`)、Canvas(`.canvas`)、および JSON(`.json`)ファイルを対象として適用される(その他の形式は対象外である)。Markdown では、同一オフセットへの並行した挿入は即座に失敗とせず、更新日時に応じて順次連結して統合できる。CouchDB の複製プロトコルは祖先リビジョンの識別子を伝播するものの祖先の内容は取得しないため [@couchdbreplication]、祖先の履歴や内容が欠落している場合、あるいは互換性のない編集競合が生じた場合、同期エンジンは自動マージを保留する。自動マージが無効または適用不能であり両方の版が読み取り可能である場合、JSON ファイルおよび内容の異なるバイナリーファイルは互換性のための動作として、「常に新しいファイルで上書きする」が無効であっても更新日時によって解決され、このオプションを有効にするとテキストの競合にも当該解決が拡張される。それ以外の場合、テキストの競合は手動解決のために保持され、両方の版が読み取り可能であれば2方向の差分(two-way diff)によって直接比較できる。
## Device-local branch provenance
## デバイスごとのブランチ来歴
データベースは競合する複数のブランチを同時に保持できるが、ローカルの Vault は任意のパスに対して単一の実体ファイルしか配置できない。ローカルファイルがどのブランチを表しているかを識別するため、本プラグインは正確なデータベースリビジョンと観測されたローカルの更新日時をデバイスローカルな Key-Value ストアに保存する。このリビジョンは当該パスの**ブランチアンカー**として機能し、データベースから Vault への実体化、または Vault からデータベースへの書き込みが成功した後に更新される。
未解決の競合が存在する状態において、ローカルで行われた編集や論理削除はアンカーされたリビジョンの子となり、競合する leaf を損なうことなく、その特定のブランチを前進させる。パスをまたぐリネームでは、移動先を保存した上で、アンカーされた移動元のブランチのみを論理削除する。この状態で来歴情報が利用できない場合、本プラグインはファイルのバイト列が利用可能な既存の単一リビジョン本文と厳密に一致する場合に限り Vault 内のファイルをそのリビジョンにひもづけ、それ以外の場合はパスや日時から勝手に推測せず、手動解決すべき競合として保持する。競合が存在しない通常時は、通常の書き込みによって単に現在のデータベースリビジョンが前進する。
未解決の競合が存在する状態において、ローカルで行われた編集や論理削除はアンカーされたリビジョンの子となり、他の競合末端リビジョンを損なうことなく、その特定のブランチを前進させる。パスをまたぐリネームでは、移動先を保存した上で、アンカーされた移動元のブランチのみを論理削除する。この状態で来歴情報が利用できない場合、本プラグインはファイルのバイト列が利用可能な既存の単一リビジョン本文と厳密に一致する場合に限り Vault 内のファイルをそのリビジョンにひもづけ、それ以外の場合はパスや日時から勝手に推測せず、手動解決すべき競合として保持する。競合が存在しない通常時は、通常の書き込みによって単に現在のデータベースリビジョンが前進する。
組み込みのコンフリクトインスペクターは、現在の winner、すべての conflict leaf、および最も近い利用可能な共通祖先を検査する。インスペクターは欠落したチャンクやファイル/データベース間の差異を報告し、現在の leaf を明示的に選択して操作できるようにする。変更を伴う操作は実行前にリビジョンを再確認し、古い画面状態によってすでに末端ではなくなったリビジョンを誤って削除したり前進させたりするのを防止する。
組み込みの競合インスペクターは、現在の代表リビジョン(winner)、すべての競合末端リビジョン、および最も近い利用可能な共通祖先を検査する。インスペクターは欠落したチャンクやファイル/データベース間の差異を報告し、現在の末端リビジョンを明示的に選択して操作できるようにする。変更を伴う操作は実行前にリビジョンを再確認し、古い画面状態によってすでに末端ではなくなったリビジョンを誤って削除したり前進させたりするのを防ぐ。
## Transport-independent replication
## 通信方式に依存しない複製
CouchDB のリビジョンモデルを基準に、Self-hosted LiveSync はデータベースの表現を通信トランスポートから分離し、バックエンドにかかわらずファイルメタデータのリビジョン識別子と競合する leaf をそのまま複製する。CouchDB [@couchdb] ではネイティブなリビジョン複製を利用する。S3 互換オブジェクトストレージでは、メタデータドキュメントの末端リビジョンと祖先リビジョンの識別子をジャーナルに記録し、新しいローカルリビジョンを作成せずに適用する一方、チャンクドキュメントは内容由来の識別子を保持し、新しいローカルリビジョンとして保存される。WebRTC ピアツーピア(P2P)アダプター [@webrtc] は、Trystero [@trystero] の DataChannels と RPC ベースのレプリケーション shim によりドキュメント要求をバッチ処理し、同一のリビジョンセマンティクスをピア間で直接保持する。CouchDB およびジャーナル転送においては、Web Streams が転送をパイプライン処理し、転送中にメモリーへ保持されるデータ量を抑制する。
CouchDB のリビジョンモデルを基準に、Self-hosted LiveSync はデータベースの表現を通信トランスポートから分離し、バックエンドにかかわらずファイルメタデータのリビジョン識別子と競合末端リビジョンをそのまま複製する。CouchDB [@couchdb] ではネイティブなリビジョン複製を利用する。S3 互換オブジェクトストレージでは、メタデータドキュメントの末端リビジョンと祖先リビジョンの識別子をジャーナルに記録し、新しいローカルリビジョンを作成せずに適用する一方、チャンクドキュメントは内容由来の識別子を保持し、新しいローカルリビジョンとして保存される。WebRTC ピアツーピア(P2P)アダプター [@webrtc] は、Trystero [@trystero] の DataChannels と RPC ベースのレプリケーション shim によりドキュメント要求をバッチ処理し、同一のリビジョンセマンティクスをピア間で直接保持する。CouchDB およびジャーナル転送においては、Web Streams が転送をパイプライン処理し、転送中にメモリーへ保持されるデータ量を抑制する。
これらのトランスポートは柔軟に組み合わせられる。P2P 同期は参加デバイスが同時にオンラインである必要があるが、中央の CouchDB やオブジェクトストレージを併用することで、オフライン期間を挟んだデバイス間でも同期できる。すべての通信方式でコンテンツのエンドツーエンド暗号化とパス難読化をサポートしている。P2P では接続交渉時のセッション記述が暗号化されるが、シグナリングリレーやネットワークサービスからは接続時刻やネットワークアドレスを観測できる。
## Retention and recovery
## 保持と復旧
分岐した各ブランチは未変更のチャンクを共有するため、競合する leaf を保持するために生じるコストは主に新規チャンクとリビジョンメタデータに限られる。蓄積した保存領域はリモートデータベースの再構築によって回収できるほか、CouchDB 向けには、明示的に開始するベータ版のガベージコレクションにより、現在の winner、すべての conflict leaf、および未解決の競合を検査するために必要な、利用可能な祖先から到達可能なチャンクを保護しながらインプレースで回収できる。過去のリビジョンで置き換えられたチャンクは後から回収されうるため、過去のリビジョン本文は無条件のバックアップではない。
分岐した各ブランチは未変更のチャンクを共有するため、競合末端リビジョンを保持するコストは主に新規チャンクとリビジョンメタデータに限られる。蓄積した保存領域はリモートデータベースの再構築によって回収できるほか、CouchDB 向けには、明示的に開始するベータ版のガベージコレクションにより、現在の代表リビジョンとすべての競合末端リビジョン、および未解決の競合を検査するために必要な、利用可能な祖先から到達可能なチャンクを保護しながらインプレースで回収できる。過去のリビジョンで置き換えられたチャンクは後から回収されうるため、過去のリビジョン本文は無条件のバックアップではない。
必要なチャンクが欠落している場合でも、読み取り不能な現在のリビジョンはリビジョンツリーに残り、競合処理によって自動的に破棄されることはない。欠落したチャンクが他のデバイスに残っている場合があるため、それらの再接続と同期を待って復旧操作を保留できる。競合インスペクターは影響を受けるリビジョンを明示し、取得の再試行や明示的な復旧操作を支援する。復旧には、デバイス、リモートストレージ、またはバックアップに内容が残っている必要がある。
# Research Impact Statement
# 研究への影響
Self-hosted LiveSync は、著者が複数のデバイスやプラットホームを対象に行うソフトウエア開発業務から生まれた。この作業では、主たるデバイスを利用できない状況でも、各デバイスでスクリーンショットを取得し、観察記録を保存する必要があった。同じワークフローは、現在では著者の先行技術調査にも利用されており、先行文献の読解に伴うメモや考察を同期するために用いられている。競合する版が保持されることで、分岐した記録が即座に上書きされず、後から比較・確認することが可能になる。
Self-hosted LiveSync は、著者が複数のデバイスやプラットフォームを対象に行うソフトウェア開発業務から生まれた。この作業では、主たるデバイスを利用できない状況でも、各デバイスでスクリーンショットを取得し、観察記録を保存する必要があった。同じワークフローは、現在では著者の先行技術調査にも利用されており、先行文献の読解に伴うメモや考察を同期するために用いられている。競合する版が保持されることで、分岐した記録が即座に上書きされず、後から比較・確認することが可能になる。
ユニットテストおよび結合テストは、リビジョンの系譜、チャンクの到達可能性、利用できない内容、およびホストの構成を対象とする。CLI および実環境の Obsidian によるシナリオでは、競合する leaf が残っている状態での編集、論理削除、およびリネームを含め、競合の伝播と解決を検証する。3ノードの P2P シナリオでは、未解決の leaf が解決前にデバイス間を移動できることを確認している。再利用可能なヘッドレステスト基盤は独立してアーカイブされている [@fancykit]。決定論的なフィクスチャーを用いて同一の生成データ上で P2P と CouchDB の経路を比較しているが、制御されたローカル測定値が普遍的な性能を示すわけではない。
ユニットテストおよび結合テストは、リビジョンの系譜、チャンクの到達可能性、利用できない内容、およびホストの構成を対象とする。CLI および実環境の Obsidian によるシナリオでは、競合する末端リビジョンが残っている状態での編集、論理削除、およびリネームを含め、競合の伝播と解決を検証する。3ノードの P2P シナリオでは、未解決の末端リビジョンが解決前にデバイス間を移動できることを確認している。再利用可能なヘッドレステスト基盤は独立してアーカイブされている [@fancykit]。決定論的なフィクスチャーを用いて同一の生成データ上で P2P と CouchDB の経路を比較しているが、制御されたローカル測定値が普遍的な性能を示すわけではない。
2026年9月2日時点で、Obsidian プラグインディレクトリーでは 90万回以上のダウンロード、デスクトップおよびモバイルのサポート、ならびに公式の Research カテゴリーへの配置が報告されている [@obsidianplugin]。GitHub リポジトリーでは 12,200件以上のスター、440件のフォーク、および広範なユーザーコミュニティーからの貢献が記録されている [@selfhostedlivesyncrepo]。これらの数値自体は研究上の直接的な影響を証明するものではないが、本ソフトウエアがコミュニティーに受容され、単一のプライベートなワークフローを超えて運用されている証拠を提供する。
2026年9月2日時点で、Obsidian プラグインディレクトリーでは 90万回以上のダウンロード、デスクトップおよびモバイルのサポート、ならびに Research カテゴリーへの配置が報告されている [@obsidianplugin]。GitHub リポジトリーでは 12,200件以上のスター、440件のフォーク、および幅広いユーザーコミュニティからの貢献が記録されている [@selfhostedlivesyncrepo]。これらの数値自体は研究上の直接的な影響を証明するものではないが、本ソフトウェアがコミュニティに受け入れられ、単一の個人的なワークフローを超えて利用されていることを示している。
本稿で説明したソフトウエアは Self-hosted LiveSync 1.0.23 [@selfhostedlivesync] であり、MIT ライセンスの下でリリースされ、Commonlib 0.1.21 [@commonlib021] に固定されている。プラグイン、再利用可能なテストハーネス [@fancykit]、および以前の Commonlib 0.1.19 のスナップショット [@commonlib] は Zenodo に恒久的にアーカイブされており、プラットホーム非依存のロジックが独立したテストと再利用を可能にしている。
本稿で説明したソフトウェアは Self-hosted LiveSync 1.0.23 [@selfhostedlivesync] であり、MIT ライセンスの下でリリースされ、Commonlib 0.1.21 [@commonlib021] に固定されている。プラグイン、再利用可能なテストハーネス [@fancykit]、および以前の Commonlib 0.1.19 のスナップショット [@commonlib] は Zenodo に恒久的にアーカイブされており、プラットフォーム非依存のロジックが独立したテストと再利用を可能にしている。
# AI Usage Disclosure
# AI の利用に関する開示
2026年7月から9月にかけて、コード探索、テストおよびベンチマークの足場作り、CI およびドキュメントの編集、原稿の推敲および校正、レビュー、引用の検証、ならびに結果の要約に GPT-5 を使用した OpenAI Codex が利用された。また、Codex を通じて GPT-6 も 9月の原稿レビューおよび改訂を支援した。本原稿の準備において、その他の生成 AI ツールは使用されていない。GitHub Copilot(モデルおよびバージョンは未記録)は、本リリースに含まれるコミットの実装、テスト、およびドキュメント作成を支援した。Google Gemini(Gemini Flash バージョン 3.5 から 3.8)は、リソースチェックおよび関連するコードベースの検証に使用された。人間の著者自身がすべての支援出力をレビュー、編集、および検証し、主要な設計判断を行い、関連する検証コマンドおよびベンチマークコマンドを実行した。著者は、提出された資料の正確性、独創性、ライセンス、および倫理的コンプライアンスについて引き続き全責任を負う。
2026年7月から9月にかけて、コード探索、テストおよびベンチマークの足場作り、CI およびドキュメントの編集、原稿の推敲および校正、レビュー、引用の検証、ならびに結果の要約に GPT-5 を使用した OpenAI Codex が利用された。また、Codex を通じて GPT-6 も 9月の原稿レビューおよび改訂を支援した。本原稿の準備において、その他の生成 AI ツールは使用されていない。GitHub Copilot は、本リリースのコミット作成を支援した。Google Gemini(Flash バージョン 3.5 から 3.8)は、コードベースの検証を支援した。人間の著者自身がすべての支援出力をレビュー、編集、および検証し、主要な設計判断を行い、関連する検証コマンドおよびベンチマークコマンドを実行した。著者は、提出された資料の正確性、独創性、ライセンス、および倫理的コンプライアンスについて引き続き全責任を負う。
# Acknowledgements
# 謝辞
著者は、プロジェクトの貢献者、ユーザー、ならびに PouchDB、CouchDB、および Trystero のアップストリームメンテナーに感謝の意を表する。本プロジェクトは、GitHub Sponsors を通じたコミュニティーの支援、JetBrains からの開発ツールライセンス、および OpenAI の Codex for Open Source プログラムによる支援を受けている。
著者は、プロジェクトの貢献者、ユーザー、ならびに PouchDB、CouchDB、および Trystero のアップストリームメンテナーに感謝の意を表する。本プロジェクトは、GitHub Sponsors を通じたコミュニティの支援、JetBrains からの開発ツールライセンス、および OpenAI の Codex for Open Source プログラムによる支援を受けている。
# References
# 参考文献
@@ -3,10 +3,18 @@
* exercised. These keys remain application-owned and must not be added to
* Commonlib merely to make them available to the LiveSync translator.
*
* Move a message to the YAML catalogue when it is ready for translation, and
* remove it from this map in the same change.
* Japanese overrides may live in liveSyncProvisionalJapaneseMessages while a
* key remains provisional. Move a message to the YAML catalogue when its wording
* is ready for the full translation workflow, and remove it from both maps in
* the same change.
*/
export const liveSyncProvisionalEnglishMessages = {
"Enable SyncGuard": "Enable SyncGuard",
"Enable device-local SyncGuard protection during synchronisation.":
"Enable device-local SyncGuard protection during synchronisation.",
"SyncGuard minimum files": "SyncGuard minimum files",
"Minimum number of Vault files required for SyncGuard protection to apply.":
"Minimum number of Vault files required for SyncGuard protection to apply.",
"Synchronisation protection": "Synchronisation protection",
"Synchronising your Vault": "Synchronising your Vault",
"Receiving changes from the server…": "Receiving changes from the server…",
@@ -415,6 +423,451 @@ export const liveSyncProvisionalEnglishMessages = {
"The fetch flag does not contain one supported selection. Choose new options and continue with the current remote?\n\nCurrent remote: ${REMOTE}\n\nThe flag contents will remain unchanged. The new choices apply only to this attempt; another restart will ask again. Completing or cancelling the fetch removes the flag.":
"The fetch flag does not contain one supported selection. Choose new options and continue with the current remote?\n\nCurrent remote: ${REMOTE}\n\nThe flag contents will remain unchanged. The new choices apply only to this attempt; another restart will ask again. Completing or cancelling the fetch removes the flag.",
"Choose new options and continue": "Choose new options and continue",
"Batch database updates in LiveSync mode": "Batch database updates in LiveSync mode",
"Wait for a bounded local quiet period before processing file updates in LiveSync mode, reducing intermediate database updates while delaying their reflection to remote devices.":
"Wait for a bounded local quiet period before processing file updates in LiveSync mode, reducing intermediate database updates while delaying their reflection to remote devices.",
Apply: "Apply",
Add: "Add",
Revert: "Revert",
"Vault history": "Vault history",
"From:": "From:",
"To:": "To:",
"Info:": "Info:",
"To stop the countdown, tap anywhere on the dialogue": "To stop the countdown, tap anywhere on the dialogue",
} as const;
export type LiveSyncProvisionalMessageKey = keyof typeof liveSyncProvisionalEnglishMessages;
/** Japanese translations for provisional LiveSync messages which are already user-facing. */
export const liveSyncProvisionalJapaneseMessages: Partial<Record<LiveSyncProvisionalMessageKey, string>> = {
"Enable SyncGuard": "SyncGuard を有効化",
"Enable device-local SyncGuard protection during synchronisation.":
"同期中に、このデバイスで SyncGuard による保護を有効にします。",
"SyncGuard minimum files": "SyncGuard を適用する最小ファイル数",
"Minimum number of Vault files required for SyncGuard protection to apply.":
"SyncGuard による保護を適用するために必要な Vault 内の最小ファイル数です。",
"Synchronisation protection": "同期保護",
"Synchronising your Vault": "Vault を同期しています",
"Receiving changes from the server…": "サーバーから変更を受信しています…",
"Applying received changes to local notes…": "受信した変更をローカルノートに適用しています…",
"Synchronisation was interrupted. You can continue locally, but some notes may be out of date.":
"同期が中断されました。ローカルで作業を続けられますが、一部のノートは最新でない可能性があります。",
"At least ${COUNT} changed files received": "変更されたファイルを少なくとも ${COUNT} 件受信しました",
"Continue without waiting": "待たずに続行",
"Hidden File Sync is disabled on this device. Enable it in Sync Settings before using Automatic or initialising hidden files.":
"このデバイスでは非表示ファイル同期が無効です。Automatic を使用する場合や非表示ファイルを初期化する場合は、同期設定で有効にしてください。",
"Hidden File Sync initialisation was not started.": "非表示ファイル同期は初期化されませんでした。",
"Unknown platform": "不明なプラットフォーム",
"This removes only this device's record from the remote.":
"リモートから削除されるのは、このデバイスの記録のみです。",
"This remote is unlocked. A device that returns can register again.":
"このリモートのロックは解除されています。再接続したデバイスは再登録できます。",
"This remote is locked. Use the existing recovery process for devices that need to join again.":
"このリモートはロックされています。再参加が必要なデバイスには、既存の復旧手順を使用してください。",
"This remote is locked. If the device returns, use the existing recovery process to join it again.":
"このリモートはロックされています。デバイスが再接続した場合は、既存の復旧手順で再参加させてください。",
"This remote does not support conditional updates, so the device record could not be removed.":
"このリモートは条件付き更新に対応していないため、デバイスの記録を削除できませんでした。",
"This remote does not currently have a record of joined devices.":
"このリモートには参加済みデバイスの記録がありません。",
"This list shows device information recorded by the remote. It does not show whether a device is online now.":
"この一覧にはリモートに記録されたデバイス情報が表示されます。デバイスが現在オンラインかどうかは示しません。",
"This device has been locked out. Use the existing recovery process to join again.":
"このデバイスは接続を拒否されています。再参加するには、既存の復旧手順を使用してください。",
"This device": "このデバイス",
"This remote has been cleaned. Use the existing recovery process to join devices again as needed.":
"このリモートはクリーンアップされました。必要に応じて、既存の復旧手順でデバイスを再参加させてください。",
"The device record was forgotten.": "デバイスの記録を削除しました。",
"The device record was already absent.": "デバイスの記録はすでにありません。",
"The device list changed while you were trying to forget this device. Refresh the list before trying again.":
"このデバイスの記録を削除しようとしている間に一覧が更新されました。もう一度試す前に一覧を再読み込みしてください。",
"The device information could not be loaded. Close this dialogue and open Joined devices again if the remote configuration has changed.":
"デバイス情報を読み込めませんでした。リモート設定を変更した場合は、このダイアログを閉じて「参加済みデバイス」を開き直してください。",
"Review device records for the active CouchDB or Object Storage connection.":
"アクティブな CouchDB またはオブジェクトストレージ接続のデバイス記録を確認します。",
Refresh: "更新",
"Plug-in version": "プラグインのバージョン",
Platform: "プラットフォーム",
"Older Self-hosted LiveSync versions detected: ${versions}.":
"古いバージョンの Self-hosted LiveSync が検出されました: ${versions}。",
"Newer Self-hosted LiveSync versions detected: ${versions}.":
"新しいバージョンの Self-hosted LiveSync が検出されました: ${versions}。",
"Reviewing and aligning Self-hosted LiveSync versions across devices is recommended.":
"各デバイスの Self-hosted LiveSync のバージョンを確認し、揃えることをおすすめします。",
Open: "開く",
"Not recorded": "未記録",
"Not accepted": "未承認",
"No joined devices are recorded on this remote.": "このリモートには参加済みデバイスが記録されていません。",
"Locked out": "接続拒否",
"Loading joined devices…": "参加済みデバイスを読み込んでいます…",
"Last recorded": "最終記録日時",
"Joined devices could not be updated. Refresh the list and try again. If the remote configuration has changed, close this dialogue and open Joined devices again.":
"参加済みデバイスを更新できませんでした。一覧を再読み込みしてもう一度お試しください。リモート設定を変更した場合は、このダイアログを閉じて「参加済みデバイス」を開き直してください。",
"Joined devices are available after database startup with an active CouchDB or Object Storage connection.":
"参加済みデバイスは、データベースの起動後に CouchDB またはオブジェクトストレージへの接続が有効になると表示されます。",
"Joined devices": "参加済みデバイス",
"Review devices": "デバイスを確認",
"Synchronisation between different plug-in versions is generally safe unless the connection is explicitly blocked. However, it may be less efficient or use more storage, and P2P synchronisation may be unavailable.":
"プラグインのバージョンが異なるデバイス間でも、接続が明示的にブロックされていなければ、通常は安全に同期できます。ただし、効率が低下したり、使用容量が増えたりする場合があります。また、P2P 同期を利用できないことがあります。",
"Forgetting…": "記録を削除しています…",
"Forgetting the record does not delete Vault files or revoke credentials.":
"記録を削除しても、Vault 内のファイルは削除されず、認証情報も無効になりません。",
"Forget this device record?": "このデバイスの記録を削除しますか?",
"Forget device": "デバイスの記録を削除",
Close: "閉じる",
Cancel: "キャンセル",
"App version": "アプリのバージョン",
Accepted: "承認済み",
"Preparing follow-up report…": "追加調査用レポートを準備しています…",
"Cancelling local search. Checked ${COUNT} entries.":
"ローカル検索をキャンセルしています。${COUNT}件を確認しました。",
"Search current Vault paths and current local Metadata only. Older history and remote-only files are not searched.":
"現在の Vault パスとローカルのメタデータのみを検索します。古い履歴やリモートにのみ存在するファイルは検索しません。",
"The lookup does not read file contents or contact a remote database. Follow-up reports include current conflicts and merge bases.":
"検索でファイルの内容を読み取ったり、リモートデータベースに接続したりすることはありません。追加調査用レポートには、現在の競合とマージベースが含まれます。",
"Local file diagnostic report": "ローカルファイル診断レポート",
"Checked ${COUNT} local entries.": "ローカルの項目を ${COUNT} 件確認しました。",
"Enter a valid report path pseudonym.": "有効なレポート用パス仮名を入力してください。",
"Search cancelled. Matches found so far are shown.":
"検索をキャンセルしました。ここまでに見つかった一致項目を表示します。",
"No matches were found in current Vault paths or current local Metadata. Older history and remote-only files are not searched.":
"現在の Vault パスとローカルのメタデータに一致する項目はありません。古い履歴やリモートにのみ存在するファイルは検索していません。",
"Found ${COUNT} local matches.": "ローカルで ${COUNT} 件見つかりました。",
"The local search could not finish.": "ローカル検索を完了できませんでした。",
"The file could not be opened.": "ファイルを開けませんでした。",
"The follow-up report could not be prepared.": "追加調査用レポートを作成できませんでした。",
"Report path pseudonym": "レポート用パス仮名",
Source: "ソース",
Metadata: "メタデータ",
Vault: "Vault",
"Metadata document ID": "メタデータドキュメント ID",
"No current Metadata document": "現在のメタデータドキュメントなし",
"Current Vault file": "現在の Vault ファイル",
Present: "あり",
Missing: "なし",
"Open file": "ファイルを開く",
"Prepare follow-up report": "追加調査用レポートを作成",
"This report uses pseudonyms. Review its coverage before sharing.":
"このレポートでは仮名を使用します。共有する前に、その対象範囲を確認してください。",
"Back to matches": "検索結果に戻る",
"Reading revisions": "リビジョンを読み込んでいます",
"Calculating database usage": "データベース使用量を計算しています",
"Discovering revisions": "リビジョンを確認しています",
"Find a file from a report pseudonym": "レポート用の仮名からファイルを検索",
"Find matching paths in this Vault and local Metadata, then prepare a follow-up report using the same pseudonyms.":
"この Vault とローカルのメタデータから一致するパスを検索し、同じ仮名を使って追加調査用レポートを作成します。",
"Find file": "ファイルを検索",
"⚠️ Initialise or overwrite the remote": "⚠️ リモートを初期化または上書き",
"🔗 Join this device": "🔗 このデバイスを参加させる",
"⚙️ Apply settings only (advanced)": "⚙️ 設定のみ適用(高度な設定)",
"Configure TURN when a direct connection cannot be established or when you select TURN relay only.":
"直接接続を確立できない場合、または TURN リレーのみを選択する場合は、TURN を設定してください。",
"TURN configuration": "TURN 設定",
Manual: "手動",
"Managed (Cloudflare)": "マネージド(Cloudflare)",
"TURN Key ID": "TURN キー ID",
"TURN Key API Token": "TURN キー API トークン",
"Unsupported TURN configuration": "未対応の TURN 設定",
"The API token is saved with this profile and included in Setup URI and QR code sharing. Temporary TURN credentials are kept in memory only.":
"API トークンはこのプロファイルに保存され、セットアップ URI や QR コードでの共有にも含まれます。一時的な TURN 認証情報はメモリー上にのみ保持されます。",
"TURN relay only requires a TURN server or a configured credential source under Advanced Settings.":
"TURN リレーのみを使うには、TURN サーバーまたは高度な設定で認証情報の取得元を設定してください。",
"Enter a TURN Key ID.": "TURN キー ID を入力してください。",
"TURN Key ID contains unsupported characters.": "TURN キー ID に未対応の文字が含まれています。",
"Enter a TURN Key API Token.": "TURN キー API トークンを入力してください。",
"TURN Key API Token must use Bearer token syntax.":
"TURN キー API トークンは Bearer トークン形式で入力してください。",
"The selected TURN configuration is not supported.": "選択した TURN 設定には対応していません。",
"Setup Complete: Preparing to Fetch from Another Device": "セットアップ完了: 別のデバイスから取得する準備中",
"The P2P connection has been configured successfully. The initial synchronisation data must now be fetched from an online source device.":
"P2P 接続を設定しました。初回の同期を行うため、オンラインのソースデバイスからデータを取得してください。",
"After restarting, select an online source device for the initial Fetch. The local LiveSync database on this device will be rebuilt from that source. Unsynchronised files in this Vault may conflict with the fetched data.":
"再起動後、初回取得のソースとなるオンラインデバイスを選択してください。このデバイスのローカル LiveSync データベースは、そのソースから再構築されます。この Vault で未同期のファイルは、取得したデータと競合する可能性があります。",
"Restart this device, then choose the source device when P2P Rebuild opens.":
"このデバイスを再起動してください。P2P 再構築が開いたら、ソースデバイスを選択します。",
"Restart and Select Source Device": "再起動してソースデバイスを選択",
"P2P Status pane": "P2P ステータスペイン",
"No central data-storage server is required, but a signalling relay is required for peer discovery. Both devices must be online at the same time. Vault data travels through the encrypted P2P connection, not through the signalling relay. Some features may be limited.":
"中央データストレージサーバーは不要ですが、ピアの検出にはシグナリングリレーが必要です。両方のデバイスが同時にオンラインである必要があります。Vault のデータは暗号化された P2P 接続を通り、シグナリングリレーを経由しません。一部の機能は制限される場合があります。",
"P2P requires no central data-storage server, but it still uses a signalling relay for peer discovery.":
"P2P には中央データストレージサーバーは不要ですが、ピアの検出にはシグナリングリレーを使用します。",
"Signalling relay URLs": "シグナリングリレー URL",
"Peer discovery uses Nostr-compatible signalling relays.":
"ピアの検出には Nostr 互換のシグナリングリレーを使用します。",
"Use the project's public signalling relay": "プロジェクトの公開シグナリングリレーを使用",
"The project's public signalling relay is a best-effort convenience operated by the project author. It does not store Vault contents, but signalling metadata may be visible to the relay. Availability and log retention are not guaranteed. You can replace it with your own Nostr-compatible relay.":
"プロジェクトの公開シグナリングリレーは、プロジェクト作者が便宜上、可能な範囲で運用しています。Vault の内容は保存されませんが、シグナリングのメタデータはリレーから見える場合があります。稼働状況やログの保存期間は保証されません。独自の Nostr 互換リレーに変更できます。",
"Learn more about P2P connections": "P2P 接続について詳しく見る",
"Learn more about signalling and TURN": "シグナリングと TURN について詳しく見る",
"WebRTC encrypts data between your devices, including when it passes through TURN. The TURN provider cannot read the transferred data. It can see network addresses and traffic volume.":
"TURN を経由する場合も含め、WebRTC はデバイス間のデータを暗号化します。TURN プロバイダーは転送データを読み取れませんが、ネットワークアドレスと通信量を確認できます。",
"Connection compatibility": "接続互換性",
"P2P message size": "P2P メッセージサイズ",
Standard: "標準",
Reduced: "縮小",
Conservative: "保守的",
"Maximum compatibility": "互換性を最大化",
"Smaller messages can improve compatibility on paths which fragment or drop larger WebRTC messages. This setting limits outgoing P2P messages, so use a compatible profile on each sending device when required.":
"メッセージを小さくすると、大きな WebRTC メッセージが分割または破棄される経路で互換性が向上する場合があります。この設定は送信する P2P メッセージのサイズを制限します。必要に応じて、送信側の各デバイスで互換性のあるプロファイルを選択してください。",
"Connection path": "接続経路",
"TURN relay only": "TURN リレーのみ",
"TURN relay only is available when at least one valid TURN server URL is configured under Advanced Settings.":
"高度な設定で有効な TURN サーバー URL を1つ以上設定すると、TURN リレーのみを選択できます。",
"TURN relay only requires at least one valid TURN server URL. Connection path has been restored to Automatic.":
"TURN リレーのみを使うには、有効な TURN サーバー URL が1つ以上必要です。接続経路を自動に戻しました。",
"Announce changes": "変更を通知",
"Announce changes automatically after connecting": "接続後に変更を自動通知",
"When enabled, this device notifies connected peers after a local change. The notification contains no Vault data; a peer which follows this device then fetches the change through the encrypted P2P connection.":
"有効にすると、ローカルで変更があったときに接続中のピアへ通知します。通知に Vault のデータは含まれません。このデバイスの変更を受け取る設定のピアは、暗号化された P2P 接続を通じて変更を取得します。",
"Stop announcing changes": "変更の通知を停止",
"Start announcing changes": "変更の通知を開始",
"Follow changes": "変更を受け取る",
"Stop following changes from this device": "このデバイスからの変更受信を停止",
"Follow changes from this device": "このデバイスからの変更を受信",
"Synchronise when this device connects": "このデバイスの接続時に同期",
"Follow whenever this device connects": "このデバイスが接続するたびに変更を受信",
"Include in the P2P synchronisation command": "P2P 同期コマンドに含める",
"More actions for ${DEVICE}": "${DEVICE} のその他の操作",
"Use this device's settings": "このデバイスの設定を使用",
Retry: "再試行",
"No Synchronisation Settings Found": "同期設定が見つかりません",
"The selected remote has no saved synchronisation settings. This is normal for a new remote. Use this device's settings, or cancel if you expected existing settings.":
"選択したリモートには保存済みの同期設定がありません。新しいリモートでは通常の状態です。このデバイスの設定を使用してください。既存の設定があるはずの場合はキャンセルしてください。",
"Could Not Read Synchronisation Settings": "同期設定を読み取れませんでした",
"Could not read the remote's synchronisation settings. Check the connection and credentials, then retry.":
"リモートの同期設定を読み取れませんでした。接続と認証情報を確認してから、もう一度お試しください。",
"Could not read the remote's synchronisation settings. Retry, or continue the overwrite with this device's settings. A working connection is still required.":
"リモートの同期設定を読み取れませんでした。再試行するか、このデバイスの設定で上書きを続行してください。続行する場合も、接続が機能している必要があります。",
"Skips checking and applying synchronisation settings from the remote.":
"リモートの同期設定の確認と適用をスキップします。",
"Change CouchDB server setting": "CouchDB サーバー設定を変更",
"Change CouchDB server setting '${SETTING}' to '${VALUE}'?":
"CouchDB サーバー設定「${SETTING}」を「${VALUE}」に変更しますか?",
"TURN relay only requires TURN configuration. Connection path has been restored to Automatic.":
"TURN リレーのみを使うには TURN の設定が必要です。接続経路を自動に戻しました。",
"This file has unresolved conflicts.": "このファイルには未解決の競合があります。",
"This file has ${COUNT} unresolved versions. They will be reviewed one pair at a time.":
"このファイルには未解決の版が ${COUNT} 件あります。1組ずつ確認します。",
"Sync now": "今すぐ同期",
"Apply pending changes now": "保留中の変更を今すぐ適用",
"Copy database information for the active file": "アクティブなファイルのデータベース情報をコピー",
"Copy database information for a file": "ファイルのデータベース情報をコピー",
"Copy revision, conflict, and local chunk availability information, including document and chunk identifiers but not file contents.":
"ファイルの内容を含めずに、ドキュメント ID やチャンク ID などのリビジョン、競合、ローカルのチャンク有無に関する情報をコピーします。",
"Choose file": "ファイルを選択",
"Choose a file to inspect": "確認するファイルを選択",
"Database information for ${FILE}": "${FILE} のデータベース情報",
"All revisions and chunk availability below are a snapshot of this device's local database; the remote is not queried. Review the Vault-relative path, document identifier, content-derived chunk identifiers, and metadata before sharing this report. File contents are omitted.":
"以下のリビジョンとチャンク有無は、このデバイスのローカルデータベースのスナップショットです。リモートには問い合わせません。このレポートを共有する前に、Vault からの相対パス、ドキュメント ID、コンテンツ由来のチャンク ID、メタデータを確認してください。ファイルの内容は含まれません。",
"📁 Vault: ${SIZE} B · ${TIME}": "📁 Vault: ${SIZE} B · ${TIME}",
"📁 Vault: missing": "📁 Vault: ファイルなし",
"🗄️ Local DB: missing": "🗄️ ローカル DB: データなし",
"Vault and database revision": "Vault とデータベースのリビジョン",
"Vault file": "Vault ファイル",
"Database revision": "データベースのリビジョン",
"Vault file is newer": "Vault ファイルの方が新しい",
"Database revision is newer": "データベースのリビジョンの方が新しい",
"Within the two-second comparison window": "2秒以内の誤差範囲内",
"Timestamp comparison unavailable": "タイムスタンプを比較できません",
"${ROLE}: ${REVISION}": "${ROLE}: ${REVISION}",
"Winner revision": "代表リビジョン",
"Conflict revision": "競合リビジョン",
"Unknown revision": "不明なリビジョン",
"🗑️ Logical deletion": "🗑️ 論理削除",
"Readable on this device; recorded size ${RECORDED}, decoded size ${ACTUAL}":
"このデバイスで読み取り可能。記録サイズ ${RECORDED}、デコード後サイズ ${ACTUAL}",
"🧩 Missing chunks: ${COUNT}": "🧩 チャンク不足: ${COUNT} 件",
"📦 DB: recorded ${RECORDED} B · decoded ${DECODED} B · Δsize ${DIFFERENCE} B":
"📦 DB: 記録 ${RECORDED} B · デコード後 ${DECODED} B · サイズ差 ${DIFFERENCE} B",
"📦 DB: recorded ${RECORDED} B · decoded unavailable": "📦 DB: 記録 ${RECORDED} B · デコード後サイズは不明",
"📁 Vault: ${VAULT} B · Δsize vs DB ${DIFFERENCE} B": "📁 Vault: ${VAULT} B · DB とのサイズ差 ${DIFFERENCE} B",
"🕒 DB ${DATABASE_TIME} · Vault ${VAULT_TIME} · Δtime ${DIFFERENCE} ms (${RELATION})":
"🕒 DB ${DATABASE_TIME} · Vault ${VAULT_TIME} · 時刻差 ${DIFFERENCE} ms(${RELATION})",
"✅ Matches Vault": "✅ Vault と一致",
"⚠️ Differs from Vault": "⚠️ Vault と不一致",
"✅ Vault matches winner": "✅ Vault は代表リビジョンと一致",
"⚠️ Conflicts: ${COUNT}": "⚠️ 競合: ${COUNT} 件",
"Compare with Vault": "Vault と比較",
"Apply this revision to Vault": "このリビジョンを Vault に適用",
"Apply database revision ${REVISION} to ${FILE}? The current Vault file will be overwritten.":
"データベースのリビジョン ${REVISION} を ${FILE} に適用しますか?現在の Vault ファイルは上書きされます。",
"Apply database revision to Vault": "データベースのリビジョンを Vault に適用",
"Mark this revision as the Vault version": "このリビジョンを Vault の版として指定",
"Store Vault file as a child of this revision": "Vault ファイルをこのリビジョンの子として保存",
"Apply logical deletion to Vault": "論理削除を Vault に適用",
"Apply logical deletion ${REVISION} to ${FILE}? The current Vault file will be removed.":
"論理削除 ${REVISION} を ${FILE} に適用しますか?現在の Vault ファイルは削除されます。",
"Retry reading revision": "リビジョンの読み込みを再試行",
"Discard this branch": "このブランチを破棄",
"Discard branch": "ブランチを破棄",
"Discard database branch ${REVISION} of ${FILE}? This creates a logical deletion for that exact live branch. The current Vault file will not be changed.":
"${FILE} のデータベースブランチ ${REVISION} を破棄しますか?この操作は該当する現行ブランチに論理削除を記録します。現在の Vault ファイルは変更されません。",
"Discard unreadable revision": "読み取れないリビジョンを破棄",
"Discard database revision ${REVISION} of ${FILE}? This creates a logical deletion for that exact live revision. Missing content cannot be recovered by this action.":
"${FILE} のデータベースリビジョン ${REVISION} を破棄しますか?この操作は該当する現行リビジョンに論理削除を記録します。不足している内容はこの操作では復元できません。",
"Revision metadata is unavailable on this device": "このデバイスではリビジョンのメタデータを利用できません",
"Shared ancestor ${REVISION} is not readable on this device. Automatic three-way merging may be unavailable, but the live revisions remain available for explicit review.":
"共有祖先 ${REVISION} はこのデバイスで読み取れません。自動3方向マージを利用できない場合がありますが、現行リビジョンは引き続き個別に確認できます。",
"No shared ancestor is available for this conflict. The live revisions remain available for explicit review.":
"この競合には共有祖先がありません。現行リビジョンは引き続き個別に確認できます。",
"More actions for revision ${REVISION}": "リビジョン ${REVISION} のその他の操作",
"More actions for ${FILE}": "${FILE} のその他の操作",
"Show revision history": "リビジョン履歴を表示",
"Store Vault file as a new local database document":
"Vault ファイルを新しいローカルデータベースドキュメントとして保存",
"Copy database information": "データベース情報をコピー",
"Recreate chunks for current Vault files": "現在の Vault ファイルのチャンクを再作成",
"Recreate chunks from the files currently present in this Vault. This cannot reconstruct unavailable historical or conflict content.":
"この Vault に現在あるファイルからチャンクを再作成します。利用できない過去の内容や競合内容は復元できません。",
"Recreate current chunks": "現在のチャンクを再作成",
"Resolve every conflict by modification time? This logically deletes every version except the newest one and cannot recover content which is already unavailable.":
"すべての競合を更新日時で解決しますか?最も新しい版以外はすべて論理削除され、すでに利用できない内容は復元できません。",
"Resolve all conflicts by the newest version": "すべての競合を最も新しい版で解決",
"Inspect conflicts and file/database differences": "競合とファイル/データベース間の差異を調査",
"Scan Vault files and local-database Metadata for conflicts, missing chunks, identity mismatches, and differences. Each result provides actions for one exact entry or revision.":
"Vault ファイルとローカルデータベースのメタデータをスキャンし、競合、チャンク不足、識別情報の不一致、差異を確認します。各結果から、該当する項目またはリビジョンに対する操作を選択できます。",
"Begin inspection": "調査を開始",
"Metadata entry requires review and was left unchanged": "確認が必要なメタデータ項目は変更せずに残しました",
"The stored document ID does not match the ID derived from its recorded path.":
"保存されたドキュメント ID が、記録されたパスから導出される ID と一致しません。",
"The stored document ID and recorded path are handled by different synchronisation features.":
"保存されたドキュメント ID と記録されたパスは、それぞれ異なる同期機能によって処理されます。",
"Stored document ID: ${ID}": "保存されたドキュメント ID: ${ID}",
"Expected document ID: ${ID}": "期待されるドキュメント ID: ${ID}",
"Source revision: ${REVISION}": "ソースリビジョン: ${REVISION}",
"One-step repair is unavailable because this entry is ambiguous, no longer current, or unsafe to change.":
"この項目は情報があいまいであるか、最新ではないか、または変更が安全ではないため、ワンステップ修復は利用できません。",
"An exact target is already present; repair can remove the obsolete ID.":
"同一の対象がすでに存在するため、修復時に古い ID を削除できます。",
"Repair is available for this entry.": "この項目は修復できます。",
"Repair this Metadata document ID": "このメタデータドキュメント ID を修復",
"Repair Metadata ID": "メタデータ ID を修復",
"Keep unchanged": "変更せずに保持",
"This moves one local Metadata entry to the ID derived from its recorded path.\n\n**File:** `${FILE}` \n**Source:** `${SOURCE}@${REVISION}` \n**Target:** `${TARGET}`\n\nThe target is verified before the source is removed. Its CouchDB revision ancestry cannot be preserved.\n\n> [!warning] Before repairing\n> - Back up this device.\n> - If file-name case or path obfuscation was intentionally changed for the whole database, use Rebuild instead.\n> - If other devices share this database, pause them, allow this device to upload the repair, then resume them one at a time.":
"ローカルのメタデータ項目を、記録されたパスから導出される ID に移動します。\n\n**ファイル:** `${FILE}` \n**移動元:** `${SOURCE}@${REVISION}` \n**移動先:** `${TARGET}`\n\n移動元を削除する前に、移動先を検証します。CouchDB のリビジョン祖先は保持できません。\n\n> [!warning] 修復する前に\n> - このデバイスをバックアップしてください。\n> - データベース全体のファイル名の大文字・小文字の扱いやパス難読化を意図的に変更した場合は、代わりに再構築してください。\n> - 他のデバイスがこのデータベースを共有している場合は、それらを一時停止し、このデバイスから修復内容がアップロードされた後、1台ずつ再開してください。",
"Metadata document ID repair and the ordinary Vault scan completed. Run this inspection again after synchronisation.":
"メタデータドキュメント ID の修復と通常の Vault スキャンが完了しました。同期後にこの調査を再実行してください。",
"Metadata document ID repair completed, but the ordinary Vault scan did not run. Keep synchronisation paused, resolve the scan condition, then run 'Scan storage and database again'.":
"メタデータドキュメント ID の修復は完了しましたが、通常の Vault スキャンは実行されませんでした。同期を一時停止したままスキャンを妨げている状態を解消し、「ストレージとデータベースを再スキャン」を実行してください。",
"The inspected state changed. No repair was performed; run inspection again.":
"調査対象の状態が変化しました。修復は行われていません。調査を再実行してください。",
"Repair stopped after creating the target. The source was retained. Run inspection again before retrying.":
"移動先を作成した後、修復を停止しました。移動元は保持されています。再試行する前に調査を実行してください。",
"Repair failed before the source was removed. Run inspection again before retrying.":
"移動元を削除する前に修復が失敗しました。再試行する前に調査を実行してください。",
"(RegExp) Limit normal files to matching paths. Leave empty to allow all normal file paths. Hidden File Sync uses its own target patterns.":
"(正規表現)通常ファイルを一致するパスに限定します。空欄にすると、すべての通常ファイルパスが対象になります。非表示ファイル同期には専用の対象パターンを使用します。",
"(RegExp) Skip changes to matching normal and hidden files, locally and remotely.":
"(正規表現)一致する通常ファイルと非表示ファイルの変更を、ローカルとリモートの両方でスキップします。",
"Repair Metadata document ID": "メタデータドキュメント ID を修復",
"Connection settings": "接続設定",
"Saved connections": "保存済みの接続",
"ID generation": "ID の生成",
"Keep current configuration": "現在の設定を維持",
"Set an ID key": "ID キーを設定",
"Current configuration: a saved ID key is used.": "現在の設定: 保存済みの ID キーを使用します。",
"Current configuration: the saved ID key is retained while E2EE is off.":
"現在の設定: E2EE が無効な間も保存済みの ID キーを保持します。",
"Current configuration: no ID key is saved. With E2EE enabled, keeping it uses legacy IDs tied to the E2EE passphrase.":
"現在の設定: ID キーは保存されていません。E2EE が有効な場合、現在の設定を維持すると E2EE パスフレーズに紐づく旧形式の ID を使用します。",
"Changing the E2EE passphrase changes IDs generated by the legacy configuration.":
"旧形式の設定では、E2EE パスフレーズを変更すると生成される ID も変わります。",
"This uses a saved key for new Chunk IDs and obfuscated Metadata document IDs, so changing the E2EE passphrase does not derive a new key automatically.":
"新しいチャンク ID と難読化されたメタデータドキュメント ID に保存済みのキーを使います。そのため、E2EE パスフレーズを変更しても新しいキーは自動導出されません。",
Configured: "設定済み",
"The saved ID key is configured. Its source cannot be shown again.":
"保存済みの ID キーが設定されています。キーの導出元を再表示することはできません。",
"Leave this input empty to keep the saved ID key.":
"保存済みの ID キーを維持するには、この欄を空のままにしてください。",
"Generate a random ID key": "ランダムな ID キーを生成",
"How to set the ID key": "ID キーの設定方法",
"Derive from current E2EE passphrase": "現在の E2EE パスフレーズから導出",
"Enter an ID source": "ID の導出元を入力",
"Import an ID recovery code": "ID 復旧コードをインポート",
"ID source": "ID の導出元",
"ID recovery code": "ID 復旧コード",
"Enter an ID recovery code": "ID 復旧コードを入力",
"Choose a long, unpredictable source. It is used once and cannot be shown again after saving. A recovery code can be displayed on this device later. This input also accepts a tagged recovery code.":
"長く推測されにくい導出元を指定してください。導出元は一度だけ使用され、保存後は再表示できません。復旧コードは後からこのデバイスで表示できます。この入力欄にはタグ付き復旧コードも指定できます。",
"Paste a tagged recovery code from an existing device to restore the same ID key.":
"既存のデバイスからタグ付き復旧コードを貼り付けると、同じ ID キーを復元できます。",
"For recovery after losing every device, save the recovery code after setup or choose an ID source you can reproduce.":
"すべてのデバイスを失った場合に備え、セットアップ後に復旧コードを保存するか、再現可能な ID の導出元を選んでください。",
"Show current recovery code": "現在の復旧コードを表示",
"Hide current recovery code": "現在の復旧コードを隠す",
"Current ID recovery code": "現在の ID 復旧コード",
"Copy recovery code": "復旧コードをコピー",
"Recovery code copied.": "復旧コードをコピーしました。",
"The displayed recovery code belongs to the current key. Reopen this dialogue after saving to copy the replacement key.":
"表示中の復旧コードは現在のキーに対応しています。キーを置き換えた場合は、保存後にこのダイアログを開き直して新しい復旧コードをコピーしてください。",
"The recovery code could not be copied. Select and copy the visible code instead.":
"復旧コードをコピーできませんでした。表示されているコードを選択してコピーしてください。",
"The ID key is derived from the current E2EE passphrase and saved separately. Changing that passphrase later does not change the saved ID key. To reduce the risk of guessing that passphrase from known IDs, use a separate, unpredictable ID source instead.":
"ID キーは現在の E2EE パスフレーズから導出され、別に保存されます。後からパスフレーズを変更しても、保存済みの ID キーは変わりません。既知の ID からパスフレーズを推測されるリスクを下げるには、別の推測されにくい ID の導出元を使用してください。",
"An ID source is required to enable this option.": "このオプションを有効にするには、ID の導出元が必要です。",
"The ID source or recovery code is invalid. Check it and try again.":
"ID の導出元または復旧コードが無効です。確認してもう一度お試しください。",
"Read normal-file Metadata and current conflicts locally and in CouchDB, then compare content for suspect groups. Choose original names or pseudonyms before starting.":
"ローカルと CouchDB の通常ファイルのメタデータおよび現在の競合を読み取り、問題が疑われる項目の内容を比較します。開始前に、元の名前を使うか仮名を使うか選択してください。",
Inspect: "調査",
"Inspect database integrity": "データベースの整合性を調査",
"Inspection completed. Review the findings and coverage before sharing.":
"調査が完了しました。共有する前に結果と調査範囲を確認してください。",
"Inspection partially completed. Some information could not be checked; review the reported reasons.":
"調査は一部のみ完了しました。確認できなかった情報があります。表示された理由を確認してください。",
"Inspection failed. The report records the available evidence and reasons.":
"調査に失敗しました。レポートには、取得できた証拠と失敗理由が記録されています。",
"Inspection cancelled. The report contains only the evidence collected so far.":
"調査をキャンセルしました。レポートには、ここまでに収集した証拠のみが含まれます。",
"Report copied.": "レポートをコピーしました。",
"Copy failed. Select the report text and copy it manually.":
"コピーに失敗しました。レポートのテキストを選択して手動でコピーしてください。",
"Download requested. You can also copy the report if downloads are unavailable on this device.":
"ダウンロードを要求しました。このデバイスでダウンロードできない場合は、レポートをコピーすることもできます。",
"Pseudonyms include a public salt, so guessed paths can be checked. Original names include private filenames. Reports omit file contents and credentials.":
"仮名には公開ソルトが含まれるため、推測したパスと照合できます。元の名前を選ぶと、非公開のファイル名が含まれます。どちらのレポートにもファイルの内容や認証情報は含まれません。",
"Read available local revisions and report their Chunk references. Settings are left unchanged.":
"利用可能なローカルリビジョンを読み取り、参照するチャンクをレポートします。設定は変更されません。",
"Inspect normal-file Metadata, current conflicts, and suspect content against the Vault. Remote inspection supports CouchDB. Older history is excluded; findings do not authorise deletion.":
"通常ファイルのメタデータ、現在の競合、問題が疑われる内容を Vault と照合します。リモートの調査は CouchDB に対応しています。古い履歴は対象外です。調査結果を根拠に削除が実行されることはありません。",
"Include remote database": "リモートデータベースを含める",
"Names in this report": "レポート内の名前",
"Pseudonyms (recommended)": "仮名(推奨)",
"Original names": "元の名前",
"Cancelling inspection…": "調査をキャンセルしています…",
"Items inspected": "調査した項目数",
"Cancel inspection": "調査をキャンセル",
"This report uses temporary pseudonyms. Counts and relationships remain visible.":
"このレポートでは一時的な仮名を使用します。件数や項目間の関係は表示されます。",
"This report contains original paths and identifiers. Review them before sharing.":
"このレポートには元のパスと識別子が含まれます。共有する前に内容を確認してください。",
"Partial Usage results are available as JSON so their coverage and reasons stay with the rows.":
"一部のみ取得できた使用状況の結果は JSON で出力できます。各行に調査範囲と理由が保持されます。",
"Report format": "レポート形式",
"Diagnostic report": "診断レポート",
"Copy report": "レポートをコピー",
"Save report": "レポートを保存",
"The inspection could not finish. Close this dialogue and try again after database startup.":
"調査を完了できませんでした。このダイアログを閉じ、データベースの起動後にもう一度お試しください。",
"Resume scheduled fetch?": "予約済みの取得を再開しますか?",
"Apply these saved choices to the current remote?\n\nCurrent remote: ${REMOTE}\n\n- ${STAGE1}\n- ${STAGE2}\n\nThe fetch flag may remain after an interruption or an incomplete cleanup. Continue only if this fetch is still intended.":
"保存済みの選択肢を現在のリモートに適用しますか?\n\n現在のリモート: ${REMOTE}\n\n- ${STAGE1}\n- ${STAGE2}\n\n中断や不完全なクリーンアップの後に取得フラグが残っている可能性があります。この取得を実行する意図がある場合にのみ続行してください。",
"Continue with these choices": "この選択肢で続行",
"Choose again": "選び直す",
"Saved fetch choices could not be read": "保存済みの取得設定を読み取れませんでした",
"The fetch flag does not contain one supported selection. Choose new options and continue with the current remote?\n\nCurrent remote: ${REMOTE}\n\nThe flag contents will remain unchanged. The new choices apply only to this attempt; another restart will ask again. Completing or cancelling the fetch removes the flag.":
"取得フラグに有効な選択肢が含まれていません。新しい選択肢を指定して、現在のリモートで続行しますか?\n\n現在のリモート: ${REMOTE}\n\nフラグの内容は変更されません。新しい選択肢は今回の試行にのみ適用され、再起動すると再度確認されます。取得を完了またはキャンセルすると、フラグは削除されます。",
"Choose new options and continue": "新しい選択肢を指定して続行",
"Batch database updates in LiveSync mode": "LiveSync モードでデータベース更新をバッチ処理",
"Wait for a bounded local quiet period before processing file updates in LiveSync mode, reducing intermediate database updates while delaying their reflection to remote devices.":
"LiveSync モードでは、ローカルでの更新が一定時間落ち着くまで待ってからファイル更新を処理します。これにより中間のデータベース更新を減らし、リモートデバイスへの反映を遅らせます。",
Apply: "適用",
Add: "追加",
Revert: "元に戻す",
"Vault history": "Vault 履歴",
"From:": "開始:",
"To:": "終了:",
"Info:": "情報:",
"To stop the countdown, tap anywhere on the dialogue":
"カウントダウンを停止するには、ダイアログの任意の場所をタップしてください",
};
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -521,7 +521,7 @@
"moduleCheckRemoteSize.option800MB": "800MB (Cloudant, fly.io)",
"moduleCheckRemoteSize.optionAskMeLater": "Ask me later",
"moduleCheckRemoteSize.optionDismiss": "Dismiss",
"moduleCheckRemoteSize.optionIncreaseLimit": "increase to ${newMax}MB",
"moduleCheckRemoteSize.optionIncreaseLimit": "Increase to ${newMax} MB",
"moduleCheckRemoteSize.optionNoWarn": "No, never warn please",
"moduleCheckRemoteSize.optionRebuildAll": "Rebuild Everything Now",
"moduleCheckRemoteSize.optionReview": "Review options",
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -679,7 +679,7 @@ moduleCheckRemoteSize:
option800MB: 800MB (Cloudant, fly.io)
optionAskMeLater: Ask me later
optionDismiss: Dismiss
optionIncreaseLimit: increase to ${newMax}MB
optionIncreaseLimit: Increase to ${newMax} MB
optionNoWarn: No, never warn please
optionReview: Review options
optionRebuildAll: Rebuild Everything Now
File diff suppressed because it is too large. Load diff
+6 -1
View File
@@ -12,6 +12,7 @@ import type { AllMessageKeys, I18N_LANGS, LiveSyncCatalogueMessageKey } from "./
import { allMessages } from "./messages/combinedMessages.prod.ts";
import {
liveSyncProvisionalEnglishMessages,
liveSyncProvisionalJapaneseMessages,
type LiveSyncProvisionalMessageKey,
} from "./messages/LiveSyncProvisionalMessages.ts";
@@ -72,7 +73,11 @@ function _getMessage(key: string, lang: I18N_LANGS) {
allMessages[key] ?? (provisionalEnglish === undefined ? undefined : ({ def: provisionalEnglish } as const));
if (msgs === undefined && isCommonlibMessageKey(key)) return englishMessageTranslator(key);
const resolvedLang = resolveLanguage(lang);
let msg = msgs?.[resolvedLang];
let msg =
msgs?.[resolvedLang] ??
(resolvedLang === "ja"
? liveSyncProvisionalJapaneseMessages[key as LiveSyncProvisionalMessageKey]
: undefined);
if (!msg) {
if (missingTranslations.indexOf(key) === -1) {
+1 -1
View File
@@ -18,7 +18,7 @@ describe("LiveSync-owned translation catalogue", () => {
});
it("retains typed placeholder substitution", () => {
expect($msg("moduleCheckRemoteSize.optionIncreaseLimit", { newMax: "800" }, "def")).toBe("increase to 800MB");
expect($msg("moduleCheckRemoteSize.optionIncreaseLimit", { newMax: "800" }, "def")).toBe("Increase to 800 MB");
});
it("keeps the active-file path compatibility warning concise", () => {
@@ -189,9 +189,9 @@
<div class="buttons">
{#if hideLocal}
<button onclick={cancel}>Cancel</button>
<button onclick={cancel}>{translateMessage("Cancel")}</button>
{/if}
<button onclick={apply}>Apply</button>
<button onclick={apply}>{translateMessage("Apply")}</button>
</div>
{/if}
+6 -3
View File
@@ -2,6 +2,7 @@ import { ButtonComponent } from "@/deps.ts";
import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting, Component } from "@/deps.ts";
import { EVENT_PLUGIN_UNLOADED, eventHub } from "@/common/events.ts";
import { compatGlobal, type CompatIntervalHandle } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
import { $msg } from "@/common/translation";
class AutoClosableModal extends Modal {
_closeByUnload = () => {
@@ -58,7 +59,7 @@ export class InputStringDialog extends AutoClosableModal {
new Setting(formEl)
.addButton((btn) =>
btn
.setButtonText("Ok")
.setButtonText($msg("Ok"))
.setCta()
.onClick(() => {
this.isManuallyClosed = true;
@@ -67,7 +68,7 @@ export class InputStringDialog extends AutoClosableModal {
)
.addButton((btn) =>
btn
.setButtonText("Cancel")
.setButtonText($msg("Cancel"))
.setCta()
.onClick(() => {
this.close();
@@ -199,7 +200,9 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
const buttonSetting = new Setting(contentEl);
const labelWrapper = contentEl.createDiv();
labelWrapper.addClass("sls-dialogue-note-wrapper");
const labelEl = labelWrapper.createEl("label", { text: "To stop the countdown, tap anywhere on the dialogue" });
const labelEl = labelWrapper.createEl("label", {
text: $msg("To stop the countdown, tap anywhere on the dialogue"),
});
labelEl.addClass("sls-dialogue-note-countdown");
if (!this.timeout || !this.timer) {
labelWrapper.empty();
@@ -39,6 +39,14 @@ function normaliseRequestBody(body: unknown): string | ArrayBuffer | undefined {
throw new TypeError(`Obsidian requestUrl does not support the request body type ${bodyType}`);
}
function stripWeakEtag(value: string): string {
const trimmed = value.trim();
if (trimmed.startsWith("W/")) {
return trimmed.slice(2).trim();
}
return trimmed;
}
/**
* This is close to origin implementation of FetchHttpHandler
* https://github.com/aws/aws-sdk-js-v3/blob/main/packages/fetch-http-handler/src/fetch-http-handler.ts
@@ -86,7 +94,11 @@ export class ObsHttpHandler extends FetchHttpHandler {
if (keyLower === "host" || keyLower === "content-length") {
continue;
}
transformedHeaders[keyLower] = request.headers[key];
let value = request.headers[key];
if (keyLower === "if-match" && typeof value === "string") {
value = stripWeakEtag(value);
}
transformedHeaders[keyLower] = value;
}
let contentType: string | undefined = undefined;
@@ -110,7 +122,12 @@ export class ObsHttpHandler extends FetchHttpHandler {
const headers = rsp.headers;
const headersLower: Record<string, string> = {};
for (const key of Object.keys(headers)) {
headersLower[key.toLowerCase()] = headers[key];
let value = headers[key];
const lowerKey = key.toLowerCase();
if (lowerKey === "etag" && typeof value === "string") {
value = stripWeakEtag(value);
}
headersLower[lowerKey] = value;
}
const stream = new ReadableStream<Uint8Array>({
start(controller) {
@@ -147,4 +147,48 @@ describe("ObsHttpHandler response handling", () => {
await expect(new ObsHttpHandler().handle(requestWithBody(new ArrayBuffer(0)))).rejects.toBe(failure);
});
it("strips weak ETag prefix from incoming etag response headers", async () => {
requestUrlMock.mockResolvedValue({
headers: { etag: 'W/"weak-revision-123"' },
status: 200,
arrayBuffer: new ArrayBuffer(0),
});
const request = new HttpRequest({
protocol: "https:",
hostname: "objects.example.com",
method: "GET",
path: "/bucket/milestone.json",
headers: {},
});
const result = await new ObsHttpHandler().handle(request);
expect(result.response.headers.etag).toBe('"weak-revision-123"');
});
it("strips weak ETag prefix from outgoing if-match request headers", async () => {
requestUrlMock.mockResolvedValue({
headers: {},
status: 200,
arrayBuffer: new ArrayBuffer(0),
});
const request = new HttpRequest({
protocol: "https:",
hostname: "objects.example.com",
method: "PUT",
path: "/bucket/milestone.json",
headers: {
"if-match": 'W/"weak-revision-123"',
},
});
await new ObsHttpHandler().handle(request);
expect(requestUrlMock).toHaveBeenCalledWith(
expect.objectContaining({
headers: expect.objectContaining({
"if-match": '"weak-revision-123"',
}),
})
);
});
});
@@ -200,14 +200,14 @@
</script>
<div class="globalhistory">
<h1>Vault history</h1>
<h1>{translateMessage("Vault history")}</h1>
<div class="control">
<div class="row">
<label for="">From:</label><input type="date" bind:value={dispDateFrom} disabled={loading} />
<label for="">{translateMessage("From:")}</label><input type="date" bind:value={dispDateFrom} disabled={loading} />
</div>
<div class="row"><label for="">To:</label><input type="date" bind:value={dispDateTo} disabled={loading} /></div>
<div class="row"><label for="">{translateMessage("To:")}</label><input type="date" bind:value={dispDateTo} disabled={loading} /></div>
<div class="row">
<label for="">Info:</label>
<label for="">{translateMessage("Info:")}</label>
<label
><input type="checkbox" bind:checked={showDiffInfo} disabled={loading} /><span
>{translateMessage("Diff")}</span
+1 -1
View File
@@ -58,7 +58,7 @@
<span>{msg("logPane.pause", {}, lang)}</span>
</label>
<span class="spacer"></span>
<button onclick={() => closeDialogue()}>Close</button>
<button onclick={() => closeDialogue()}>{msg("Close", {}, lang)}</button>
</div>
</div>
<div class="log" bind:this={scroll}>
@@ -39,19 +39,19 @@
{/each}
<li>
<label>
<button on:click={() => add()}>Add</button>
<button on:click={() => add()}>{translateMessage("Add")}</button>
</label>
</li>
<li class="buttons">
<button
on:click={() => apply(patterns)}
disabled={statusName.some((e) => e === CHECK_NG) || modified.every((e) => e === "")}
>Apply
>{translateMessage("Apply")}
</button>
<button
on:click={() => revert()}
disabled={statusName.some((e) => e === CHECK_NG) || modified.every((e) => e === "")}
>Revert
>{translateMessage("Revert")}
</button>
</li>
</ul>
@@ -585,7 +585,7 @@ describe("ObsidianLiveSyncSettingTab pending-setting initialisation", () => {
);
expect(loggerMocks.Logger).toHaveBeenCalledWith(
expect.stringContaining(
"Rebuild both databases from a complete authoritative Vault and Fetch on the other devices."
"then Rebuild both databases from a complete authoritative Vault and Fetch on the other devices."
),
LOG_LEVEL_NOTICE
);
@@ -98,7 +98,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
el.setText(`Current adapter: ${useIndexedDBAdapter ? "IndexedDB" : "IDB"}`);
if (!useIndexedDBAdapter) {
setting.addButton((button) => {
button.setButtonText("Switch to IndexedDB").onClick(async () => {
button.setButtonText($msg("Switch to IndexedDB")).onClick(async () => {
Logger("Migrating all data to IndexedDB...", LOG_LEVEL_NOTICE);
await migrateAllToIndexedDB();
Logger(
@@ -109,7 +109,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
});
} else {
setting.addButton((button) => {
button.setButtonText("Switch to IDB").onClick(async () => {
button.setButtonText($msg("Switch to IDB")).onClick(async () => {
Logger("Migrating all data to IDB...", LOG_LEVEL_NOTICE);
await migrateAllToIDB();
Logger(
@@ -253,21 +253,21 @@ export function paneSyncSettings(
new Setting(paneEl)
.setName($msg("obsidianLiveSyncSettingTab.nameEnableHiddenFileSync"))
.addButton((button) => {
button.setButtonText("Merge").onClick(async () => {
button.setButtonText($msg("Merge")).onClick(async () => {
this.closeSetting();
// this.resetEditingSettings();
await this.services.setting.enableOptionalFeature("MERGE");
});
})
.addButton((button) => {
button.setButtonText("Fetch").onClick(async () => {
button.setButtonText($msg("Fetch")).onClick(async () => {
this.closeSetting();
// this.resetEditingSettings();
await this.services.setting.enableOptionalFeature("FETCH");
});
})
.addButton((button) => {
button.setButtonText("Overwrite").onClick(async () => {
button.setButtonText($msg("Overwrite")).onClick(async () => {
this.closeSetting();
// this.resetEditingSettings();
await this.services.setting.enableOptionalFeature("OVERWRITE");
@@ -13,7 +13,8 @@ import {
type AllBooleanItemKey as CommonlibAllBooleanItemKey,
} from "@vrtmrz/livesync-commonlib/compat/common/settingConstants";
import type { ConfigurationItem } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { MessageTranslator } from "@vrtmrz/livesync-commonlib/context";
import type { AllMessageKeys } from "@/common/rosetta";
import type { LiveSyncMessageTranslator } from "@/common/translation";
import { translateLiveSyncMessage } from "@/common/translation";
import {
DEFAULT_SYNC_GUARD_SETTINGS,
@@ -52,13 +53,19 @@ export const SettingInformation: Partial<Record<AllSettingItemKey, Configuration
// site has to remember the second argument.
/** `getConfig` with the LiveSync catalogue applied by default. */
export function getConfig(key: AllSettingItemKey, translate: MessageTranslator = translateLiveSyncMessage) {
if (isSyncGuardSettingKey(key)) return syncGuardSettingInformation[key];
export function getConfig(key: AllSettingItemKey, translate: LiveSyncMessageTranslator = translateLiveSyncMessage) {
if (isSyncGuardSettingKey(key)) {
const info = syncGuardSettingInformation[key];
return {
name: translate(info.name as AllMessageKeys),
desc: info.desc ? translate(info.desc as AllMessageKeys) : undefined,
};
}
return getCommonlibConfig(key, translate);
}
/** `getConfName` with the LiveSync catalogue applied by default. See `getConfig`. */
export function getConfName(key: AllSettingItemKey, translate: MessageTranslator = translateLiveSyncMessage) {
if (isSyncGuardSettingKey(key)) return syncGuardSettingInformation[key].name;
export function getConfName(key: AllSettingItemKey, translate: LiveSyncMessageTranslator = translateLiveSyncMessage) {
if (isSyncGuardSettingKey(key)) return translate(syncGuardSettingInformation[key].name as AllMessageKeys);
return getCommonlibConfName(key, translate);
}
@@ -25,6 +25,13 @@ describe("setting manifest labels", () => {
});
});
it("renders LiveSync provisional settings in Japanese", () => {
setLang("ja");
expect(getConfName("batchSaveInLiveSync")).toBe("LiveSync モードでデータベース更新をバッチ処理");
expect(getConfName("syncGuardEnabled")).toBe("SyncGuard を有効化");
});
it("passes through labels which Commonlib owns but the catalogue does not carry", () => {
setLang("zh-tw");
+3 -1
View File
@@ -77,7 +77,9 @@ async function confirmTranslatedChoice(
const selected = await host.services.UI.confirm.confirmWithMessage(title, message, labels, $msg(defaultAction), 0);
if (selected === false) return false;
const index = labels.indexOf(selected);
return index < 0 ? false : buttons[index];
if (index >= 0) return buttons[index];
const buttonIndex = buttons.indexOf(selected as AllMessageKeys);
return buttonIndex < 0 ? false : buttons[buttonIndex];
}
export async function askSimpleFetchMode(
@@ -220,7 +220,7 @@ describe("Fetch restart confirmation regressions", () => {
expect(confirmWithMessage).toHaveBeenCalledOnce();
expect(confirmWithMessage.mock.calls[0][1]).toContain("example.invalid");
expect(confirmWithMessage.mock.calls[0][1]).toContain(REMOTE_WINS);
expect(confirmWithMessage.mock.calls[0][1]).toContain(KEEP_LOCAL);
expect(confirmWithMessage.mock.calls[0][1]).toContain($msg(KEEP_LOCAL));
expect(files.get(flagPath)).toBe(rememberedMarkdown);
});
+1 -2
View File
@@ -497,8 +497,7 @@ describe("Red Flag Feature", () => {
expect(host.mocks.rebuilder.$fetchLocalDBFast).toHaveBeenCalled();
expect(synchroniseAllFilesBetweenDBandStorage).toHaveBeenCalled();
const firstPrompt = host.mocks.ui.confirm.confirmWithMessage.mock.calls[0]?.[1];
expect(firstPrompt).toContain("data retrieved from this remote source");
expect(firstPrompt).not.toContain("remote server");
expect(firstPrompt).toContain("We are about to retrieve the remote data.");
// We can't easily check performFullScan call here because it's imported,
// but we can verify rebuilder was called.
});