diff --git a/.github/ISSUE_TEMPLATE/issue-report.md b/.github/ISSUE_TEMPLATE/issue-report.md index d12287f..0184d93 100644 --- a/.github/ISSUE_TEMPLATE/issue-report.md +++ b/.github/ISSUE_TEMPLATE/issue-report.md @@ -53,11 +53,12 @@ The hatch report (below) includes version information. If you cannot provide the - Self-hosted LiveSync version: -### Report from LiveSync -Open the `Hatch` pane in LiveSync settings and press `Make report`. Paste here or upload to [Gist](https://gist.github.com/) and share the link. +### Report and Logs from LiveSync +Perform a `Generate full report for opening the issue with debug info` command and provide the generated report. This contains detailed information and recent 1000 log lines, which is very helpful for debugging. **PLEASE AMEND THE REPORT TO REMOVE ANY SENSITIVE INFORMATION BEFORE PASTING.** +If too large to paste here, upload to [Gist](https://gist.github.com/) and share the link.
-Report from hatch (primary) +Report and Logs (primary) ``` @@ -65,29 +66,7 @@ Open the `Hatch` pane in LiveSync settings and press `Make report`. Paste here o
-Report from hatch (if applicable) - -``` - -``` -
- - -### Plug-in log -Enable `Verbose Log` in General Settings first, then reproduce the issue and copy the log (tap the document box icon in the ribbon). -Paste here or upload to [Gist](https://gist.github.com/) and share the link. - -
-Plug-in log (primary) - -``` - -``` -
- - -
-Plug-in log (if applicable) +Report and Logs (if applicable) ``` diff --git a/.gitignore b/.gitignore index c7cfbd9..986b95c 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ src/apps/cli/dist/* # Obsidian E2E test artefacts test_e2e/playwright-report/ -test_e2e/test-results/ \ No newline at end of file +test_e2e/test-results/ +_testdata/** +utils/bench/splitResults.csv diff --git a/README.md b/README.md index 9980ca7..ed2540e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Self-hosted LiveSync is a community-developed synchronisation plug-in available on all Obsidian-compatible platforms. It leverages robust server solutions such as CouchDB or object storage systems (e.g., MinIO, S3, R2, etc.) to ensure reliable data synchronisation. -Additionally, it supports peer-to-peer synchronisation using WebRTC now (experimental), enabling you to synchronise your notes directly between devices without relying on a server. +Additionally, it supports peer-to-peer synchronisation using WebRTC, enabling you to synchronise your notes directly between devices without relying on a server. Documentations is available for [Peer-to-Peer Synchronisation](./docs/p2p_sync_updates_2026.md). ![obsidian_live_sync_demo](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif) diff --git a/docs/p2p_sync_updates_2026.md b/docs/p2p_sync_updates_2026.md new file mode 100644 index 0000000..b6f3903 --- /dev/null +++ b/docs/p2p_sync_updates_2026.md @@ -0,0 +1,59 @@ +# User Guide: Peer-to-Peer Synchronisation (2026 Edition) + +Peer-to-Peer (P2P) synchronisation has evolved significantly. This guide covers the essential setup and the new features introduced in the 2026 updates. + +## 1. Core Concept: Server-less Freedom +P2P synchronisation allows your devices to talk directly to each other using WebRTC. A central server is not required for data storage, ensuring maximum privacy and "freedom." + +## 2. Setting Up via P2P Status Pane +You no longer need to navigate through complex menus. Simply open the **P2P Status** (via the ribbon icon or command palette) and click the **βš™ (Cog)** icon. + +This opens the **P2P Setup** dialogue where you can configure the essentials: +- **Room ID:** A unique identifier for your synchronisation group. +- **Passphrase:** Your encryption key. Ensure all your devices use the exact same passphrase. +- **Device Name:** A recognisable name for the current device (e.g., `iphone-16`). + +Once you have saved the settings, return to the **P2P Status Pane** and click the **Connect** button to join the network. + +*Tip: You can also toggle **Auto Connect** in the setup dialogue to automatically join the network whenever Obsidian starts.* + +## 3. Real-time Control +The status pane in the right sidebar provides granular control over your synchronisation: + +- **Active P2P Remote (new):** P2P now has its own active remote selection, separate from the normal active remote for database replication. Use the combo box next to the cog icon to choose which P2P remote configuration is active for P2P features. +- **Create P2P Remote (new):** Use the **+** button to open the P2P setup dialogue and create a dedicated P2P remote configuration. This is recommended when no P2P active remote has been selected yet. +- **Selection required (new):** If no P2P active remote is selected, the pane asks for selection before P2P target-related changes are saved. + +- **Signalling Status:** Shows if you are connected to the relay (🟒 Online). +- **Live-push (Broadcast):** Toggle "Broadcast changes" to notify other peers whenever you make an edit. +- **Replicate now (πŸ”„):** Start immediate bidirectional replication with a visible peer (Pull, then Push). +- **Watch (πŸ””/πŸ”•):** Enable "Watch" on specific peers to automatically pull changes when they broadcast. This creates a "LiveSync-like" experience. +- **Sync target (πŸ”—/⛓️‍πŸ’₯):** Mark specific peers as **sync targets**. Peers marked here will be included when you run the **"P2P: Sync with targets"** command (see section 5). Click the button next to a peer to toggle it on (πŸ”—, highlighted) or off (⛓️‍πŸ’₯). This setting is persisted in your configuration. + +## 4. Replication Dialogue +If you want to synchronise with a specific peer manually, use the **Replication** command or button. This opens the **Replication Dialogue** listing available devices. + +Inside the dialogue, the **Server Status** card at the top confirms you are still connected while performing the sync. +The status card now shows a stable **Room ID suffix** above **Peer ID**. The Room ID suffix is better for identifying your P2P group, while Peer ID may change between connections. + +Two actions are available per peer: + +- **Sync** β€” Starts a bidirectional synchronisation (Pull then Push) and keeps the dialogue open so you can monitor progress or sync with additional peers. +- **Start Sync & Close** β€” Starts the same bidirectional sync in the background and **immediately closes the dialogue**, so you can continue working without waiting. + +## 5. Syncing with Registered Targets via Command Palette + +You can now trigger a synchronisation with all your pre-registered target peers in one step, without opening any UI. + +1. Open the **Command Palette** (`Ctrl/Cmd + P`). +2. Run **"P2P: Sync with targets"**. + +This command synchronises with every peer whose **SYNC** toggle is enabled in the **Detected Peers** list. If no targets are registered, or if the P2P server is not running, the command will notify you accordingly. + +*Tip: Pair this command with a hotkey for a quick, keyboard-driven sync workflow.* + +## 6. Technical Improvements in 2026 +- **Decoupled Architecture:** The UI is now strictly separated from the core logic, making the plugin more stable across different platforms (Mobile, Desktop, and Web). +- **Svelte 5 UI:** The interface has been rebuilt for better responsiveness and clearer status indicators. +- **Security:** All data remains end-to-end encrypted. Even the signalling relay never sees your actual notes. + diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index fe22a00..f463e36 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -255,14 +255,20 @@ It depends on Obsidian detects. May toggling `Detect all extensions` of ### I hope to report the issue, but you said you needs `Report`. How to make it? -We can copy the report to the clipboard, by pressing the `Make report` button on -the `Hatch` pane. ![Screenshot](../images/hatch.png) +We can copy the report to the clipboard, by performing +`Generate full report for opening the issue with debug info` command! ### Where can I check the log? We can launch the log pane by `Show log` on the command palette. And if you have troubled something, please enable the `Verbose Log` on the `General Setting` pane. +`Generate full report for opening the issue with debug info` command also contains +the recent 1000 log lines, which is very helpful for debugging. Full-report is +already set to the verbose level, so it contains all the logs without enabling the +`Verbose Log` toggle. + +Let me note that please be sure to remove any sensitive information before sharing the report. However, the logs would not be kept so long and cleared when restarted. If you want to check the logs, please enable `Write logs into the file` temporarily. diff --git a/manifest.json b/manifest.json index 66379e4..72f7349 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.25.62", + "version": "0.25.65", "minAppVersion": "1.7.2", "description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "author": "vorotamoroz", diff --git a/package-lock.json b/package-lock.json index 4349282..43bf109 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.25.62", + "version": "0.25.65", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.25.62", + "version": "0.25.65", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", @@ -1851,9 +1851,9 @@ "license": "MIT" }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -1887,7 +1887,7 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-helpers/node_modules/@eslint/core": { + "node_modules/@eslint/core": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", @@ -1932,9 +1932,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -1984,19 +1984,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/json/node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@eslint/object-schema": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", @@ -2021,19 +2008,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@fidm/asn1": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@fidm/asn1/-/asn1-1.0.4.tgz", @@ -3572,20 +3546,13 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.12", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", - "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.20", - "@smithy/util-utf8": "^4.2.2", - "@smithy/uuid": "^1.1.2", + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -3752,11 +3719,12 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", - "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.3.3.tgz", + "integrity": "sha512-RRxYqjUa/n8dRVkbhyuiRarppLzt4H/AtMUEFmiHlDy8o4wrgqAdzxsk9naemzu6iX67ZV375fNmX7Q8dynGKw==", "license": "Apache-2.0", "dependencies": { + "@smithy/core": "^3.24.3", "tslib": "^2.6.2" }, "engines": { @@ -4020,9 +3988,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", - "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4084,12 +4052,12 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", - "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.3.3.tgz", + "integrity": "sha512-5xlgilVaX96HdVlLZymKUa7vOTZtisOTxBJloM2J4PeRqyAWBeFIq0DnIxQISvwxT4rgJAvk7rHhB+GlCCKe8g==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", + "@smithy/core": "^3.24.3", "tslib": "^2.6.2" }, "engines": { @@ -4226,12 +4194,12 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", - "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.3.3.tgz", + "integrity": "sha512-c1QpRBn3aMsoqE64dd4Imgjy8Pynfw+eR7GkjElquxUFSnezwYVaOFm8JcYa+Bo/5ssbEyPKcT3+4bmrWYh6eQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.2", + "@smithy/core": "^3.24.3", "tslib": "^2.6.2" }, "engines": { @@ -4521,9 +4489,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", - "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", "dev": true, "license": "MIT", "dependencies": { @@ -5255,9 +5223,9 @@ "license": "MIT" }, "node_modules/@wdio/config/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -5377,9 +5345,9 @@ } }, "node_modules/@wdio/repl/node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5407,9 +5375,9 @@ } }, "node_modules/@wdio/types/node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5532,9 +5500,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -5623,9 +5591,9 @@ "license": "MIT" }, "node_modules/archiver-utils/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -6234,9 +6202,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -7150,9 +7118,9 @@ } }, "node_modules/dotenv": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.0.tgz", - "integrity": "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -7914,9 +7882,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -8262,14 +8230,14 @@ } }, "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", - "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", + "is-core-module": "^2.16.2", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", @@ -8432,19 +8400,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/eslint/node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -8453,9 +8408,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -9289,9 +9244,9 @@ "license": "MIT" }, "node_modules/globby/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -9443,9 +9398,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "license": "MIT", "dependencies": { @@ -9786,13 +9741,13 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -10973,9 +10928,9 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", + "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -12695,9 +12650,9 @@ "license": "MIT" }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -12817,12 +12772,13 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "dev": true, "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" @@ -13119,9 +13075,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -15293,9 +15249,9 @@ } }, "node_modules/undici": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz", - "integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "dev": true, "license": "MIT", "engines": { @@ -16157,9 +16113,9 @@ } }, "node_modules/vitest/node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, @@ -16240,9 +16196,9 @@ } }, "node_modules/webdriver/node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16250,9 +16206,9 @@ } }, "node_modules/webdriver/node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", "dev": true, "license": "MIT", "engines": { @@ -16312,9 +16268,9 @@ } }, "node_modules/webdriverio/node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16778,9 +16734,9 @@ "license": "BSD" }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "dev": true, "license": "MIT", "engines": { @@ -16848,9 +16804,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", "bin": { diff --git a/package.json b/package.json index 37ab158..8aa8338 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.25.62", + "version": "0.25.65", "description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "main": "main.js", "type": "module", diff --git a/src/apps/cli/Dockerfile b/src/apps/cli/Dockerfile index 7e85bdb..beed3b5 100644 --- a/src/apps/cli/Dockerfile +++ b/src/apps/cli/Dockerfile @@ -60,7 +60,7 @@ RUN apt-get update \ WORKDIR /build # Install workspace dependencies first (layer-cache friendly) -COPY package.json ./ +COPY package.json package-lock.json ./ RUN npm install # Copy the full source tree and build the CLI bundle diff --git a/src/apps/cli/commands/p2p.ts b/src/apps/cli/commands/p2p.ts index f47b62e..e3297ee 100644 --- a/src/apps/cli/commands/p2p.ts +++ b/src/apps/cli/commands/p2p.ts @@ -32,10 +32,15 @@ function validateP2PSettings(core: LiveSyncBaseCore) { settings.P2P_IsHeadless = true; } -function createReplicator(core: LiveSyncBaseCore): LiveSyncTrysteroReplicator { +async function createReplicator(core: LiveSyncBaseCore): Promise { validateP2PSettings(core); - const replicator = new LiveSyncTrysteroReplicator({ services: core.services }); - addP2PEventHandlers(replicator); + const replicator = await core.services.replicator.getNewReplicator(); + if (!replicator) { + throw new Error("Failed to create replicator instance. Ensure P2P is enabled in settings."); + } + if (!(replicator instanceof LiveSyncTrysteroReplicator)) { + throw new Error("Unexpected replicator type. Expected LiveSyncTrysteroReplicator."); + } return replicator; } @@ -49,7 +54,7 @@ export async function collectPeers( core: LiveSyncBaseCore, timeoutSec: number ): Promise { - const replicator = createReplicator(core); + const replicator = await createReplicator(core); await replicator.open(); try { await delay(timeoutSec * 1000); @@ -79,7 +84,7 @@ export async function syncWithPeer( peerToken: string, timeoutSec: number ): Promise { - const replicator = createReplicator(core); + const replicator = await createReplicator(core); await replicator.open(); try { const timeoutMs = timeoutSec * 1000; @@ -115,7 +120,7 @@ export async function syncWithPeer( } export async function openP2PHost(core: LiveSyncBaseCore): Promise { - const replicator = createReplicator(core); + const replicator = await createReplicator(core); await replicator.open(); return replicator; } diff --git a/src/apps/cli/main.ts b/src/apps/cli/main.ts index 7067c70..b75dd34 100644 --- a/src/apps/cli/main.ts +++ b/src/apps/cli/main.ts @@ -8,7 +8,6 @@ import * as path from "path"; import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub"; import { configureNodeLocalStorage, ensureGlobalNodeLocalStorage } from "./services/NodeLocalStorage"; import { LiveSyncBaseCore } from "../../LiveSyncBaseCore"; -import { ModuleReplicatorP2P } from "../../modules/core/ModuleReplicatorP2P"; import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules"; import { DEFAULT_SETTINGS, LOG_LEVEL_VERBOSE, type LOG_LEVEL, type ObsidianLiveSyncSettings } from "@lib/common/types"; import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub"; @@ -27,6 +26,7 @@ import type { CLICommand, CLIOptions } from "./commands/types"; import { getPathFromUXFileInfo } from "@lib/common/typeUtils"; import { stripAllPrefixes } from "@lib/string_and_binary/path"; import { IgnoreRules } from "./serviceModules/IgnoreRules"; +import { useP2PReplicatorFeature } from "@/lib/src/replication/trystero/useP2PReplicatorFeature"; const SETTINGS_FILE = ".livesync/settings.json"; ensureGlobalNodeLocalStorage(); @@ -368,12 +368,11 @@ export async function main() { (core: LiveSyncBaseCore, serviceHub: InjectableServiceHub) => { return initialiseServiceModulesCLI(vaultPath, core, serviceHub, ignoreRules, watchEnabled); }, - (core) => [ - // No modules need to be registered for P2P replication in CLI. Directly using Replicators in p2p.ts - // new ModuleReplicatorP2P(core), - ], + (core) => [], () => [], // No add-ons (core) => { + // Register P2P replicator feature. + const _replicator = useP2PReplicatorFeature(core); // Add target filter to prevent internal files are handled core.services.vault.isTargetFile.addHandler(async (target) => { const targetPath = stripAllPrefixes(getPathFromUXFileInfo(target)); @@ -424,7 +423,7 @@ export async function main() { // Save the settings file before any lifecycle events can mutate and persist them. // suspendAllSync and other lifecycle hooks clobber sync settings in memory, and // various code paths persist the clobbered state to disk. We restore on shutdown. - const settingsBackup = await fs.readFile(settingsPath, "utf-8").catch(() => null); + const settingsBackup = await fs.readFile(settingsPath, "utf-8").catch(() => null!); // Restore settings file on any exit to undo lifecycle mutations. // Write to a temp path first so a crash mid-write doesn't leave a truncated file. diff --git a/src/common/reportTool.ts b/src/common/reportTool.ts new file mode 100644 index 0000000..8eef539 --- /dev/null +++ b/src/common/reportTool.ts @@ -0,0 +1,142 @@ +import { REMOTE_COUCHDB, REMOTE_MINIO } from "@lib/common/models/setting.const"; +import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type"; +import { generateCredentialObject } from "@lib/replication/httplib"; +import { parseHeaderValues } from "@lib/common/utils"; +import { requestToCouchDBWithCredentials } from "./utils"; +import { LOG_LEVEL_VERBOSE, Logger } from "@lib/common/logger"; +import { DEFAULT_SETTINGS } from "@lib/common/models/setting.const.defaults"; +import { isCloudantURI } from "@lib/pouchdb/utils_couchdb"; +import { compatGlobal } from "@lib/common/coreEnvFunctions"; +import { manifestVersion, packageVersion } from "@lib/common/coreEnvVars"; +import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore"; +function redactObject(obj: Record, dotted: string, redactedValue = "REDACTED") { + const keys = dotted.split("."); + let current = obj; + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]; + if (!(key in current)) { + current[key] = {} as Record; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + current = current[key]; + } + const lastKey = keys[keys.length - 1]; + if (lastKey in current) { + current[lastKey] = redactedValue; + } + return obj; +} +export async function generateReport(settings: ObsidianLiveSyncSettings, core: LiveSyncBaseCore) { + let responseConfig: Record = {}; + const REDACTED = "𝑅𝐸𝐷𝐴𝐢𝑇𝐸𝐷"; + if (settings.remoteType == REMOTE_COUCHDB) { + try { + const credential = generateCredentialObject(settings); + const customHeaders = parseHeaderValues(settings.couchDB_CustomHeaders); + const r = await requestToCouchDBWithCredentials( + settings.couchDB_URI, + credential, + window.origin, + undefined, + undefined, + undefined, + customHeaders + ); + responseConfig = r.json as Record; + redactObject(responseConfig, "couch_httpd_auth.secret"); + redactObject(responseConfig, "couch_httpd_auth.authentication_db"); + redactObject(responseConfig, "couch_httpd_auth.authentication_redirect"); + redactObject(responseConfig, "couchdb.uuid"); + redactObject(responseConfig, "admins"); + redactObject(responseConfig, "users"); + redactObject(responseConfig, "chttpd_auth.secret"); + delete responseConfig["jwt_keys"]; + } catch (ex) { + Logger(ex, LOG_LEVEL_VERBOSE); + responseConfig = { + error: "Requesting information from the remote CouchDB has failed. If you are using IBM Cloudant, this is normal behaviour.", + }; + } + } else if (settings.remoteType == REMOTE_MINIO) { + responseConfig = { error: "Object Storage Synchronisation" }; + // + } + const defaultKeys = Object.keys(DEFAULT_SETTINGS) as (keyof ObsidianLiveSyncSettings)[]; + const pluginConfig = JSON.parse(JSON.stringify(settings)) as ObsidianLiveSyncSettings; + const pluginKeys = Object.keys(pluginConfig); + for (const key of pluginKeys) { + if (defaultKeys.includes(key as keyof ObsidianLiveSyncSettings)) continue; + delete pluginConfig[key as keyof ObsidianLiveSyncSettings]; + } + + pluginConfig.couchDB_DBNAME = REDACTED; + pluginConfig.couchDB_PASSWORD = REDACTED; + const scheme = pluginConfig.couchDB_URI.startsWith("http:") + ? "(HTTP)" + : pluginConfig.couchDB_URI.startsWith("https:") + ? "(HTTPS)" + : ""; + pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI) ? "cloudant" : `self-hosted${scheme}`; + pluginConfig.couchDB_USER = REDACTED; + pluginConfig.passphrase = REDACTED; + pluginConfig.encryptedPassphrase = REDACTED; + pluginConfig.encryptedCouchDBConnection = REDACTED; + pluginConfig.accessKey = REDACTED; + pluginConfig.secretKey = REDACTED; + const redact = (source: string) => `${REDACTED}(${source.length} letters)`; + const toSchemeOnly = (uri: string) => { + try { + return `${new URL(uri).protocol}//`; + } catch { + const matched = uri.match(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//); + return matched?.[0] ?? REDACTED; + } + }; + pluginConfig.remoteConfigurations = Object.fromEntries( + Object.entries(pluginConfig.remoteConfigurations || {}).map(([id, config]) => [ + id, + { + ...config, + uri: toSchemeOnly(config.uri), + }, + ]) + ); + pluginConfig.region = redact(pluginConfig.region); + pluginConfig.bucket = redact(pluginConfig.bucket); + pluginConfig.pluginSyncExtendedSetting = {}; + pluginConfig.P2P_AppID = redact(pluginConfig.P2P_AppID); + pluginConfig.P2P_passphrase = redact(pluginConfig.P2P_passphrase); + pluginConfig.P2P_roomID = redact(pluginConfig.P2P_roomID); + pluginConfig.P2P_relays = redact(pluginConfig.P2P_relays); + pluginConfig.jwtKey = redact(pluginConfig.jwtKey); + pluginConfig.jwtSub = redact(pluginConfig.jwtSub); + pluginConfig.jwtKid = redact(pluginConfig.jwtKid); + pluginConfig.bucketCustomHeaders = redact(pluginConfig.bucketCustomHeaders); + pluginConfig.couchDB_CustomHeaders = redact(pluginConfig.couchDB_CustomHeaders); + pluginConfig.P2P_turnCredential = redact(pluginConfig.P2P_turnCredential); + pluginConfig.P2P_turnUsername = redact(pluginConfig.P2P_turnUsername); + pluginConfig.P2P_turnServers = `(${pluginConfig.P2P_turnServers.split(",").length} servers configured)`; + const endpoint = pluginConfig.endpoint; + if (endpoint == "") { + pluginConfig.endpoint = "Not configured or AWS"; + } else { + const endpointScheme = pluginConfig.endpoint.startsWith("http:") + ? "(HTTP)" + : pluginConfig.endpoint.startsWith("https:") + ? "(HTTPS)" + : ""; + pluginConfig.endpoint = `${endpoint.indexOf(".r2.cloudflarestorage.") !== -1 ? "R2" : "self-hosted?"}(${endpointScheme})`; + } + const obsidianInfo = { + navigator: compatGlobal.navigator.userAgent, + fileSystem: core.services.vault.isStorageInsensitive() ? "insensitive" : "sensitive", + }; + const result = { + obsidianInfo, + responseConfig, + pluginConfig, + manifestVersion, + packageVersion, + }; + return result; +} diff --git a/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.ts b/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.ts new file mode 100644 index 0000000..aa2cd94 --- /dev/null +++ b/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.ts @@ -0,0 +1,80 @@ +import { App, Modal } from "@/deps.ts"; +import P2POpenReplicationPane from "./P2POpenReplicationPane.svelte"; +import { mount, unmount } from "svelte"; +import type { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator"; + +export type P2POpenReplicationModalCallback = { + onSync: (peerId: string) => Promise; + onSyncAndClose: (peerId: string) => Promise; +}; + +export class P2POpenReplicationModal extends Modal { + liveSyncReplicator: LiveSyncTrysteroReplicator; + callback?: P2POpenReplicationModalCallback; + component?: ReturnType; + showResult: boolean; + title: string; + onClosed?: () => void; + rebuildMode: boolean; + + constructor( + app: App, + liveSyncReplicator: LiveSyncTrysteroReplicator, + callback?: P2POpenReplicationModalCallback, + showResult: boolean = false, + title: string = "P2P Replication", + onClosed?: () => void, + rebuildMode: boolean = false + ) { + super(app); + this.liveSyncReplicator = liveSyncReplicator; + this.callback = callback; + this.showResult = showResult; + this.title = title; + this.onClosed = onClosed; + this.rebuildMode = rebuildMode; + } + + async onSync(peerId: string) { + if (this.callback?.onSync) { + await this.callback.onSync(peerId); + } + } + + async onSyncAndClose(peerId: string) { + if (this.callback?.onSyncAndClose) { + await this.callback.onSyncAndClose(peerId); + } + this.close(); + } + + override onOpen() { + const { contentEl } = this; + this.titleEl.setText(this.title); + contentEl.empty(); + + if (this.component === undefined) { + this.component = mount(P2POpenReplicationPane, { + target: contentEl, + props: { + liveSyncReplicator: this.liveSyncReplicator, + onSync: (peerId: string) => this.onSync(peerId), + onSyncAndClose: (peerId: string) => this.onSyncAndClose(peerId), + onClose: () => this.close(), + showResult: this.showResult, + rebuildMode: this.rebuildMode, + }, + }); + } + } + + override onClose() { + const { contentEl } = this; + contentEl.empty(); + if (this.component !== undefined) { + void unmount(this.component); + this.component = undefined; + } + this.onClosed?.(); + } +} diff --git a/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte b/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte new file mode 100644 index 0000000..58c7e99 --- /dev/null +++ b/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte @@ -0,0 +1,313 @@ + + +
+ + +
+

Available Peers

+ {#if serverInfo && serverInfo.knownAdvertisements.length > 0} +
+ {#each serverInfo.knownAdvertisements as peer (peer.peerId)} +
+
+
{peer.name}
+
+ {peer.platform} + + {peer.peerId.slice(0, 8)} + + + {getAcceptanceStatus(peer)} + +
+
+
+ {#if !rebuildMode} + + + {:else} + + {/if} +
+
+ {/each} +
+ {:else if serverInfo} +

No devices available. Waiting for other devices to connect...

+ {/if} +
+ + +
+ + diff --git a/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts new file mode 100644 index 0000000..ae5f02f --- /dev/null +++ b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts @@ -0,0 +1,131 @@ +import { App } from "@/deps.ts"; +import { Logger } from "@lib/common/logger"; +import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@lib/common/types"; +import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator"; +import { P2POpenReplicationModal } from "./P2POpenReplicationModal"; + +/** + * Creates an openReplicationUI factory for Obsidian environments. + * Returns a per-replicator closure that opens the P2P Replication modal + * and performs bidirectional sync (pull then push on success). + * + * Usage: + * const factory = createOpenReplicationUI(app); + * useP2PReplicatorFeature(core, factory); + */ +export function createOpenReplicationUI( + app: App +): (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise { + return (replicator: LiveSyncTrysteroReplicator) => + (showResult: boolean): Promise => { + const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; + return new Promise((resolve) => { + const modal = new P2POpenReplicationModal( + app, + replicator, + { + onSync: async (peerId: string) => { + try { + // pull (replicateFrom) first; push only on success + const pullResult = await replicator.replicateFrom(peerId, showResult); + if (pullResult?.ok) { + const pushResult = await replicator.requestSynchroniseToPeer(peerId); + resolve(pushResult?.ok ?? true); + } else { + resolve(false); + } + } catch (e) { + Logger( + `Error in bidirectional sync with ${peerId}: ${e instanceof Error ? e.message : String(e)}`, + logLevel + ); + resolve(false); + } + }, + onSyncAndClose: async (peerId: string) => { + try { + const pullResult = await replicator.replicateFrom(peerId, showResult); + if (pullResult?.ok) { + const pushResult = await replicator.requestSynchroniseToPeer(peerId); + if (pushResult?.ok ?? true) { + await replicator.close(); + resolve(true); + } else { + resolve(false); + } + } else { + resolve(false); + } + } catch (e) { + Logger( + `Error in bidirectional sync with ${peerId}: ${e instanceof Error ? e.message : String(e)}`, + logLevel + ); + resolve(false); + } + }, + }, + showResult + ); + modal.open(); + }); + }; +} + +/** + * Creates an openRebuildUI factory for Obsidian environments. + * Opens the P2P Replication modal in "rebuild" mode β€” one-way pull only, + * with setOnSetup / clearOnSetup bracketing the replicateFrom call. + * + * Usage: + * const factory = createOpenRebuildUI(app); + * useP2PReplicatorFeature(core, createOpenReplicationUI(app), factory); + */ +export function createOpenRebuildUI( + app: App +): (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise { + return (replicator: LiveSyncTrysteroReplicator) => + (showResult: boolean): Promise => { + const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; + return new Promise((resolve) => { + let resolved = false; + const safeResolve = (val: boolean) => { + if (!resolved) { + resolved = true; + resolve(val); + } + }; + + const doRebuild = async (peerId: string) => { + replicator.setOnSetup(); + try { + Logger(`Rebuilding from peer ${peerId}`, logLevel); + const result = await replicator.replicateFrom(peerId, showResult); + safeResolve(result?.ok ?? false); + } catch (e) { + Logger( + `Error in rebuild from ${peerId}: ${e instanceof Error ? e.message : String(e)}`, + logLevel + ); + safeResolve(false); + } finally { + replicator.clearOnSetup(); + } + }; + + const modal = new P2POpenReplicationModal( + app, + replicator, + { + onSync: doRebuild, + onSyncAndClose: doRebuild, + }, + showResult, + "P2P Rebuild", + () => safeResolve(false), + true + ); + modal.open(); + }); + }; +} diff --git a/src/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte b/src/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte index 9dd7c8e..948a20e 100644 --- a/src/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte +++ b/src/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte @@ -5,20 +5,21 @@ AcceptedStatus, ConnectionStatus, type PeerStatus, - } from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon"; - import type { LiveSyncTrysteroReplicator } from "../../../lib/src/replication/trystero/LiveSyncTrysteroReplicator"; + } from "@lib/replication/trystero/P2PReplicatorPaneCommon"; + import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator"; import PeerStatusRow from "../P2PReplicator/PeerStatusRow.svelte"; - import { EVENT_LAYOUT_READY, eventHub } from "../../../common/events"; + import { EVENT_LAYOUT_READY, eventHub } from "@/common/events"; import { type PeerInfo, type P2PServerInfo, EVENT_SERVER_STATUS, EVENT_REQUEST_STATUS, EVENT_P2P_REPLICATOR_STATUS, - } from "../../../lib/src/replication/trystero/TrysteroReplicatorP2PServer"; - import { type P2PReplicatorStatus } from "../../../lib/src/replication/trystero/TrysteroReplicator"; - import { $msg as _msg } from "../../../lib/src/common/i18n"; - import { SETTING_KEY_P2P_DEVICE_NAME } from "../../../lib/src/common/types"; + } from "@lib/replication/trystero/TrysteroReplicatorP2PServer"; + import { type P2PReplicatorStatus } from "@lib/replication/trystero/TrysteroReplicator"; + import { $msg as _msg } from "@lib/common/i18n"; + import { SETTING_KEY_P2P_DEVICE_NAME } from "@lib/common/types"; + import { generateP2PRoomId } from "@lib/common/utils"; import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore"; interface Props { @@ -148,6 +149,7 @@ eventHub.emitEvent(EVENT_REQUEST_STATUS); return () => { r(); + rx(); r2(); r3(); }; @@ -216,18 +218,8 @@ function useDefaultRelay() { eRelay = DEFAULT_SETTINGS.P2P_relays; } - function _generateRandom() { - return (Math.floor(Math.random() * 1000) + 1000).toString().substring(1); - } - function generateRandom(length: number) { - let buf = ""; - while (buf.length < length) { - buf += "-" + _generateRandom(); - } - return buf.substring(1, length); - } function chooseRandom() { - eRoomId = generateRandom(12) + "-" + Math.random().toString(36).substring(2, 5); + eRoomId = generateP2PRoomId(); } async function openServer() { @@ -251,7 +243,7 @@ setting?: boolean; }; return initialDialogStatus; - } catch (e) { + } catch { return {}; } }; diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte b/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte new file mode 100644 index 0000000..4d53651 --- /dev/null +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte @@ -0,0 +1,224 @@ + + +
+

Signalling Status

+ +
+ Connection: + + {isConnected ? "🟒 Connected" : "πŸ”΄ Disconnected"} + +
+ +
+ {#if !isConnected} + + {:else} + + {/if} +
+ + {#if serverInfo} +
+ Room ID suffix: + + {roomSuffix || "-"} + +
+ +
+ Peer ID: + + {serverInfo.serverPeerId.slice(0, 12)}... + +
+ +
+ Devices: + {serverInfo.knownAdvertisements.length} +
+ {/if} + + {#if showBroadcastToggle} +
+ + + +
+ {/if} +
+ + \ No newline at end of file diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte b/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte new file mode 100644 index 0000000..2ed575c --- /dev/null +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte @@ -0,0 +1,891 @@ + + +
+
+

P2P Status

+
+
+ + +
+ +
+
+ + {#if !canEditP2PSettings()} +

Please select an active P2P remote configuration to change P2P sync targets.

+ {/if} + + + +
+
+

Detected Peers

+ +
+ + {#if serverInfo && serverInfo.knownAdvertisements.length > 0} +
+ {#each serverInfo.knownAdvertisements as peer (peer.peerId)} +
+
+
+ {peer.name} : + ({peer.peerId.slice(0, 8)}) + {#if isCommunicating(peer.peerId)} + πŸ“‘ + {/if} +
+
+ {peer.platform} +
+
+
+ {#if isAccepted(peer)} +
+ + {getAcceptanceStatus(peer)} + + + +
+
+ WATCH + +
+
+ SYNC + +
+ {:else} +
+ + {getAcceptanceStatus(peer)} + +
+
+ PERMANENT + + +
+
+ SESSION + + +
+ {/if} + {#if !isAccepted(peer) && (peer.isAccepted !== undefined || peer.isTemporaryAccepted !== undefined)} + + {/if} +
+
+ {/each} +
+ {:else if serverInfo} +

No devices available. Waiting for other devices to connect...

+ {:else} +

Fetching status...

+ {/if} +
+
+ + diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts b/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts new file mode 100644 index 0000000..a182b8a --- /dev/null +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts @@ -0,0 +1,43 @@ +import { WorkspaceLeaf } from "@/deps.ts"; +import { mount } from "svelte"; +import { SvelteItemView } from "@/common/SvelteItemView.ts"; +import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts"; +import type { P2PPaneParams } from "@/lib/src/replication/trystero/UseP2PReplicatorResult"; +import P2PServerStatusPane from "./P2PServerStatusPane.svelte"; + +export const VIEW_TYPE_P2P_SERVER_STATUS = "p2p-server-status"; + +export class P2PServerStatusPaneView extends SvelteItemView { + core: LiveSyncBaseCore; + private _p2pResult: P2PPaneParams; + override icon = "waypoints"; + override navigation = false; + + constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, p2pResult: P2PPaneParams) { + super(leaf); + this.core = core; + this._p2pResult = p2pResult; + } + + override getIcon(): string { + return "waypoints"; + } + + getViewType() { + return VIEW_TYPE_P2P_SERVER_STATUS; + } + + getDisplayText() { + return "P2P Status"; + } + + instantiateComponent(target: HTMLElement) { + return mount(P2PServerStatusPane, { + target, + props: { + liveSyncReplicator: this._p2pResult.replicator, + core: this.core, + }, + }); + } +} diff --git a/src/lib b/src/lib index ed4502e..6abcea6 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit ed4502e0035bfee88eca5f311d09ffc239ab9734 +Subproject commit 6abcea69eb929ea261308b543ac42cd54a00eee2 diff --git a/src/main.ts b/src/main.ts index 63242b0..6c5c3f3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -44,6 +44,7 @@ import { useSetupManagerHandlersFeature } from "./serviceFeatures/setupObsidian/ import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplicatorFeature.ts"; import { useP2PReplicatorCommands } from "@lib/replication/trystero/useP2PReplicatorCommands.ts"; import { useP2PReplicatorUI } from "./serviceFeatures/useP2PReplicatorUI.ts"; +import { createOpenReplicationUI, createOpenRebuildUI } from "./features/P2PSync/P2PReplicator/P2PReplicationUI.ts"; export type LiveSyncCore = LiveSyncBaseCore; export default class ObsidianLiveSyncPlugin extends Plugin { core: LiveSyncCore; @@ -176,7 +177,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin { const curriedFeature = () => featuresInitialiser(core); core.services.appLifecycle.onLayoutReady.addHandler(curriedFeature); const setupManager = core.getModule(SetupManager); - + const replicator = useP2PReplicatorFeature( + core, + createOpenReplicationUI(this.app), + createOpenRebuildUI(this.app) + ); + useP2PReplicatorCommands(core, replicator); + useP2PReplicatorUI(core, core, replicator); useRemoteConfiguration(core); useSetupProtocolFeature(core, setupManager); @@ -190,9 +197,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin { // VIEW_TYPE_P2P, // (leaf: any) => new P2PReplicatorPaneView(leaf, core, p2pReplicatorResult!), // ]); - const replicator = useP2PReplicatorFeature(core); - useP2PReplicatorCommands(core, replicator); - useP2PReplicatorUI(core, core, replicator); } ); } diff --git a/src/modules/core/ModuleReplicator.ts b/src/modules/core/ModuleReplicator.ts index 79b7b49..ddc59d0 100644 --- a/src/modules/core/ModuleReplicator.ts +++ b/src/modules/core/ModuleReplicator.ts @@ -1,3 +1,4 @@ +import type PouchDB from "pouchdb-core"; import { fireAndForget } from "octagonal-wheels/promises"; import { AbstractModule } from "../AbstractModule"; import { Logger, LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "octagonal-wheels/common/logger"; diff --git a/src/modules/core/ModuleReplicatorP2P.ts b/src/modules/core/ModuleReplicatorP2P.ts deleted file mode 100644 index 7f59794..0000000 --- a/src/modules/core/ModuleReplicatorP2P.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { REMOTE_P2P, type RemoteDBSettings } from "../../lib/src/common/types"; -import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator"; -import { AbstractModule } from "../AbstractModule"; -import { LiveSyncTrysteroReplicator } from "../../lib/src/replication/trystero/LiveSyncTrysteroReplicator"; -import type { LiveSyncCore } from "../../main"; - -// Note: -// This module registers only the `getNewReplicator` handler for the P2P replicator. -// `useP2PReplicator` (see P2PReplicatorCore.ts) already registers the same `getNewReplicator` -// handler internally, so this module is redundant in environments that call `useP2PReplicator`. -// Register this module only in environments that do NOT use `useP2PReplicator` (e.g. CLI). -// In other words: just resolving `getNewReplicator` via this module is all that is needed -// to satisfy what `useP2PReplicator` requires from the replicator service. -export class ModuleReplicatorP2P extends AbstractModule { - _anyNewReplicator(settingOverride: Partial = {}): Promise { - const settings = { ...this.settings, ...settingOverride }; - if (settings.remoteType == REMOTE_P2P) { - return Promise.resolve(new LiveSyncTrysteroReplicator(this.core)); - } - return Promise.resolve(false); - } - override onBindFunction(core: LiveSyncCore, services: typeof core.services): void { - services.replicator.getNewReplicator.addHandler(this._anyNewReplicator.bind(this)); - } -} diff --git a/src/modules/coreFeatures/ModuleRedFlag.ts b/src/modules/coreFeatures/ModuleRedFlag.ts deleted file mode 100644 index d57cb0f..0000000 --- a/src/modules/coreFeatures/ModuleRedFlag.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; -import { normalizePath } from "../../deps.ts"; -import { - FlagFilesHumanReadable, - FlagFilesOriginal, - REMOTE_MINIO, - TweakValuesShouldMatchedTemplate, - type ObsidianLiveSyncSettings, -} from "../../lib/src/common/types.ts"; -import { AbstractModule } from "../AbstractModule.ts"; -import type { LiveSyncCore } from "../../main.ts"; -import FetchEverything from "../features/SetupWizard/dialogs/FetchEverything.svelte"; -import RebuildEverything from "../features/SetupWizard/dialogs/RebuildEverything.svelte"; -import { extractObject } from "octagonal-wheels/object"; -import { SvelteDialogManagerBase } from "@lib/UI/svelteDialog.ts"; -import type { ServiceContext } from "@lib/services/base/ServiceBase.ts"; - -export class ModuleRedFlag extends AbstractModule { - async isFlagFileExist(path: string) { - const redflag = await this.core.storageAccess.isExists(normalizePath(path)); - if (redflag) { - return true; - } - return false; - } - - async deleteFlagFile(path: string) { - try { - const isFlagged = await this.core.storageAccess.isExists(normalizePath(path)); - if (isFlagged) { - await this.core.storageAccess.delete(path, true); - } - } catch (ex) { - this._log(`Could not delete ${path}`); - this._log(ex, LOG_LEVEL_VERBOSE); - } - } - - isSuspendFlagActive = async () => await this.isFlagFileExist(FlagFilesOriginal.SUSPEND_ALL); - isRebuildFlagActive = async () => - (await this.isFlagFileExist(FlagFilesOriginal.REBUILD_ALL)) || - (await this.isFlagFileExist(FlagFilesHumanReadable.REBUILD_ALL)); - isFetchAllFlagActive = async () => - (await this.isFlagFileExist(FlagFilesOriginal.FETCH_ALL)) || - (await this.isFlagFileExist(FlagFilesHumanReadable.FETCH_ALL)); - - async cleanupRebuildFlag() { - await this.deleteFlagFile(FlagFilesOriginal.REBUILD_ALL); - await this.deleteFlagFile(FlagFilesHumanReadable.REBUILD_ALL); - } - - async cleanupFetchAllFlag() { - await this.deleteFlagFile(FlagFilesOriginal.FETCH_ALL); - await this.deleteFlagFile(FlagFilesHumanReadable.FETCH_ALL); - } - // dialogManager = new SvelteDialogManagerBase(this.core); - get dialogManager(): SvelteDialogManagerBase { - return this.core.services.UI.dialogManager; - } - - /** - * Adjust setting to remote if needed. - * @param extra result of dialogues that may contain preventFetchingConfig flag (e.g, from FetchEverything or RebuildEverything) - * @param config current configuration to retrieve remote preferred config - */ - async adjustSettingToRemoteIfNeeded(extra: { preventFetchingConfig: boolean }, config: ObsidianLiveSyncSettings) { - if (extra && extra.preventFetchingConfig) { - return; - } - - // Remote configuration fetched and applied. - if (await this.adjustSettingToRemote(config)) { - config = this.core.settings; - } else { - this._log("Remote configuration not applied.", LOG_LEVEL_NOTICE); - } - console.debug(config); - } - - /** - * Adjust setting to remote configuration. - * @param config current configuration to retrieve remote preferred config - * @returns updated configuration if applied, otherwise null. - */ - async adjustSettingToRemote(config: ObsidianLiveSyncSettings) { - // Fetch remote configuration unless prevented. - const SKIP_FETCH = "Skip and proceed"; - const RETRY_FETCH = "Retry (recommended)"; - let canProceed = false; - do { - const remoteTweaks = await this.services.tweakValue.fetchRemotePreferred(config); - if (!remoteTweaks) { - const choice = await this.core.confirm.askSelectStringDialogue( - "Could not fetch configuration from remote. If you are new to the Self-hosted LiveSync, this might be expected. If not, you should check your network or server settings.", - [SKIP_FETCH, RETRY_FETCH] as const, - { - defaultAction: RETRY_FETCH, - timeout: 0, - title: "Fetch Remote Configuration Failed", - } - ); - if (choice === SKIP_FETCH) { - canProceed = true; - } - } else { - const necessary = extractObject(TweakValuesShouldMatchedTemplate, remoteTweaks); - // Check if any necessary tweak value is different from current config. - const differentItems = Object.entries(necessary).filter(([key, value]) => { - return (config as any)[key] !== value; - }); - if (differentItems.length === 0) { - this._log( - "Remote configuration matches local configuration. No changes applied.", - LOG_LEVEL_NOTICE - ); - } else { - await this.core.confirm.askSelectStringDialogue( - "Your settings differed slightly from the server's. The plug-in has supplemented the incompatible parts with the server settings!", - ["OK"] as const, - { - defaultAction: "OK", - timeout: 0, - } - ); - } - - config = { - ...config, - ...Object.fromEntries(differentItems), - } satisfies ObsidianLiveSyncSettings; - this.core.settings = config; - await this.core.services.setting.saveSettingData(); - this._log("Remote configuration applied.", LOG_LEVEL_NOTICE); - canProceed = true; - return this.core.settings; - } - } while (!canProceed); - } - - /** - * Process vault initialisation with suspending file watching and sync. - * @param proc process to be executed during initialisation, should return true if can be continued, false if app is unable to continue the process. - * @param keepSuspending whether to keep suspending file watching after the process. - * @returns result of the process, or false if error occurs. - */ - async processVaultInitialisation(proc: () => Promise, keepSuspending = false) { - try { - // Disable batch saving and file watching during initialisation. - this.settings.batchSave = false; - await this.services.setting.suspendAllSync(); - await this.services.setting.suspendExtraSync(); - this.settings.suspendFileWatching = true; - await this.saveSettings(); - try { - const result = await proc(); - return result; - } catch (ex) { - this._log("Error during vault initialisation process.", LOG_LEVEL_NOTICE); - this._log(ex, LOG_LEVEL_VERBOSE); - return false; - } - } catch (ex) { - this._log("Error during vault initialisation.", LOG_LEVEL_NOTICE); - this._log(ex, LOG_LEVEL_VERBOSE); - return false; - } finally { - if (!keepSuspending) { - // Re-enable file watching after initialisation. - this.settings.suspendFileWatching = false; - await this.saveSettings(); - } - } - } - - /** - * Handle the rebuild everything scheduled operation. - * @returns true if can be continued, false if app restart is needed. - */ - async onRebuildEverythingScheduled() { - const method = await this.dialogManager.openWithExplicitCancel(RebuildEverything); - if (method === "cancelled") { - // Clean up the flag file and restart the app. - this._log("Rebuild everything cancelled by user.", LOG_LEVEL_NOTICE); - await this.cleanupRebuildFlag(); - this.services.appLifecycle.performRestart(); - return false; - } - const { extra } = method; - await this.adjustSettingToRemoteIfNeeded(extra, this.settings); - return await this.processVaultInitialisation(async () => { - await this.core.rebuilder.$rebuildEverything(); - await this.cleanupRebuildFlag(); - this._log("Rebuild everything operation completed.", LOG_LEVEL_NOTICE); - return true; - }); - } - /** - * Handle the fetch all scheduled operation. - * @returns true if can be continued, false if app restart is needed. - */ - async onFetchAllScheduled() { - const method = await this.dialogManager.openWithExplicitCancel(FetchEverything); - if (method === "cancelled") { - this._log("Fetch everything cancelled by user.", LOG_LEVEL_NOTICE); - // Clean up the flag file and restart the app. - await this.cleanupFetchAllFlag(); - this.services.appLifecycle.performRestart(); - return false; - } - const { vault, extra } = method; - // If remote is MinIO, makeLocalChunkBeforeSync is not available. (because no-deduplication on sending). - const makeLocalChunkBeforeSyncAvailable = this.settings.remoteType !== REMOTE_MINIO; - const mapVaultStateToAction = { - identical: { - // If both are identical, no need to make local files/chunks before sync, - // Just for the efficiency, chunks should be made before sync. - makeLocalChunkBeforeSync: makeLocalChunkBeforeSyncAvailable, - makeLocalFilesBeforeSync: false, - }, - independent: { - // If both are independent, nothing needs to be made before sync. - // Respect the remote state. - makeLocalChunkBeforeSync: false, - makeLocalFilesBeforeSync: false, - }, - unbalanced: { - // If both are unbalanced, local files should be made before sync to avoid data loss. - // Then, chunks should be made before sync for the efficiency, but also the metadata made and should be detected as conflicting. - makeLocalChunkBeforeSync: false, - makeLocalFilesBeforeSync: true, - }, - cancelled: { - // Cancelled case, not actually used. - makeLocalChunkBeforeSync: false, - makeLocalFilesBeforeSync: false, - }, - } as const; - - return await this.processVaultInitialisation(async () => { - await this.adjustSettingToRemoteIfNeeded(extra, this.settings); - // Okay, proceed to fetch everything. - const { makeLocalChunkBeforeSync, makeLocalFilesBeforeSync } = mapVaultStateToAction[vault]; - this._log( - `Fetching everything with settings: makeLocalChunkBeforeSync=${makeLocalChunkBeforeSync}, makeLocalFilesBeforeSync=${makeLocalFilesBeforeSync}`, - LOG_LEVEL_INFO - ); - await this.core.rebuilder.$fetchLocal(makeLocalChunkBeforeSync, !makeLocalFilesBeforeSync); - await this.cleanupFetchAllFlag(); - this._log("Fetch everything operation completed. Vault files will be gradually synced.", LOG_LEVEL_NOTICE); - return true; - }); - } - - async onSuspendAllScheduled() { - this._log("SCRAM is detected. All operations are suspended.", LOG_LEVEL_NOTICE); - return await this.processVaultInitialisation(async () => { - this._log( - "All operations are suspended as per SCRAM.\nLogs will be written to the file. This might be a performance impact.", - LOG_LEVEL_NOTICE - ); - this.settings.writeLogToTheFile = true; - await this.core.services.setting.saveSettingData(); - return Promise.resolve(false); - }, true); - } - - async verifyAndUnlockSuspension() { - if (!this.settings.suspendFileWatching) { - return true; - } - if ( - (await this.core.confirm.askYesNoDialog( - "Do you want to resume file and database processing, and restart obsidian now?", - { defaultOption: "Yes", timeout: 15 } - )) != "yes" - ) { - // TODO: Confirm actually proceed to next process. - return true; - } - this.settings.suspendFileWatching = false; - await this.saveSettings(); - this.services.appLifecycle.performRestart(); - return false; - } - - private async processFlagFilesOnStartup(): Promise { - const isFlagSuspensionActive = await this.isSuspendFlagActive(); - const isFlagRebuildActive = await this.isRebuildFlagActive(); - const isFlagFetchAllActive = await this.isFetchAllFlagActive(); - // TODO: Address the case when both flags are active (very unlikely though). - // if(isFlagFetchAllActive && isFlagRebuildActive) { - // const message = "Rebuild everything and Fetch everything flags are both detected."; - // await this.core.confirm.askSelectStringDialogue( - // "Both Rebuild Everything and Fetch Everything flags are detected. Please remove one of them and restart the app.", - // ["OK"] as const,) - if (isFlagFetchAllActive) { - const res = await this.onFetchAllScheduled(); - if (res) { - return await this.verifyAndUnlockSuspension(); - } - return false; - } - if (isFlagRebuildActive) { - const res = await this.onRebuildEverythingScheduled(); - if (res) { - return await this.verifyAndUnlockSuspension(); - } - return false; - } - if (isFlagSuspensionActive) { - const res = await this.onSuspendAllScheduled(); - return res; - } - return true; - } - - async _everyOnLayoutReady(): Promise { - try { - const flagProcessResult = await this.processFlagFilesOnStartup(); - return flagProcessResult; - } catch (ex) { - this._log("Something went wrong on FlagFile Handling", LOG_LEVEL_NOTICE); - this._log(ex, LOG_LEVEL_VERBOSE); - } - return true; - } - override onBindFunction(core: LiveSyncCore, services: typeof core.services): void { - super.onBindFunction(core, services); - services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this)); - } -} diff --git a/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts b/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts index fab0091..d5776ef 100644 --- a/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts +++ b/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts @@ -14,6 +14,7 @@ import { AbstractModule } from "../AbstractModule.ts"; import { $msg } from "../../lib/src/common/i18n.ts"; import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts"; import type { LiveSyncCore } from "../../main.ts"; +import { REMOTE_P2P } from "@lib/common/models/setting.const.ts"; export class ModuleResolvingMismatchedTweaks extends AbstractModule { async _anyAfterConnectCheckFailed(): Promise { @@ -186,6 +187,9 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule { async _checkAndAskUseRemoteConfiguration( trialSetting: RemoteDBSettings ): Promise<{ result: false | TweakValues; requireFetch: boolean }> { + if (trialSetting.remoteType === REMOTE_P2P) { + return { result: false, requireFetch: false }; + } const preferred = await this.services.tweakValue.fetchRemotePreferred(trialSetting); if (preferred) { return await this.services.tweakValue.askUseRemoteConfiguration(trialSetting, preferred); diff --git a/src/modules/essential/ModuleInitializerFile.ts b/src/modules/essential/ModuleInitializerFile.ts deleted file mode 100644 index b26ac88..0000000 --- a/src/modules/essential/ModuleInitializerFile.ts +++ /dev/null @@ -1,429 +0,0 @@ -import { unique } from "octagonal-wheels/collection"; -import { throttle } from "octagonal-wheels/function"; -import { EVENT_ON_UNRESOLVED_ERROR, eventHub } from "../../common/events.ts"; -import { BASE_IS_NEW, EVEN, isValidPath, TARGET_IS_NEW } from "../../common/utils.ts"; -import { - type FilePathWithPrefixLC, - type FilePathWithPrefix, - type MetaEntry, - isMetaEntry, - type EntryDoc, - LOG_LEVEL_VERBOSE, - LOG_LEVEL_NOTICE, - LOG_LEVEL_INFO, - LOG_LEVEL_DEBUG, - type UXFileInfoStub, - type LOG_LEVEL, -} from "../../lib/src/common/types.ts"; -import { isAnyNote } from "../../lib/src/common/utils.ts"; -import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts"; -import { AbstractModule } from "../AbstractModule.ts"; -import { withConcurrency } from "octagonal-wheels/iterable/map"; -import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts"; -import type { LiveSyncCore } from "../../main.ts"; -export class ModuleInitializerFile extends AbstractModule { - private _detectedErrors = new Set(); - - private logDetectedError(message: string, logLevel: LOG_LEVEL = LOG_LEVEL_INFO, key?: string) { - this._detectedErrors.add(message); - eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR); - this._log(message, logLevel, key); - } - private resetDetectedError(message: string) { - eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR); - this._detectedErrors.delete(message); - } - private async _performFullScan(showingNotice?: boolean, ignoreSuspending: boolean = false): Promise { - this._log("Opening the key-value database", LOG_LEVEL_VERBOSE); - const isInitialized = (await this.core.kvDB.get("initialized")) || false; - // synchronize all files between database and storage. - - const ERR_NOT_CONFIGURED = - "LiveSync is not configured yet. Synchronising between the storage and the local database is now prevented."; - if (!this.settings.isConfigured) { - this.logDetectedError(ERR_NOT_CONFIGURED, showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "syncAll"); - return false; - } - this.resetDetectedError(ERR_NOT_CONFIGURED); - - const ERR_SUSPENDING = - "Now suspending file watching. Synchronising between the storage and the local database is now prevented."; - if (!ignoreSuspending && this.settings.suspendFileWatching) { - this.logDetectedError(ERR_SUSPENDING, showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "syncAll"); - return false; - } - const MSG_IN_REMEDIATION = `Started in remediation Mode! (Max mtime for reflect events is set). Synchronising between the storage and the local database is now prevented.`; - this.resetDetectedError(ERR_SUSPENDING); - if (this.settings.maxMTimeForReflectEvents > 0) { - this.logDetectedError(MSG_IN_REMEDIATION, LOG_LEVEL_NOTICE, "syncAll"); - return false; - } - this.resetDetectedError(MSG_IN_REMEDIATION); - - if (showingNotice) { - this._log("Initializing", LOG_LEVEL_NOTICE, "syncAll"); - } - if (isInitialized) { - this._log("Restoring storage state", LOG_LEVEL_VERBOSE); - await this.core.storageAccess.restoreState(); - } - - this._log("Initialize and checking database files"); - this._log("Checking deleted files"); - await this.collectDeletedFiles(); - - this._log("Collecting local files on the storage", LOG_LEVEL_VERBOSE); - const filesStorageSrc = await this.core.storageAccess.getFiles(); - - const _filesStorage = [] as typeof filesStorageSrc; - - for (const f of filesStorageSrc) { - if (await this.services.vault.isTargetFile(f.path)) { - _filesStorage.push(f); - } - } - - const convertCase = (path: FilePathWithPrefix): FilePathWithPrefixLC => { - if (this.settings.handleFilenameCaseSensitive) { - return path as FilePathWithPrefixLC; - } - return (path as string).toLowerCase() as FilePathWithPrefixLC; - }; - - // If handleFilenameCaseSensitive is enabled, `FilePathWithPrefixLC` is the same as `FilePathWithPrefix`. - - const storageFileNameMap = Object.fromEntries( - _filesStorage.map((e) => [e.path, e] as [FilePathWithPrefix, UXFileInfoStub]) - ); - - const storageFileNames = Object.keys(storageFileNameMap) as FilePathWithPrefix[]; - - const storageFileNameCapsPair = storageFileNames.map( - (e) => [e, convertCase(e)] as [FilePathWithPrefix, FilePathWithPrefixLC] - ); - - // const storageFileNameCS2CI = Object.fromEntries(storageFileNameCapsPair) as Record; - const storageFileNameCI2CS = Object.fromEntries(storageFileNameCapsPair.map((e) => [e[1], e[0]])) as Record< - FilePathWithPrefixLC, - FilePathWithPrefix - >; - - this._log("Collecting local files on the DB", LOG_LEVEL_VERBOSE); - const _DBEntries = [] as MetaEntry[]; - let count = 0; - // Fetch all documents from the database (including conflicts to prevent overwriting). - for await (const doc of this.localDatabase.findAllNormalDocs({ conflicts: true })) { - count++; - if (count % 25 == 0) - this._log( - `Collecting local files on the DB: ${count}`, - showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, - "syncAll" - ); - const path = this.getPath(doc); - - if (isValidPath(path) && (await this.services.vault.isTargetFile(path))) { - if (!isMetaEntry(doc)) { - this._log(`Invalid entry: ${path}`, LOG_LEVEL_INFO); - continue; - } - _DBEntries.push(doc); - } - } - - const databaseFileNameMap = Object.fromEntries( - _DBEntries.map((e) => [this.getPath(e), e] as [FilePathWithPrefix, MetaEntry]) - ); - const databaseFileNames = Object.keys(databaseFileNameMap) as FilePathWithPrefix[]; - const databaseFileNameCapsPair = databaseFileNames.map( - (e) => [e, convertCase(e)] as [FilePathWithPrefix, FilePathWithPrefixLC] - ); - // const databaseFileNameCS2CI = Object.fromEntries(databaseFileNameCapsPair) as Record; - const databaseFileNameCI2CS = Object.fromEntries(databaseFileNameCapsPair.map((e) => [e[1], e[0]])) as Record< - FilePathWithPrefix, - FilePathWithPrefixLC - >; - - const allFiles = unique([ - ...Object.keys(databaseFileNameCI2CS), - ...Object.keys(storageFileNameCI2CS), - ]) as FilePathWithPrefixLC[]; - - this._log(`Total files in the database: ${databaseFileNames.length}`, LOG_LEVEL_VERBOSE, "syncAll"); - this._log(`Total files in the storage: ${storageFileNames.length}`, LOG_LEVEL_VERBOSE, "syncAll"); - this._log(`Total files: ${allFiles.length}`, LOG_LEVEL_VERBOSE, "syncAll"); - const filesExistOnlyInStorage = allFiles.filter((e) => !databaseFileNameCI2CS[e]); - const filesExistOnlyInDatabase = allFiles.filter((e) => !storageFileNameCI2CS[e]); - const filesExistBoth = allFiles.filter((e) => databaseFileNameCI2CS[e] && storageFileNameCI2CS[e]); - - this._log(`Files exist only in storage: ${filesExistOnlyInStorage.length}`, LOG_LEVEL_VERBOSE, "syncAll"); - this._log(`Files exist only in database: ${filesExistOnlyInDatabase.length}`, LOG_LEVEL_VERBOSE, "syncAll"); - this._log(`Files exist both in storage and database: ${filesExistBoth.length}`, LOG_LEVEL_VERBOSE, "syncAll"); - - this._log("Synchronising..."); - const processStatus = {} as Record; - const logLevel = showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; - const updateLog = throttle((key: string, msg: string) => { - processStatus[key] = msg; - const log = Object.values(processStatus).join("\n"); - this._log(log, logLevel, "syncAll"); - }, 25); - - const initProcess = []; - const runAll = async (procedureName: string, objects: T[], callback: (arg: T) => Promise) => { - if (objects.length == 0) { - this._log(`${procedureName}: Nothing to do`); - return; - } - this._log(procedureName); - if (!this.localDatabase.isReady) throw Error("Database is not ready!"); - let success = 0; - let failed = 0; - let total = 0; - for await (const result of withConcurrency( - objects, - async (e) => { - try { - await callback(e); - return true; - } catch (ex) { - this._log(`Error while ${procedureName}`, LOG_LEVEL_NOTICE); - this._log(ex, LOG_LEVEL_VERBOSE); - return false; - } - }, - 10 - )) { - if (result) { - success++; - } else { - failed++; - } - total++; - const msg = `${procedureName}: DONE:${success}, FAILED:${failed}, LAST:${objects.length - total}`; - updateLog(procedureName, msg); - } - const msg = `${procedureName} All done: DONE:${success}, FAILED:${failed}`; - updateLog(procedureName, msg); - }; - initProcess.push( - runAll("UPDATE DATABASE", filesExistOnlyInStorage, async (e) => { - // Exists in storage but not in database. - const file = storageFileNameMap[storageFileNameCI2CS[e]]; - if (!this.services.vault.isFileSizeTooLarge(file.stat.size)) { - const path = file.path; - await this.core.fileHandler.storeFileToDB(file); - // fireAndForget(() => this.checkAndApplySettingFromMarkdown(path, true)); - eventHub.emitEvent("event-file-changed", { file: path, automated: true }); - } else { - this._log(`UPDATE DATABASE: ${e} has been skipped due to file size exceeding the limit`, logLevel); - } - }) - ); - initProcess.push( - runAll("UPDATE STORAGE", filesExistOnlyInDatabase, async (e) => { - const w = databaseFileNameMap[databaseFileNameCI2CS[e]]; - // Exists in database but not in storage. - const path = this.getPath(w) ?? e; - if (w && !(w.deleted || w._deleted)) { - if (!this.services.vault.isFileSizeTooLarge(w.size)) { - // Prevent applying the conflicted state to the storage. - if (w._conflicts?.length ?? 0 > 0) { - this._log(`UPDATE STORAGE: ${path} has conflicts. skipped (x)`, LOG_LEVEL_INFO); - return; - } - // await this.pullFile(path, undefined, false, undefined, false); - // Memo: No need to force - await this.core.fileHandler.dbToStorage(path, null, true); - // fireAndForget(() => this.checkAndApplySettingFromMarkdown(e, true)); - eventHub.emitEvent("event-file-changed", { - file: e, - automated: true, - }); - this._log(`Check or pull from db:${path} OK`); - } else { - this._log( - `UPDATE STORAGE: ${path} has been skipped due to file size exceeding the limit`, - logLevel - ); - } - } else if (w) { - this._log(`Deletion history skipped: ${path}`, LOG_LEVEL_VERBOSE); - } else { - this._log(`entry not found: ${path}`); - } - }) - ); - - const fileMap = filesExistBoth.map((path) => { - const file = storageFileNameMap[storageFileNameCI2CS[path]]; - const doc = databaseFileNameMap[databaseFileNameCI2CS[path]]; - return { file, doc }; - }); - initProcess.push( - runAll("SYNC DATABASE AND STORAGE", fileMap, async (e) => { - const { file, doc } = e; - // Prevent applying the conflicted state to the storage. - if (doc._conflicts?.length ?? 0 > 0) { - this._log(`SYNC DATABASE AND STORAGE: ${file.path} has conflicts. skipped`, LOG_LEVEL_INFO); - return; - } - if ( - !this.services.vault.isFileSizeTooLarge(file.stat.size) && - !this.services.vault.isFileSizeTooLarge(doc.size) - ) { - await this.syncFileBetweenDBandStorage(file, doc); - } else { - this._log( - `SYNC DATABASE AND STORAGE: ${this.getPath(doc)} has been skipped due to file size exceeding the limit`, - logLevel - ); - } - }) - ); - - await Promise.all(initProcess); - - // this.setStatusBarText(`NOW TRACKING!`); - this._log("Initialized, NOW TRACKING!"); - if (!isInitialized) { - await this.core.kvDB.set("initialized", true); - } - if (showingNotice) { - this._log("Initialize done!", LOG_LEVEL_NOTICE, "syncAll"); - } - return true; - } - - async syncFileBetweenDBandStorage(file: UXFileInfoStub, doc: MetaEntry) { - if (!doc) { - throw new Error(`Missing doc:${(file as any).path}`); - } - if ("path" in file) { - const w = await this.core.storageAccess.getFileStub((file as any).path); - if (w) { - file = w; - } else { - throw new Error(`Missing file:${(file as any).path}`); - } - } - - const compareResult = this.services.path.compareFileFreshness(file, doc); - switch (compareResult) { - case BASE_IS_NEW: - if (!this.services.vault.isFileSizeTooLarge(file.stat.size)) { - this._log("STORAGE -> DB :" + file.path); - await this.core.fileHandler.storeFileToDB(file); - } else { - this._log( - `STORAGE -> DB : ${file.path} has been skipped due to file size exceeding the limit`, - LOG_LEVEL_NOTICE - ); - } - break; - case TARGET_IS_NEW: - if (!this.services.vault.isFileSizeTooLarge(doc.size)) { - this._log("STORAGE <- DB :" + file.path); - if (await this.core.fileHandler.dbToStorage(doc, stripAllPrefixes(file.path), true)) { - eventHub.emitEvent("event-file-changed", { - file: file.path, - automated: true, - }); - } else { - this._log(`STORAGE <- DB : Cloud not read ${file.path}, possibly deleted`, LOG_LEVEL_NOTICE); - } - return caches; - } else { - this._log( - `STORAGE <- DB : ${file.path} has been skipped due to file size exceeding the limit`, - LOG_LEVEL_NOTICE - ); - } - break; - case EVEN: - this._log("STORAGE == DB :" + file.path + "", LOG_LEVEL_DEBUG); - break; - default: - this._log("STORAGE ?? DB :" + file.path + " Something got weird"); - } - } - - // This method uses an old version of database accessor, which is not recommended. - // TODO: Fix - async collectDeletedFiles() { - const limitDays = this.settings.automaticallyDeleteMetadataOfDeletedFiles; - if (limitDays <= 0) return; - this._log(`Checking expired file history`); - const limit = Date.now() - 86400 * 1000 * limitDays; - const notes: { - path: string; - mtime: number; - ttl: number; - doc: PouchDB.Core.ExistingDocument; - }[] = []; - for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) { - if (isAnyNote(doc)) { - if (doc.deleted && doc.mtime - limit < 0) { - notes.push({ - path: this.getPath(doc), - mtime: doc.mtime, - ttl: (doc.mtime - limit) / 1000 / 86400, - doc: doc, - }); - } - } - } - if (notes.length == 0) { - this._log("There are no old documents"); - this._log(`Checking expired file history done`); - return; - } - for (const v of notes) { - this._log(`Deletion history expired: ${v.path}`); - const delDoc = v.doc; - delDoc._deleted = true; - await this.localDatabase.putRaw(delDoc); - } - this._log(`Checking expired file history done`); - } - - private async _initializeDatabase( - showingNotice: boolean = false, - reopenDatabase = true, - ignoreSuspending: boolean = false - ): Promise { - this.services.appLifecycle.resetIsReady(); - if ( - !reopenDatabase || - (await this.services.database.openDatabase({ - databaseEvents: this.services.databaseEvents, - replicator: this.services.replicator, - })) - ) { - if (this.localDatabase.isReady) { - await this.services.vault.scanVault(showingNotice, ignoreSuspending); - } - const ERR_INITIALISATION_FAILED = `Initializing database has been failed on some module!`; - if (!(await this.services.databaseEvents.onDatabaseInitialised(showingNotice))) { - this.logDetectedError(ERR_INITIALISATION_FAILED, LOG_LEVEL_NOTICE); - return false; - } - this.resetDetectedError(ERR_INITIALISATION_FAILED); - this.services.appLifecycle.markIsReady(); - // run queued event once. - await this.services.fileProcessing.commitPendingFileEvents(); - return true; - } else { - this.services.appLifecycle.resetIsReady(); - return false; - } - } - private _reportDetectedErrors(): Promise { - return Promise.resolve(Array.from(this._detectedErrors)); - } - override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void { - services.appLifecycle.getUnresolvedMessages.addHandler(this._reportDetectedErrors.bind(this)); - services.databaseEvents.initialiseDatabase.addHandler(this._initializeDatabase.bind(this)); - services.vault.scanVault.addHandler(this._performFullScan.bind(this)); - } -} diff --git a/src/modules/essentialObsidian/ModuleCheckRemoteSize_obsolete.ts b/src/modules/essentialObsidian/ModuleCheckRemoteSize_obsolete.ts deleted file mode 100644 index 7d1c9a3..0000000 --- a/src/modules/essentialObsidian/ModuleCheckRemoteSize_obsolete.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; -import { sizeToHumanReadable } from "octagonal-wheels/number"; -import { $msg } from "src/lib/src/common/i18n.ts"; -import type { LiveSyncCore } from "../../main.ts"; -import { EVENT_REQUEST_CHECK_REMOTE_SIZE, eventHub } from "@/common/events.ts"; -import { AbstractModule } from "../AbstractModule.ts"; - -export class ModuleCheckRemoteSize extends AbstractModule { - checkRemoteSize(): Promise { - this.settings.notifyThresholdOfRemoteStorageSize = 1; - return this._allScanStat(); - } - - private async _allScanStat(): Promise { - if (this.services.API.isOnline === false) { - this._log("Network is offline, skipping remote size check.", LOG_LEVEL_INFO); - return true; - } - this._log($msg("moduleCheckRemoteSize.logCheckingStorageSizes"), LOG_LEVEL_VERBOSE); - if (this.settings.notifyThresholdOfRemoteStorageSize < 0) { - const message = $msg("moduleCheckRemoteSize.msgSetDBCapacity"); - const ANSWER_0 = $msg("moduleCheckRemoteSize.optionNoWarn"); - const ANSWER_800 = $msg("moduleCheckRemoteSize.option800MB"); - const ANSWER_2000 = $msg("moduleCheckRemoteSize.option2GB"); - const ASK_ME_NEXT_TIME = $msg("moduleCheckRemoteSize.optionAskMeLater"); - - const ret = await this.core.confirm.askSelectStringDialogue( - message, - [ANSWER_0, ANSWER_800, ANSWER_2000, ASK_ME_NEXT_TIME], - { - defaultAction: ASK_ME_NEXT_TIME, - title: $msg("moduleCheckRemoteSize.titleDatabaseSizeNotify"), - timeout: 40, - } - ); - if (ret == ANSWER_0) { - this.settings.notifyThresholdOfRemoteStorageSize = 0; - await this.saveSettings(); - } else if (ret == ANSWER_800) { - this.settings.notifyThresholdOfRemoteStorageSize = 800; - await this.saveSettings(); - } else if (ret == ANSWER_2000) { - this.settings.notifyThresholdOfRemoteStorageSize = 2000; - await this.saveSettings(); - } - } - if (this.settings.notifyThresholdOfRemoteStorageSize > 0) { - const remoteStat = await this.core.replicator?.getRemoteStatus(this.settings); - if (remoteStat) { - const estimatedSize = remoteStat.estimatedSize; - if (estimatedSize) { - const maxSize = this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024; - if (estimatedSize > maxSize) { - const message = $msg("moduleCheckRemoteSize.msgDatabaseGrowing", { - estimatedSize: sizeToHumanReadable(estimatedSize), - maxSize: sizeToHumanReadable(maxSize), - }); - const newMax = ~~(estimatedSize / 1024 / 1024) + 100; - const ANSWER_ENLARGE_LIMIT = $msg("moduleCheckRemoteSize.optionIncreaseLimit", { - newMax: newMax.toString(), - }); - const ANSWER_REBUILD = $msg("moduleCheckRemoteSize.optionRebuildAll"); - const ANSWER_IGNORE = $msg("moduleCheckRemoteSize.optionDismiss"); - const ret = await this.core.confirm.askSelectStringDialogue( - message, - [ANSWER_ENLARGE_LIMIT, ANSWER_REBUILD, ANSWER_IGNORE], - { - defaultAction: ANSWER_IGNORE, - title: $msg("moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded"), - timeout: 60, - } - ); - if (ret == ANSWER_REBUILD) { - const ret = await this.core.confirm.askYesNoDialog( - $msg("moduleCheckRemoteSize.msgConfirmRebuild"), - { defaultOption: "No" } - ); - if (ret == "yes") { - this.core.settings.notifyThresholdOfRemoteStorageSize = -1; - await this.saveSettings(); - await this.core.rebuilder.scheduleRebuild(); - } - } else if (ret == ANSWER_ENLARGE_LIMIT) { - this.settings.notifyThresholdOfRemoteStorageSize = ~~(estimatedSize / 1024 / 1024) + 100; - this._log( - $msg("moduleCheckRemoteSize.logThresholdEnlarged", { - size: this.settings.notifyThresholdOfRemoteStorageSize.toString(), - }), - LOG_LEVEL_NOTICE - ); - // await this.core.saveSettings(); - await this.core.services.setting.saveSettingData(); - } else { - // Dismiss or Close the dialog - } - - this._log( - $msg("moduleCheckRemoteSize.logExceededWarning", { - measuredSize: sizeToHumanReadable(estimatedSize), - notifySize: sizeToHumanReadable( - this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024 - ), - }), - LOG_LEVEL_INFO - ); - } else { - this._log( - $msg("moduleCheckRemoteSize.logCurrentStorageSize", { - measuredSize: sizeToHumanReadable(estimatedSize), - }), - LOG_LEVEL_INFO - ); - } - } - } - } - return true; - } - - private _everyOnloadStart(): Promise { - this.addCommand({ - id: "livesync-reset-remote-size-threshold-and-check", - name: "Reset notification threshold and check the remote database usage", - callback: async () => { - await this.checkRemoteSize(); - }, - }); - eventHub.onEvent(EVENT_REQUEST_CHECK_REMOTE_SIZE, () => this.checkRemoteSize()); - return Promise.resolve(true); - } - override onBindFunction(core: LiveSyncCore, services: typeof core.services): void { - services.appLifecycle.onScanningStartupIssues.addHandler(this._allScanStat.bind(this)); - services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this)); - } -} diff --git a/src/modules/essentialObsidian/ModuleObsidianEvents.ts b/src/modules/essentialObsidian/ModuleObsidianEvents.ts index 7445e11..33f0485 100644 --- a/src/modules/essentialObsidian/ModuleObsidianEvents.ts +++ b/src/modules/essentialObsidian/ModuleObsidianEvents.ts @@ -121,7 +121,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule { return; } - const isHidden = document.hidden; + const isHidden = activeWindow.document.hidden; if (this.isLastHidden === isHidden) { return; } @@ -134,7 +134,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule { } else { // suspend all temporary. if (this.services.appLifecycle.isSuspended()) return; - if (!this.hasFocus) return; + // Do not block resume by focus state here; visibility recovery should be enough. await this.services.appLifecycle.onResuming(); await this.services.appLifecycle.onResumed(); } diff --git a/src/modules/features/ModuleLog.ts b/src/modules/features/ModuleLog.ts index 16de2d9..1eb7945 100644 --- a/src/modules/features/ModuleLog.ts +++ b/src/modules/features/ModuleLog.ts @@ -25,7 +25,7 @@ import { EVENT_ON_UNRESOLVED_ERROR, } from "../../common/events.ts"; import { AbstractObsidianModule } from "../AbstractObsidianModule.ts"; -import { addIcon, normalizePath, Notice } from "../../deps.ts"; +import { addIcon, debounce, normalizePath, Notice, stringifyYaml, type WorkspaceLeaf } from "../../deps.ts"; import { LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/logger"; import { LogPaneView, VIEW_TYPE_LOG } from "./Log/LogPaneView.ts"; import { serialized } from "octagonal-wheels/concurrency/lock"; @@ -41,6 +41,8 @@ import { } from "@lib/string_and_binary/path.ts"; import { MARK_LOG_NETWORK_ERROR, MARK_LOG_SEPARATOR } from "@lib/services/lib/logUtils.ts"; import { NetworkWarningStyles } from "@lib/common/models/setting.const.ts"; +import { compatGlobal } from "@lib/common/coreEnvFunctions.ts"; +import { generateReport } from "@/common/reportTool.ts"; // This module cannot be a core module because it depends on the Obsidian UI. @@ -50,18 +52,51 @@ const globalLogFunction = (message: any, level?: number, key?: string) => { const messageX = message instanceof Error ? new LiveSyncError("[Error Logged]: " + message.message, { cause: message }) - : message; + : typeof message === "string" + ? message + : JSON.stringify(message); const entry = { message: messageX, level, key } as LogEntry; recentLogEntries.value = [...recentLogEntries.value, entry]; }; setGlobalLogFunction(globalLogFunction); -let recentLogs = [] as string[]; +// Keep the recent logs in memory for display, but also keep a longer history in logForDump for when the user wants to see more logs. +// logForDump is not reactive and is only used for dumping logs when requested, while recentLogs is reactive and is used for displaying logs in the UI. +const logForDump = [] as string[]; function addLog(log: string) { - recentLogs = [...recentLogs, log].splice(-200); - logMessages.value = recentLogs; + logForDump.push(log); + while (logForDump.length > 1000) { + logForDump.shift(); + } } + +// Display log is kept separate from the full log history to optimize performance and memory usage. +// And debounce the updates to the display log to avoid excessive UI updates when there are many log entries in a short time. +const logForDisplay = [] as string[]; + +const updateLogMessage = debounce(() => { + logMessages.value = [...logForDisplay]; +}, 25); +function addDisplayLog(log: string) { + logForDisplay.push(log); + while (logForDisplay.length > 200) { + logForDisplay.shift(); + } + updateLogMessage(); +} + +const redactPatterns = [/PBKDF2 salt \(Security Seed\):.*$/]; +function redactLog(log: string) { + let redactedLog = log; + for (const pattern of redactPatterns) { + redactedLog = redactedLog.replace(pattern, (match) => { + return match.split(":")[0] + ": [REDACTED]"; + }); + } + return redactedLog; +} + // logStore.intercept(e => e.slice(Math.min(e.length - 200, 0))); const showDebugLog = false; @@ -86,15 +121,15 @@ export class ModuleLog extends AbstractObsidianModule { // const emptyMark = `\u{2003}`; function padLeftSpComputed(numI: ReactiveValue, mark: string) { const formatted = reactiveSource(""); - let timer: ReturnType | undefined = undefined; + let timer: number | undefined = undefined; let maxLen = 1; numI.onChanged((numX) => { const num = numX.value; const numLen = `${Math.abs(num)}`.length + 1; maxLen = maxLen < numLen ? numLen : maxLen; - if (timer) clearTimeout(timer); + if (timer) compatGlobal.clearTimeout(timer); if (num == 0) { - timer = setTimeout(() => { + timer = compatGlobal.setTimeout(() => { formatted.value = ""; maxLen = 1; }, 3000); @@ -323,7 +358,7 @@ export class ModuleLog extends AbstractObsidianModule { if (this.nextFrameQueue) { return; } - this.nextFrameQueue = requestAnimationFrame(() => { + this.nextFrameQueue = compatGlobal.requestAnimationFrame(() => { this.nextFrameQueue = undefined; const { message, status } = this.statusBarLabels.value; // const recent = logMessages.value; @@ -346,7 +381,8 @@ export class ModuleLog extends AbstractObsidianModule { (a, b) => (a < b.ttl ? a : b.ttl), Number.MAX_SAFE_INTEGER ); - if (this.logLines.length > 0) setTimeout(() => this.applyStatusBarText(), minimumNext - now); + if (this.logLines.length > 0) + compatGlobal.setTimeout(() => this.applyStatusBarText(), minimumNext - now); const recent = this.logLines.map((e) => e.message); const recentLogs = recent.reverse().join("\n"); if (isDirty("recentLogs", recentLogs)) this.logHistory!.innerText = recentLogs; @@ -368,7 +404,7 @@ export class ModuleLog extends AbstractObsidianModule { if (this.statusDiv) { this.statusDiv.remove(); } - document.querySelectorAll(`.livesync-status`)?.forEach((e) => e.remove()); + compatGlobal.document.querySelectorAll(`.livesync-status`)?.forEach((e) => e.remove()); return Promise.resolve(true); } _everyOnloadStart(): Promise { @@ -390,7 +426,28 @@ export class ModuleLog extends AbstractObsidianModule { void this.services.API.showWindow(VIEW_TYPE_LOG); }, }); - this.registerView(VIEW_TYPE_LOG, (leaf) => new LogPaneView(leaf, this.plugin)); + this.addCommand({ + id: "dump-debug-info", + name: "Generate full report for opening the issue with debug info", + callback: async () => { + const recentLog = [...logForDump]; + const report = await generateReport(this.services.setting.currentSettings(), this.core); + const info = { + ...report, + recentLog: recentLog.map(redactLog), + }; + const yaml = `\`\`\`\` +# ---- Debug Info Dump ---- +${stringifyYaml(info)} +\`\`\`\``; + if (await this.services.UI.promptCopyToClipboard("Debug info", yaml)) { + new Notice( + "Debug info copied to clipboard. You can paste it in the issue. Be careful as it may contain sensitive information, review it before sharing." + ); + } + }, + }); + this.registerView(VIEW_TYPE_LOG, (leaf: WorkspaceLeaf) => new LogPaneView(leaf, this.plugin)); return Promise.resolve(true); } private _everyOnloadAfterLoadSettings(): Promise { @@ -404,7 +461,7 @@ export class ModuleLog extends AbstractObsidianModule { void this.setFileStatus(); }); - const w = document.querySelectorAll(`.livesync-status`); + const w = compatGlobal.document.querySelectorAll(`.livesync-status`); w.forEach((e) => e.remove()); this.observeForLogs(); @@ -421,6 +478,8 @@ export class ModuleLog extends AbstractObsidianModule { this.statusBar?.addClass("syncstatusbar"); } this.adjustStatusDivPosition(); + this._log("Log module loaded", LOG_LEVEL_INFO); + this._log("Verbose log", LOG_LEVEL_VERBOSE); return Promise.resolve(true); } @@ -444,11 +503,12 @@ export class ModuleLog extends AbstractObsidianModule { if (level == LOG_LEVEL_DEBUG && !showDebugLog) { return; } + let memoOnly = false; if (level <= LOG_LEVEL_INFO && this.settings && this.settings.lessInformationInLog) { - return; + memoOnly = true; } if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL_VERBOSE) { - return; + memoOnly = true; } const vaultName = this.services.vault.getVaultName(); const now = new Date(); @@ -469,6 +529,15 @@ export class ModuleLog extends AbstractObsidianModule { ? `${errorInfo}` : JSON.stringify(message, null, 2); const newMessage = timestamp + "->" + messageContent; + + if (this.settings?.writeLogToTheFile) { + this.writeLogToTheFile(now, vaultName, newMessage); + } + addLog(newMessage); + if (memoOnly) { + return; + } + addDisplayLog(newMessage); if (message instanceof Error) { console.error(vaultName + ":" + newMessage); } else if (level >= LOG_LEVEL_INFO) { @@ -479,10 +548,6 @@ export class ModuleLog extends AbstractObsidianModule { if (!this.settings?.showOnlyIconsOnEditor) { this.statusLog.value = messageContent; } - if (this.settings?.writeLogToTheFile) { - this.writeLogToTheFile(now, vaultName, newMessage); - } - addLog(newMessage); this.logLines.push({ ttl: now.getTime() + 3000, message: newMessage }); if (level >= LOG_LEVEL_NOTICE) { diff --git a/src/modules/features/SettingDialogue/PaneHatch.ts b/src/modules/features/SettingDialogue/PaneHatch.ts index ebfb377..d9ca27c 100644 --- a/src/modules/features/SettingDialogue/PaneHatch.ts +++ b/src/modules/features/SettingDialogue/PaneHatch.ts @@ -39,6 +39,7 @@ import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts"; import { generateCredentialObject } from "../../../lib/src/replication/httplib.ts"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import type { PageFunctions } from "./SettingPane.ts"; +import { generateReport } from "@/common/reportTool.ts"; export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void { // const hatchWarn = this.createEl(paneEl, "div", { text: `To stop the boot up sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` }); // hatchWarn.addClass("op-warn-info"); @@ -69,140 +70,14 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, eventHub.emitEvent(EVENT_REQUEST_RUN_FIX_INCOMPLETE); }) ); + new Setting(paneEl).setName($msg("Prepare the 'report' to create an issue")).addButton((button) => button .setButtonText($msg("Copy Report to clipboard")) .setCta() .setDisabled(false) .onClick(async () => { - let responseConfig: any = {}; - const REDACTED = "𝑅𝐸𝐷𝐴𝐢𝑇𝐸𝐷"; - if (this.editingSettings.remoteType == REMOTE_COUCHDB) { - try { - const credential = generateCredentialObject(this.editingSettings); - const customHeaders = parseHeaderValues(this.editingSettings.couchDB_CustomHeaders); - const r = await requestToCouchDBWithCredentials( - this.editingSettings.couchDB_URI, - credential, - window.origin, - undefined, - undefined, - undefined, - customHeaders - ); - - Logger(JSON.stringify(r.json, null, 2)); - - responseConfig = r.json; - responseConfig["couch_httpd_auth"].secret = REDACTED; - responseConfig["couch_httpd_auth"].authentication_db = REDACTED; - responseConfig["couch_httpd_auth"].authentication_redirect = REDACTED; - responseConfig["couchdb"].uuid = REDACTED; - responseConfig["admins"] = REDACTED; - delete responseConfig["jwt_keys"]; - if ("secret" in responseConfig["chttpd_auth"]) - responseConfig["chttpd_auth"].secret = REDACTED; - } catch (ex) { - Logger(ex, LOG_LEVEL_VERBOSE); - responseConfig = { - error: "Requesting information from the remote CouchDB has failed. If you are using IBM Cloudant, this is normal behaviour.", - }; - } - } else if (this.editingSettings.remoteType == REMOTE_MINIO) { - responseConfig = { error: "Object Storage Synchronisation" }; - // - } - const defaultKeys = Object.keys(DEFAULT_SETTINGS) as (keyof ObsidianLiveSyncSettings)[]; - const pluginConfig = JSON.parse(JSON.stringify(this.editingSettings)) as ObsidianLiveSyncSettings; - const pluginKeys = Object.keys(pluginConfig); - for (const key of pluginKeys) { - if (defaultKeys.includes(key as any)) continue; - delete pluginConfig[key as keyof ObsidianLiveSyncSettings]; - } - - pluginConfig.couchDB_DBNAME = REDACTED; - pluginConfig.couchDB_PASSWORD = REDACTED; - const scheme = pluginConfig.couchDB_URI.startsWith("http:") - ? "(HTTP)" - : pluginConfig.couchDB_URI.startsWith("https:") - ? "(HTTPS)" - : ""; - pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI) - ? "cloudant" - : `self-hosted${scheme}`; - pluginConfig.couchDB_USER = REDACTED; - pluginConfig.passphrase = REDACTED; - pluginConfig.encryptedPassphrase = REDACTED; - pluginConfig.encryptedCouchDBConnection = REDACTED; - pluginConfig.accessKey = REDACTED; - pluginConfig.secretKey = REDACTED; - const redact = (source: string) => `${REDACTED}(${source.length} letters)`; - const toSchemeOnly = (uri: string) => { - try { - return `${new URL(uri).protocol}//`; - } catch { - const matched = uri.match(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//); - return matched?.[0] ?? REDACTED; - } - }; - pluginConfig.remoteConfigurations = Object.fromEntries( - Object.entries(pluginConfig.remoteConfigurations || {}).map(([id, config]) => [ - id, - { - ...config, - uri: toSchemeOnly(config.uri), - }, - ]) - ); - pluginConfig.region = redact(pluginConfig.region); - pluginConfig.bucket = redact(pluginConfig.bucket); - pluginConfig.pluginSyncExtendedSetting = {}; - pluginConfig.P2P_AppID = redact(pluginConfig.P2P_AppID); - pluginConfig.P2P_passphrase = redact(pluginConfig.P2P_passphrase); - pluginConfig.P2P_roomID = redact(pluginConfig.P2P_roomID); - pluginConfig.P2P_relays = redact(pluginConfig.P2P_relays); - pluginConfig.jwtKey = redact(pluginConfig.jwtKey); - pluginConfig.jwtSub = redact(pluginConfig.jwtSub); - pluginConfig.jwtKid = redact(pluginConfig.jwtKid); - pluginConfig.bucketCustomHeaders = redact(pluginConfig.bucketCustomHeaders); - pluginConfig.couchDB_CustomHeaders = redact(pluginConfig.couchDB_CustomHeaders); - pluginConfig.P2P_turnCredential = redact(pluginConfig.P2P_turnCredential); - pluginConfig.P2P_turnUsername = redact(pluginConfig.P2P_turnUsername); - pluginConfig.P2P_turnServers = `(${pluginConfig.P2P_turnServers.split(",").length} servers configured)`; - const endpoint = pluginConfig.endpoint; - if (endpoint == "") { - pluginConfig.endpoint = "Not configured or AWS"; - } else { - const endpointScheme = pluginConfig.endpoint.startsWith("http:") - ? "(HTTP)" - : pluginConfig.endpoint.startsWith("https:") - ? "(HTTPS)" - : ""; - pluginConfig.endpoint = `${endpoint.indexOf(".r2.cloudflarestorage.") !== -1 ? "R2" : "self-hosted?"}(${endpointScheme})`; - } - const obsidianInfo = { - navigator: navigator.userAgent, - fileSystem: this.core.services.vault.isStorageInsensitive() ? "insensitive" : "sensitive", - }; - const msgConfig = `# ---- Obsidian info ---- -${stringifyYaml(obsidianInfo)} ---- -# ---- remote config ---- -${stringifyYaml(responseConfig)} ---- -# ---- Plug-in config ---- -${stringifyYaml({ - version: this.manifestVersion, - ...pluginConfig, -})}`; - console.log(msgConfig); - if ((await this.services.UI.promptCopyToClipboard("Generated report", msgConfig)) == true) { - // await navigator.clipboard.writeText(msgConfig); - // Logger( - // `Generated report has been copied to clipboard. Please report the issue with this! Thank you for your cooperation!`, - // LOG_LEVEL_NOTICE - // ); - } + await this.app.commands.executeCommandById("obsidian-livesync:dump-debug-info"); }) ); new Setting(paneEl) diff --git a/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte b/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte index 2478521..b4dd9ea 100644 --- a/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte +++ b/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte @@ -48,6 +48,8 @@ bind:value={userType} > This is an advanced option for users who do not have a URI or who wish to configure detailed settings. + You can also select this option if you intend to use P2P (Peer-to-Peer) synchronisation + instead of a CouchDB/S3 server β€” P2P requires no server setup at all. diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte index b07c157..cd2020e 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte @@ -1,13 +1,13 @@