mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-08 08:41:50 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
124a49b80f | ||
|
|
3e76292aa7 | ||
|
|
4634ab73b1 | ||
|
|
359c10f1d7 | ||
|
|
59ebac3efc | ||
|
|
b4edca3a99 | ||
|
|
4b76b10a6f | ||
|
|
d4b53280e3 | ||
|
|
dbd9b17b20 | ||
|
|
dcfb9867f2 | ||
|
|
46ff17fdf3 | ||
|
|
728dabce60 | ||
|
|
3783fc6926 | ||
|
|
236f2293ce | ||
|
|
4cb908cf62 | ||
|
|
fab2327937 | ||
|
|
0837648aa6 | ||
|
|
58dcc13b50 | ||
|
|
e2da4ec454 | ||
|
|
f613f1b887 | ||
|
|
88ef7c316a | ||
|
|
3fbecdf567 | ||
|
|
5db3a374a9 | ||
|
|
6f76f90075 | ||
|
|
9acf9fe093 | ||
|
|
1e3de47d92 | ||
|
|
a50f0965f6 | ||
|
|
9d3aa35b0b |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "src/lib"]
|
||||||
|
path = src/lib
|
||||||
|
url = https://github.com/vrtmrz/livesync-commonlib
|
||||||
123
README.md
123
README.md
@@ -1,32 +1,25 @@
|
|||||||
# Self-hosted LiveSync
|
# Self-hosted LiveSync
|
||||||
|
|
||||||
Sorry for late! [Japanese docs](./README_ja.md) is also coming up.
|
[Japanese docs](./README_ja.md).
|
||||||
|
|
||||||
**Renamed from: obsidian-livesync**
|
Self-hosted LiveSync is a community implemented synchronization plugin.
|
||||||
|
It uses Self-hosted or you procured CouchDB as the server. Available on every obsidian installed devices.
|
||||||
Using a self-hosted database, live-sync to multi-devices bidirectionally.
|
Note: It has no compatibilities with official "Sync".
|
||||||
Runs in Mac, Android, Windows, and iOS. Perhaps available on Linux too.
|
|
||||||
Community implementation, not compatible with official "Sync".
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
**It's getting almost stable now, But Please make sure to back your vault up!**
|
If you install or upgrade LiveSync, please back your vault up.
|
||||||
|
|
||||||
Limitations: ~~Folder deletion handling is not completed.~~ **It would work now.**
|
## Features
|
||||||
|
|
||||||
## This plugin enables...
|
- Visual conflict resolver included.
|
||||||
|
- Synchronize with other devices bidirectionally near-real-time
|
||||||
- Runs in Windows, Mac, iPad, iPhone, Android, Chromebook
|
- You can use CouchDB or its compatibles like IBM Cloudant.
|
||||||
- Synchronize to Self-hosted Database
|
- End-to-End encryption.
|
||||||
- Replicate to/from other devices bidirectionally near-real-time
|
- Plugin synchronization(Beta)
|
||||||
- Resolving synchronizing conflicts in the Obsidian.
|
|
||||||
- You can use CouchDB or its compatibles like IBM Cloudant. CouchDB is OSS, and IBM Cloudant has the terms and certificates about security. Your notes are yours.
|
|
||||||
- Off-line sync is also available.
|
|
||||||
- End-to-End encryption is available (beta).
|
|
||||||
- Receive WebClip from [obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf) (End-to-End encryption will not be applicable.)
|
- Receive WebClip from [obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf) (End-to-End encryption will not be applicable.)
|
||||||
|
|
||||||
It must be useful for the Researcher, Engineer, Developer who has to keep NDA or something like agreement.
|
It must be useful for the Researcher, Engineer, Developer who has to keep NDA or something like agreement. Especially, in some companies, people have to store all data to their fully controlled host, even End-to-End encryption applied.
|
||||||
Especially, in some companies, people have to store all data to their fully controlled host, even End-to-End encryption applied.
|
|
||||||
|
|
||||||
## IMPORTANT NOTICE
|
## IMPORTANT NOTICE
|
||||||
|
|
||||||
@@ -36,35 +29,68 @@ Especially, in some companies, people have to store all data to their fully cont
|
|||||||
- When the device's storage has been run out, Database corruption may happen.
|
- When the device's storage has been run out, Database corruption may happen.
|
||||||
- When editing hidden files or any other invisible files from obsidian, the file wouldn't be kept in the database. (**Or be deleted.**)
|
- When editing hidden files or any other invisible files from obsidian, the file wouldn't be kept in the database. (**Or be deleted.**)
|
||||||
|
|
||||||
## Supplements
|
|
||||||
|
|
||||||
- When the file has been deleted, the deletion of the file is replicated to other devices.
|
|
||||||
- When the folder became empty by replication, The folder will be deleted in the default setting. But you can change this behaivour. Check the [Settings](docs/settings.md).
|
|
||||||
- LiveSync drains many batteries in mobile devices.
|
|
||||||
- Mobile Obsidian can not connect to the non-secure(HTTP) or local CA-signed servers, even though the certificate is stored in the device store.
|
|
||||||
- There are no 'exclude_folders' like configurations.
|
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
1. Install from Obsidian, or download from this repo's releases, copy `main.js`, `styles.css` and `manifest.json` into `[your-vault]/.obsidian/plugins/`
|
### Get your database ready.
|
||||||
2. Get your database. IBM Cloudant is preferred for testing. Or you can use your own server with CouchDB.
|
|
||||||
For more information, refer below:
|
First, get your database ready. IBM Cloudant is preferred for testing. Or you can use your own server with CouchDB. For more information, refer below:
|
||||||
1. [Setup IBM Cloudant](docs/setup_cloudant.md)
|
1. [Setup IBM Cloudant](docs/setup_cloudant.md)
|
||||||
2. [Setup your CouchDB](docs/setup_own_server.md)
|
2. [Setup your CouchDB](docs/setup_own_server.md)
|
||||||
3. Enter connection information to Plugin's setting dialog. In details, refer [Settings of Self-hosted LiveSync](docs/settings.md)
|
|
||||||
4. Enable LiveSync or other Synchronize method as you like.
|
### First device
|
||||||
|
|
||||||
|
1. Install the plugin on your device.
|
||||||
|
2. Configure with the remote database.
|
||||||
|
1. Fill your server's information into the `Remote Database configuration` pane.
|
||||||
|
2. Enabling `End to End Encryption` is recommended. After inputting the passphrase, you have to press `Just apply`.
|
||||||
|
3. Hit `Test Database Connection` and make sure that the plugin says `Connected`.
|
||||||
|
4. Hit `Check database configuration` and make sure all tests have been passed.
|
||||||
|
3. Configure how to synchronize on `Sync setting`. (You can leave these configures later)
|
||||||
|
1. If you want to synchronize in real-time, enable `LiveSync`.
|
||||||
|
2. Or, set up the synchronization as you like.
|
||||||
|
3. Additional configuration is also here. I recommend enabling `Use Trash for deleted files, but you can leave all configurations disabled.
|
||||||
|
4. Configure miscellaneous features.
|
||||||
|
1. Enabling `Show staus inside editor` bring you information. While edit mode, you can see the status on the top-right of the editor. (Recommended)
|
||||||
|
2. Enabling `Use history` let you see the diffs between your edit and synchronization. (Recommended)
|
||||||
|
5. Back to the editor. I hope that initial scan is in the progress or done.
|
||||||
|
6. When status became stabilized (All ⏳ and 🧩 have been disappeared), you are ready to synchronize with the server.
|
||||||
|
7. Press the replicate icon on the Ribbon or run `Replicate now` from the Command pallet. You'll send all your data to the server.
|
||||||
|
8. Open the command palette, `Copy setup URI`, and set the passphrase to encrypt the information. Then your configuration will be copied to the clipboard. Please share copied URI with your other devices.
|
||||||
|
**IMPORTANT NOTICE: BE CAREFUL TO TREAT THIS URI. THE URI CONTAINS YOUR CREDENTIALS EVEN THOUGH NOBODY COULD READ WITHOUT THE PASSPHRASE.**
|
||||||
|
|
||||||
|
### Subsequent Devices
|
||||||
|
|
||||||
|
Strongly recommend using the vault in which all files are completely synchronized including timestamps. Otherwise, some files will be corrupted if failed to resolve conflicts. To simplify, I recommend using a new empty vault.
|
||||||
|
|
||||||
|
1. Install the plug-in.
|
||||||
|
2. Open the link that you had been copied to the other device.
|
||||||
|
3. The plug-in asks you that are you sure to apply the configurations. Please answer `Yes` and the following instruction below:
|
||||||
|
1. Answer `Yes` to `Keep local DB?`.
|
||||||
|
*Note: If you started with existed vault, you have to answer `No`. And `No` to `Rebuild the database?`.*
|
||||||
|
2. Answer `Yes` to `Keep remote DB?`.
|
||||||
|
3. Answer `Yes` to `Replicate once?`.
|
||||||
|
Yes, you have to answer `Yes` to everything.
|
||||||
|
Then, all your settings are copied from the first device.
|
||||||
|
4. Your notes will arrive soon.
|
||||||
|
|
||||||
|
## Something looks corrupted...
|
||||||
|
|
||||||
|
Please open the link again and Answer as below:
|
||||||
|
- If your local database looks corrupted
|
||||||
|
(in other words, when your Obsidian getting weird even standalone.)
|
||||||
|
- Answer `No` to `Keep local DB?`
|
||||||
|
- If your remote database looks corrupted
|
||||||
|
(in other words, when something happens while replicating)
|
||||||
|
- Answer `No` to `Keep remote DB?`
|
||||||
|
|
||||||
|
If you answered `No` to both, your databases will be rebuilt by the content on your device. And the remote database will lock out other devices. You have to synchronize all your devices again. (When this time, almost all your files should be synchronized including a timestamp. So you can use the existed vault).
|
||||||
|
|
||||||
## Test Server
|
## Test Server
|
||||||
|
|
||||||
Setting up an instance of Cloudant or local CouchDB is a little complicated, so I made the [Tasting server of self-hosted-livesync](https://olstaste.vrtmrz.net/) up. Try free!
|
Setting up an instance of Cloudant or local CouchDB is a little complicated, so I made the [Tasting server of self-hosted-livesync](https://olstaste.vrtmrz.net/) up. Try free!
|
||||||
Note: Please read "Limitations" carefully. Do not send your private vault.
|
Note: Please read "Limitations" carefully. Do not send your private vault.
|
||||||
|
|
||||||
## WebClipper is also available.
|
## Information in StatusBar
|
||||||
|
|
||||||
Available from on Chrome Web Store:[obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
|
|
||||||
Repo is here: [obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip). (Docs are work in progress.)
|
|
||||||
|
|
||||||
# Information in StatusBar
|
|
||||||
|
|
||||||
Synchronization status is shown in statusbar.
|
Synchronization status is shown in statusbar.
|
||||||
|
|
||||||
@@ -75,11 +101,16 @@ Synchronization status is shown in statusbar.
|
|||||||
- ⚠ Error occurred.
|
- ⚠ Error occurred.
|
||||||
- ↑ Uploaded pieces
|
- ↑ Uploaded pieces
|
||||||
- ↓ Downloaded pieces
|
- ↓ Downloaded pieces
|
||||||
- ⏳ Count of the pending process
|
- ⏳ Number of the pending processes
|
||||||
If you have deleted or renamed files, please wait until this disappears.
|
- 🧩 Number of the files that waiting for their chunks.
|
||||||
|
If you have deleted or renamed files, please wait until ⏳ disappeared.
|
||||||
|
|
||||||
# More supplements
|
|
||||||
|
|
||||||
|
## Hints
|
||||||
|
- When the folder became empty by replication, The folder will be deleted in the default setting. But you can change this behaivour. Check the [Settings](docs/settings.md).
|
||||||
|
- LiveSync mode drains many batteries in mobile devices. Periodic sync and some automatic sync is recommended.
|
||||||
|
- Mobile Obsidian can not connect to the non-secure(HTTP) or local CA-signed servers, even though the certificate is stored in the device store.
|
||||||
|
- There are no 'exclude_folders' like configurations.
|
||||||
- When synchronized, files are compared by their modified times and overwritten by the newer ones once. Then plugin checks the conflicts and if a merge is needed, the dialog will open.
|
- When synchronized, files are compared by their modified times and overwritten by the newer ones once. Then plugin checks the conflicts and if a merge is needed, the dialog will open.
|
||||||
- Rarely, the file in the database would be broken. The plugin will not write storage when it looks broken, so some old files must be on your device. If you edit the file, it will be cured. But if the file does not exist on any device, can not rescue it. So you can delete these items from the setting dialog.
|
- Rarely, the file in the database would be broken. The plugin will not write storage when it looks broken, so some old files must be on your device. If you edit the file, it will be cured. But if the file does not exist on any device, can not rescue it. So you can delete these items from the setting dialog.
|
||||||
- If your database looks corrupted, try "Drop History". Usually, It is the easiest way.
|
- If your database looks corrupted, try "Drop History". Usually, It is the easiest way.
|
||||||
@@ -87,7 +118,11 @@ If you have deleted or renamed files, please wait until this disappears.
|
|||||||
- Q: Database is growing, how can I shrink it up?
|
- Q: Database is growing, how can I shrink it up?
|
||||||
A: each of the docs is saved with their old 100 revisions to detect and resolve confliction. Picture yourself that one device has been off the line for a while, and joined again. The device has to check his note and remote saved note. If exists in revision histories of remote notes even though the device's note is a little different from the latest one, it could be merged safely. Even if that is not in revision histories, we only have to check differences after the revision that both devices commonly have. This is like The git's conflict resolving method. So, We have to make the database again like an enlarged git repo if you want to solve the root of the problem.
|
A: each of the docs is saved with their old 100 revisions to detect and resolve confliction. Picture yourself that one device has been off the line for a while, and joined again. The device has to check his note and remote saved note. If exists in revision histories of remote notes even though the device's note is a little different from the latest one, it could be merged safely. Even if that is not in revision histories, we only have to check differences after the revision that both devices commonly have. This is like The git's conflict resolving method. So, We have to make the database again like an enlarged git repo if you want to solve the root of the problem.
|
||||||
- And more technical Information are in the [Technical Information](docs/tech_info.md)
|
- And more technical Information are in the [Technical Information](docs/tech_info.md)
|
||||||
|
- If you want to synchronize files without obsidian, you can use [filesystem-livesync](https://github.com/vrtmrz/filesystem-livesync).
|
||||||
|
- WebClipper is also available.
|
||||||
|
Available from on Chrome Web Store:[obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
|
||||||
|
Repo is here: [obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip). (Docs are work in progress.)
|
||||||
|
|
||||||
# License
|
## License
|
||||||
|
|
||||||
The source code is licensed MIT.
|
The source code is licensed MIT.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
NOTE: This document surely became outdated. I'll improve this doc in a while. but your contributions are always welcome.
|
||||||
|
|
||||||
# Settings of this plugin
|
# Settings of this plugin
|
||||||
|
|
||||||
## Remote Database Configurations
|
## Remote Database Configurations
|
||||||
@@ -19,14 +21,51 @@ The Database name to synchronize.
|
|||||||
⚠️If not exist, created automatically.
|
⚠️If not exist, created automatically.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Use the old connecting method
|
### Use the old connecting method
|
||||||
Since v0.8.0, Self-hosted LiveSync uses Obsidian's API to connect to the CouchDB instead of the browser API.
|
This option has been removed at v0.10.0
|
||||||
This method will increase the performance and avoid troubles with the CORS.
|
|
||||||
But it doesn't been well tested yet. If you are troubled, please disable this option once.
|
|
||||||
|
|
||||||
|
### End to End Encryption
|
||||||
|
Encrypt your database. It affects only the database, your files are left as plain.
|
||||||
|
|
||||||
|
The encryption algorithm is AES-GCM.
|
||||||
|
|
||||||
|
Note: If you want to use "Plugins and their settings", you have to enable this.
|
||||||
|
|
||||||
|
### Passphrase
|
||||||
|
The passphrase to used as the key of encryption. Please use the long text.
|
||||||
|
|
||||||
|
### Apply
|
||||||
|
To enable End-to-End encryption, there must be no items of the same content encrypted with different passphrases to avoid attackers guessing passphrases. Self-hosted LiveSync uses crc32 of the chunks, It is really a must.
|
||||||
|
|
||||||
|
So, this plugin completely deletes everything from both local and remote databases before enabling it and then synchronizing again.
|
||||||
|
|
||||||
|
To enable, "Apply and send" from the most powerful device.
|
||||||
|
If you want to synchronize an existing database, set passphrase and press "Just apply".
|
||||||
|
|
||||||
|
- Apply and send
|
||||||
|
1. Initialize the Local Database and set (or clear) passphrase, put all files into the database again.
|
||||||
|
2. Initialize the Remote Database.
|
||||||
|
3. Lock the Remote Database.
|
||||||
|
4. Send it all.
|
||||||
|
|
||||||
|
This process is simply heavy. Using a PC or Mac is preferred.
|
||||||
|
- Apply and receive
|
||||||
|
1. Initialize the Local Database and set (or clear) the passphrase.
|
||||||
|
2. Unlock the Remote Database.
|
||||||
|
3. Retrieve all and decrypt to file.
|
||||||
|
|
||||||
|
When running these operations, every synchronization settings is disabled.
|
||||||
|
|
||||||
### Test Database connection
|
### Test Database connection
|
||||||
You can check the connection by clicking this button.
|
You can check the connection by clicking this button.
|
||||||
|
|
||||||
|
### Check database configuration
|
||||||
|
You can check and modify your CouchDB's configuration from here directly.
|
||||||
|
|
||||||
|
|
||||||
## Local Database Configurations
|
## Local Database Configurations
|
||||||
"Local Database" is created inside your obsidian.
|
"Local Database" is created inside your obsidian.
|
||||||
|
|
||||||
@@ -56,39 +95,6 @@ Note: If you want to use "Use history", this vault must be set to 0.
|
|||||||
### Manual Garbage Collect
|
### Manual Garbage Collect
|
||||||
Run "Garbage Collection" manually.
|
Run "Garbage Collection" manually.
|
||||||
|
|
||||||
### End to End Encryption
|
|
||||||
Encrypt your database. It affects only the database, your files are left as plain.
|
|
||||||
|
|
||||||
The encryption algorithm is AES-GCM.
|
|
||||||
|
|
||||||
Note: If you want to use "Plugins and their settings", you have to enable this.
|
|
||||||
|
|
||||||
### Passphrase
|
|
||||||
The passphrase to used as the key of encryption. Please use the long text.
|
|
||||||
|
|
||||||
### Apply
|
|
||||||
To enable End-to-End encryption, there must be no items of the same content encrypted with different passphrases to avoid attackers guessing passphrases. Self-hosted LiveSync uses crc32 of the chunks, It is really a must.
|
|
||||||
|
|
||||||
So, this plugin completely deletes everything from both local and remote databases before enabling it and then synchronizing again.
|
|
||||||
|
|
||||||
To enable, "Apply and send" from the most powerful device, and "Apply and receive" from every other device.
|
|
||||||
|
|
||||||
- Apply and send
|
|
||||||
1. Initialize the Local Database and set (or clear) passphrase, put all files into the database again.
|
|
||||||
2. Initialize the Remote Database.
|
|
||||||
3. Lock the Remote Database.
|
|
||||||
4. Send it all.
|
|
||||||
|
|
||||||
This process is simply heavy. Using a PC or Mac is preferred.
|
|
||||||
- Apply and receive
|
|
||||||
1. Initialize the Local Database and set (or clear) the passphrase.
|
|
||||||
2. Unlock the Remote Database.
|
|
||||||
3. Retrieve all and decrypt to file.
|
|
||||||
|
|
||||||
When running these operations, every synchronization settings is disabled.
|
|
||||||
|
|
||||||
**And even your passphrase is wrong, It doesn't be checked before the plugin really decrypts. So If you set the wrong passphrase and run "Apply and Receive", you will get an amount of decryption error. But, this is the specification.**
|
|
||||||
|
|
||||||
### minimum chunk size and LongLine threshold
|
### minimum chunk size and LongLine threshold
|
||||||
The configuration of chunk splitting.
|
The configuration of chunk splitting.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
注意:少し内容が古くなっています。
|
||||||
|
|
||||||
# このプラグインの設定項目
|
# このプラグインの設定項目
|
||||||
|
|
||||||
## Remote Database Configurations
|
## Remote Database Configurations
|
||||||
@@ -19,9 +21,44 @@ CouchDBのURIを入力します。Cloudantの場合は「External Endpoint(prefe
|
|||||||
⚠️存在しない場合は、テストや接続を行った際、自動的に作成されます[^1]。
|
⚠️存在しない場合は、テストや接続を行った際、自動的に作成されます[^1]。
|
||||||
[^1]:権限がない場合は自動作成には失敗します。
|
[^1]:権限がない場合は自動作成には失敗します。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### End to End Encryption
|
||||||
|
データベースを暗号化します。この効果はデータベースに格納されるデータに限られ、ディスク上のファイルは平文のままです。
|
||||||
|
暗号化はAES-GCMを使用して行っています。
|
||||||
|
|
||||||
|
### Passphrase
|
||||||
|
暗号化を行う際に使用するパスフレーズです。充分に長いものを使用してください。
|
||||||
|
|
||||||
|
### Apply
|
||||||
|
End to End 暗号化を行うに当たって、異なるパスフレーズで暗号化された同一の内容を入手されることは避けるべきです。また、Self-hosted LiveSyncはコンテンツのcrc32を重複回避に使用しているため、その点でも攻撃が有効になってしまいます。
|
||||||
|
|
||||||
|
そのため、End to End 暗号化を有効にする際には、ローカル、リモートすべてのデータベースをいったん破棄し、新しいパスフレーズで暗号化された内容のみを、改めて同期し直します。
|
||||||
|
|
||||||
|
有効化するには、一番体力のある端末からApply and sendを行います。
|
||||||
|
既に存在するリモートと同期する場合は、設定してJust applyを行ってください。
|
||||||
|
|
||||||
|
- Apply and send
|
||||||
|
1. ローカルのデータベースを初期化しパスフレーズを設定(またはクリア)します。その後、すべてのファイルをもう一度データベースに登録します。
|
||||||
|
2. リモートのデータベースを初期化します。
|
||||||
|
3. リモートのデータベースをロックし、他の端末を締め出します。
|
||||||
|
4. すべて再送信します。
|
||||||
|
|
||||||
|
負荷と時間がかかるため、デスクトップから行う方が好ましいです。
|
||||||
|
- Apply and receive
|
||||||
|
1. ローカルのデータベースを初期化し、パスフレーズを設定(またはクリア)します。
|
||||||
|
2. リモートのデータベースにかかっているロックを解除します。
|
||||||
|
3. すべて受信して、復号します。
|
||||||
|
|
||||||
|
どちらのオペレーションも、実行するとすべての同期設定が無効化されます。
|
||||||
|
|
||||||
|
|
||||||
### Test Database connection
|
### Test Database connection
|
||||||
上記の設定でデータベースに接続できるか確認します。
|
上記の設定でデータベースに接続できるか確認します。
|
||||||
|
|
||||||
|
### Check database configuration
|
||||||
|
ここから直接CouchDBの設定を確認・変更できます。
|
||||||
|
|
||||||
## Local Database Configurations
|
## Local Database Configurations
|
||||||
端末内に作成されるデータベースの設定です。
|
端末内に作成されるデータベースの設定です。
|
||||||
|
|
||||||
@@ -49,35 +86,6 @@ Obsidianでのファイル操作が終わってから指定秒数が経過した
|
|||||||
### Manual Garbage Collect
|
### Manual Garbage Collect
|
||||||
上記のGarbage Collectionを手動で行います。
|
上記のGarbage Collectionを手動で行います。
|
||||||
|
|
||||||
### End to End Encryption
|
|
||||||
データベースを暗号化します。この効果はデータベースに格納されるデータに限られ、ディスク上のファイルは平文のままです。
|
|
||||||
暗号化はAES-GCMを使用して行っています。
|
|
||||||
|
|
||||||
### Passphrase
|
|
||||||
暗号化を行う際に使用するパスフレーズです。充分に長いものを使用してください。
|
|
||||||
|
|
||||||
### Apply
|
|
||||||
End to End 暗号化を行うに当たって、異なるパスフレーズで暗号化された同一の内容を入手されることは避けるべきです。また、Self-hosted LiveSyncはコンテンツのcrc32を重複回避に使用しているため、その点でも攻撃が有効になってしまいます。
|
|
||||||
|
|
||||||
そのため、End to End 暗号化を有効にする際には、ローカル、リモートすべてのデータベースをいったん破棄し、新しいパスフレーズで暗号化された内容のみを、改めて同期し直します。
|
|
||||||
|
|
||||||
有効化するには、一番体力のある端末からApply and sendを行い、他の端末でApply and receiveを行います。
|
|
||||||
|
|
||||||
- Apply and send
|
|
||||||
1. ローカルのデータベースを初期化しパスフレーズを設定(またはクリア)します。その後、すべてのファイルをもう一度データベースに登録します。
|
|
||||||
2. リモートのデータベースを初期化します。
|
|
||||||
3. リモートのデータベースをロックし、他の端末を締め出します。
|
|
||||||
4. すべて再送信します。
|
|
||||||
|
|
||||||
負荷と時間がかかるため、デスクトップから行う方が好ましいです。
|
|
||||||
- Apply and receive
|
|
||||||
1. ローカルのデータベースを初期化し、パスフレーズを設定(またはクリア)します。
|
|
||||||
2. リモートのデータベースにかかっているロックを解除します。
|
|
||||||
3. すべて受信して、復号します。
|
|
||||||
|
|
||||||
どちらのオペレーションも、実行するとすべての同期設定が無効化されます。
|
|
||||||
**また、パスフレーズのチェックは、実際に復号するまで行いません。そのため、パスフレーズを間違えて設定し、Apply and receiveで同期を行うと、大量のエラーが発生します。これは仕様です。**
|
|
||||||
|
|
||||||
### minimum chunk size と LongLine threshold
|
### minimum chunk size と LongLine threshold
|
||||||
チャンクの分割についての設定です。
|
チャンクの分割についての設定です。
|
||||||
Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk size文字確保した上で、できるだけ効率的に同期できるよう、ノートを分割してチャンクを作成します。
|
Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk size文字確保した上で、できるだけ効率的に同期できるよう、ノートを分割してチャンクを作成します。
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.8.2",
|
"version": "0.11.7",
|
||||||
"minAppVersion": "0.9.12",
|
"minAppVersion": "0.9.12",
|
||||||
"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.",
|
"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",
|
"author": "vorotamoroz",
|
||||||
"authorUrl": "https://github.com/vrtmrz",
|
"authorUrl": "https://github.com/vrtmrz",
|
||||||
"isDesktopOnly": false
|
"isDesktopOnly": false
|
||||||
}
|
}
|
||||||
48
package-lock.json
generated
48
package-lock.json
generated
@@ -1,15 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.8.2",
|
"version": "0.11.7",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.8.2",
|
"version": "0.11.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
"esbuild": "0.13.12",
|
||||||
|
"esbuild-svelte": "^0.6.0",
|
||||||
|
"idb": "^7.0.1",
|
||||||
|
"svelte-preprocess": "^4.10.2",
|
||||||
"xxhash-wasm": "^0.4.2"
|
"xxhash-wasm": "^0.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -27,7 +31,7 @@
|
|||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-config-airbnb-base": "^14.2.1",
|
"eslint-config-airbnb-base": "^14.2.1",
|
||||||
"eslint-plugin-import": "^2.25.2",
|
"eslint-plugin-import": "^2.25.2",
|
||||||
"obsidian": "^0.13.30",
|
"obsidian": "^0.14.6",
|
||||||
"rollup": "^2.32.1",
|
"rollup": "^2.32.1",
|
||||||
"svelte-preprocess": "^4.10.2",
|
"svelte-preprocess": "^4.10.2",
|
||||||
"tslib": "^2.2.0",
|
"tslib": "^2.2.0",
|
||||||
@@ -2122,6 +2126,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/idb": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg=="
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.1.9",
|
"version": "5.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz",
|
||||||
@@ -2571,9 +2580,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/moment": {
|
"node_modules/moment": {
|
||||||
"version": "2.29.1",
|
"version": "2.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
|
||||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
|
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
@@ -2659,15 +2668,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/obsidian": {
|
"node_modules/obsidian": {
|
||||||
"version": "0.13.30",
|
"version": "0.14.6",
|
||||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.13.30.tgz",
|
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.14.6.tgz",
|
||||||
"integrity": "sha512-uAOrIyeHE9qYzg1Qjfpy/qlyLUFX9oyKWeHYO8NVDoI+pm5VUTMe7XWcsXPwb9iVsVmggVJcdV15Vqm9bljhxQ==",
|
"integrity": "sha512-oXPJ8Zt10WhN19bk5l4mZuXRZbbdT1QoMgxGGJ0bB7UcJa0bozDzugS5L/QiV9gDoujpUPxDWNVahEel6r0Fpw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^0.19.6",
|
"@codemirror/state": "^0.19.6",
|
||||||
"@codemirror/view": "^0.19.31",
|
"@codemirror/view": "^0.19.31",
|
||||||
"@types/codemirror": "0.0.108",
|
"@types/codemirror": "0.0.108",
|
||||||
"moment": "2.29.1"
|
"moment": "2.29.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/once": {
|
"node_modules/once": {
|
||||||
@@ -5074,6 +5083,11 @@
|
|||||||
"has-symbols": "^1.0.2"
|
"has-symbols": "^1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"idb": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg=="
|
||||||
|
},
|
||||||
"ignore": {
|
"ignore": {
|
||||||
"version": "5.1.9",
|
"version": "5.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz",
|
||||||
@@ -5406,9 +5420,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"moment": {
|
"moment": {
|
||||||
"version": "2.29.1",
|
"version": "2.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
|
||||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
|
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
@@ -5470,15 +5484,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"obsidian": {
|
"obsidian": {
|
||||||
"version": "0.13.30",
|
"version": "0.14.6",
|
||||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.13.30.tgz",
|
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.14.6.tgz",
|
||||||
"integrity": "sha512-uAOrIyeHE9qYzg1Qjfpy/qlyLUFX9oyKWeHYO8NVDoI+pm5VUTMe7XWcsXPwb9iVsVmggVJcdV15Vqm9bljhxQ==",
|
"integrity": "sha512-oXPJ8Zt10WhN19bk5l4mZuXRZbbdT1QoMgxGGJ0bB7UcJa0bozDzugS5L/QiV9gDoujpUPxDWNVahEel6r0Fpw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@codemirror/state": "^0.19.6",
|
"@codemirror/state": "^0.19.6",
|
||||||
"@codemirror/view": "^0.19.31",
|
"@codemirror/view": "^0.19.31",
|
||||||
"@types/codemirror": "0.0.108",
|
"@types/codemirror": "0.0.108",
|
||||||
"moment": "2.29.1"
|
"moment": "2.29.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"once": {
|
"once": {
|
||||||
|
|||||||
24
package.json
24
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.8.2",
|
"version": "0.11.7",
|
||||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"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",
|
"main": "main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -21,20 +21,24 @@
|
|||||||
"@types/pouchdb-browser": "^6.1.3",
|
"@types/pouchdb-browser": "^6.1.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.7.0",
|
"@typescript-eslint/eslint-plugin": "^5.7.0",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
"eslint": "^7.32.0",
|
|
||||||
"eslint-config-airbnb-base": "^14.2.1",
|
|
||||||
"eslint-plugin-import": "^2.25.2",
|
|
||||||
"obsidian": "^0.13.30",
|
|
||||||
"rollup": "^2.32.1",
|
|
||||||
"tslib": "^2.2.0",
|
|
||||||
"typescript": "^4.2.4",
|
|
||||||
"builtin-modules": "^3.2.0",
|
"builtin-modules": "^3.2.0",
|
||||||
"esbuild": "0.13.12",
|
"esbuild": "0.13.12",
|
||||||
"esbuild-svelte": "^0.6.0",
|
"esbuild-svelte": "^0.6.0",
|
||||||
"svelte-preprocess": "^4.10.2"
|
"eslint": "^7.32.0",
|
||||||
|
"eslint-config-airbnb-base": "^14.2.1",
|
||||||
|
"eslint-plugin-import": "^2.25.2",
|
||||||
|
"obsidian": "^0.14.6",
|
||||||
|
"rollup": "^2.32.1",
|
||||||
|
"svelte-preprocess": "^4.10.2",
|
||||||
|
"tslib": "^2.2.0",
|
||||||
|
"typescript": "^4.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
"esbuild": "0.13.12",
|
||||||
|
"esbuild-svelte": "^0.6.0",
|
||||||
|
"idb": "^7.0.1",
|
||||||
|
"svelte-preprocess": "^4.10.2",
|
||||||
"xxhash-wasm": "^0.4.2"
|
"xxhash-wasm": "^0.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
# PouchDB-browser
|
# PouchDB-browser
|
||||||
just webpacked.
|
|
||||||
|
Just webpacked.
|
||||||
|
(Rollup couldn't pack pouchdb-browser into browser bundle)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
9820
pouchdb-browser-webpack/package-lock.json
generated
9820
pouchdb-browser-webpack/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "pouchdb-browser-webpack",
|
"name": "pouchdb-browser-webpack",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "pouchdb-browser webpack",
|
"description": "pouchdb-browser webpack",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"build": "webpack --mode=production --node-env=production",
|
"build": "webpack --mode=production --node-env=production",
|
||||||
"build:dev": "webpack --mode=development",
|
"build:dev": "webpack --mode=development",
|
||||||
"build:prod": "webpack --mode=production --node-env=production",
|
"build:prod": "webpack --mode=production --node-env=production",
|
||||||
"watch": "webpack --watch"
|
"watch": "webpack --watch"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pouchdb-browser": "^7.2.2"
|
"pouchdb-browser": "^7.3.0",
|
||||||
},
|
"transform-pouch": "^2.0.0",
|
||||||
"devDependencies": {
|
"pouchdb-find": "^7.3.0"
|
||||||
"webpack": "^5.58.1",
|
},
|
||||||
"webpack-cli": "^4.9.0"
|
"devDependencies": {
|
||||||
}
|
"webpack": "^5.58.1",
|
||||||
|
"webpack-cli": "^4.9.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
// This module just webpacks pouchdb-browser
|
// This module just webpacks pouchdb-browser
|
||||||
import * as PouchDB_src from "pouchdb-browser";
|
// import * as PouchDB_src from "pouchdb-browser";
|
||||||
const PouchDB = PouchDB_src.default;
|
const pouch = require("pouchdb-browser").default;
|
||||||
|
const find = require("pouchdb-find").default;
|
||||||
|
const transform = require("transform-pouch");
|
||||||
|
const PouchDB = pouch.plugin(find).plugin(transform);
|
||||||
|
|
||||||
export { PouchDB };
|
export { PouchDB };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { App, Modal } from "obsidian";
|
import { App, Modal } from "obsidian";
|
||||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
||||||
import { diff_result } from "./types";
|
import { diff_result } from "./lib/src/types";
|
||||||
import { escapeStringToHTML } from "./utils";
|
import { escapeStringToHTML } from "./lib/src/utils";
|
||||||
|
|
||||||
export class ConflictResolveModal extends Modal {
|
export class ConflictResolveModal extends Modal {
|
||||||
// result: Array<[number, string]>;
|
// result: Array<[number, string]>;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { TFile, Modal, App } from "obsidian";
|
import { TFile, Modal, App } from "obsidian";
|
||||||
import { path2id, escapeStringToHTML } from "./utils";
|
import { path2id } from "./utils";
|
||||||
|
import { escapeStringToHTML } from "./lib/src/utils";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
||||||
import { LOG_LEVEL } from "./types";
|
import { LOG_LEVEL } from "./lib/src/types";
|
||||||
import { Logger } from "./logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
|
|
||||||
export class DocumentHistoryModal extends Modal {
|
export class DocumentHistoryModal extends Modal {
|
||||||
plugin: ObsidianLiveSyncPlugin;
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
|
|||||||
50
src/KeyValueDB.ts
Normal file
50
src/KeyValueDB.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { deleteDB, IDBPDatabase, openDB } from "idb";
|
||||||
|
export interface KeyValueDatabase {
|
||||||
|
get<T>(key: string): Promise<T>;
|
||||||
|
set<T>(key: string, value: T): Promise<IDBValidKey>;
|
||||||
|
del(key: string): Promise<void>;
|
||||||
|
clear(): Promise<void>;
|
||||||
|
keys(query?: IDBValidKey | IDBKeyRange, count?: number): Promise<IDBValidKey[]>;
|
||||||
|
close(): void;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
const databaseCache: { [key: string]: IDBPDatabase<any> } = {};
|
||||||
|
export const OpenKeyValueDatabase = (dbKey: string): KeyValueDatabase => {
|
||||||
|
if (dbKey in databaseCache) {
|
||||||
|
databaseCache[dbKey].close();
|
||||||
|
delete databaseCache[dbKey];
|
||||||
|
}
|
||||||
|
const storeKey = dbKey;
|
||||||
|
const dbPromise = openDB(dbKey, 1, {
|
||||||
|
upgrade(db) {
|
||||||
|
db.createObjectStore(storeKey);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
~(async () => (databaseCache[dbKey] = await dbPromise))();
|
||||||
|
return {
|
||||||
|
async get<T>(key: string): Promise<T> {
|
||||||
|
return (await dbPromise).get(storeKey, key);
|
||||||
|
},
|
||||||
|
async set<T>(key: string, value: T) {
|
||||||
|
return (await dbPromise).put(storeKey, value, key);
|
||||||
|
},
|
||||||
|
async del(key: string) {
|
||||||
|
return (await dbPromise).delete(storeKey, key);
|
||||||
|
},
|
||||||
|
async clear() {
|
||||||
|
return (await dbPromise).clear(storeKey);
|
||||||
|
},
|
||||||
|
async keys(query?: IDBValidKey | IDBKeyRange, count?: number) {
|
||||||
|
return (await dbPromise).getAllKeys(storeKey, query, count);
|
||||||
|
},
|
||||||
|
async close() {
|
||||||
|
delete databaseCache[dbKey];
|
||||||
|
return (await dbPromise).close();
|
||||||
|
},
|
||||||
|
async destroy() {
|
||||||
|
delete databaseCache[dbKey];
|
||||||
|
(await dbPromise).close();
|
||||||
|
await deleteDB(dbKey);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
1171
src/LocalPouchDB.ts
1171
src/LocalPouchDB.ts
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
import { App, Modal } from "obsidian";
|
import { App, Modal } from "obsidian";
|
||||||
import { escapeStringToHTML } from "./utils";
|
import { escapeStringToHTML } from "./lib/src/utils";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
|
|
||||||
export class LogDisplayModal extends Modal {
|
export class LogDisplayModal extends Modal {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { App, Notice, PluginSettingTab, Setting, sanitizeHTMLToDom } from "obsidian";
|
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl } from "obsidian";
|
||||||
import { EntryDoc, LOG_LEVEL } from "./types";
|
import { EntryDoc, LOG_LEVEL, RemoteDBSettings } from "./lib/src/types";
|
||||||
import { path2id, id2path, runWithLock } from "./utils";
|
import { path2id, id2path } from "./utils";
|
||||||
import { Logger } from "./logger";
|
import { NewNotice, runWithLock } from "./lib/src/utils";
|
||||||
import { connectRemoteCouchDB } from "./utils_couchdb";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { testCrypt } from "./e2ee";
|
import { checkSyncInfo, connectRemoteCouchDBWithSetting } from "./utils_couchdb";
|
||||||
|
import { testCrypt } from "./lib/src/e2ee_v2";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
|
|
||||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||||
@@ -14,14 +15,16 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
async testConnection(): Promise<void> {
|
async testConnection(): Promise<void> {
|
||||||
const db = await connectRemoteCouchDB(
|
// const db = await connectRemoteCouchDB(
|
||||||
this.plugin.settings.couchDB_URI + (this.plugin.settings.couchDB_DBNAME == "" ? "" : "/" + this.plugin.settings.couchDB_DBNAME),
|
// this.plugin.settings.couchDB_URI + (this.plugin.settings.couchDB_DBNAME == "" ? "" : "/" + this.plugin.settings.couchDB_DBNAME),
|
||||||
{
|
// {
|
||||||
username: this.plugin.settings.couchDB_USER,
|
// username: this.plugin.settings.couchDB_USER,
|
||||||
password: this.plugin.settings.couchDB_PASSWORD,
|
// password: this.plugin.settings.couchDB_PASSWORD,
|
||||||
},
|
// },
|
||||||
this.plugin.settings.disableRequestURI
|
// this.plugin.settings.disableRequestURI,
|
||||||
);
|
// this.plugin.settings.encrypt ? this.plugin.settings.passphrase : this.plugin.settings.encrypt
|
||||||
|
// );
|
||||||
|
const db = await connectRemoteCouchDBWithSetting(this.plugin.settings, this.plugin.localDatabase.isMobile);
|
||||||
if (typeof db === "string") {
|
if (typeof db === "string") {
|
||||||
this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI} : ${this.plugin.settings.couchDB_DBNAME} \n(${db})`, LOG_LEVEL.NOTICE);
|
this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI} : ${this.plugin.settings.couchDB_DBNAME} \n(${db})`, LOG_LEVEL.NOTICE);
|
||||||
return;
|
return;
|
||||||
@@ -77,7 +80,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
const containerRemoteDatabaseEl = containerEl.createDiv();
|
const containerRemoteDatabaseEl = containerEl.createDiv();
|
||||||
containerRemoteDatabaseEl.createEl("h3", { text: "Remote Database configuration" });
|
containerRemoteDatabaseEl.createEl("h3", { text: "Remote Database configuration" });
|
||||||
const syncWarn = containerRemoteDatabaseEl.createEl("div", { text: `These settings are kept locked while automatic synchronization options are enabled. Disable these options in the "Sync Settings" tab to unlock.` });
|
const syncWarn = containerRemoteDatabaseEl.createEl("div", { text: `These settings are kept locked while automatic synchronization options are enabled. Disable these options in the "Sync Settings" tab to unlock.` });
|
||||||
syncWarn.addClass("op-warn");
|
syncWarn.addClass("op-warn-info");
|
||||||
syncWarn.addClass("sls-hidden");
|
syncWarn.addClass("sls-hidden");
|
||||||
|
|
||||||
const isAnySyncEnabled = (): boolean => {
|
const isAnySyncEnabled = (): boolean => {
|
||||||
@@ -169,14 +172,126 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
this.plugin.settings.couchDB_DBNAME = value;
|
this.plugin.settings.couchDB_DBNAME = value;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
})
|
})
|
||||||
),
|
)
|
||||||
new Setting(containerRemoteDatabaseEl).setName("Use the old connecting method").addToggle((toggle) =>
|
|
||||||
toggle.setValue(this.plugin.settings.disableRequestURI).onChange(async (value) => {
|
// new Setting(containerRemoteDatabaseEl)
|
||||||
this.plugin.settings.disableRequestURI = value;
|
// .setDesc("This feature is locked in mobile")
|
||||||
|
// .setName("Use the old connecting method")
|
||||||
|
// .addToggle((toggle) => {
|
||||||
|
// toggle.setValue(this.plugin.settings.disableRequestURI).onChange(async (value) => {
|
||||||
|
// this.plugin.settings.disableRequestURI = value;
|
||||||
|
// await this.plugin.saveSettings();
|
||||||
|
// });
|
||||||
|
// toggle.setDisabled(this.plugin.isMobile);
|
||||||
|
// return toggle;
|
||||||
|
// })
|
||||||
|
);
|
||||||
|
new Setting(containerRemoteDatabaseEl)
|
||||||
|
.setName("End to End Encryption")
|
||||||
|
.setDesc("Encrypt contents on the remote database. If you use the plugins synchronizing feature, enabling this is recommend.")
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle.setValue(this.plugin.settings.workingEncrypt).onChange(async (value) => {
|
||||||
|
this.plugin.settings.workingEncrypt = value;
|
||||||
|
phasspharase.setDisabled(!value);
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
})
|
})
|
||||||
|
);
|
||||||
|
const phasspharase = new Setting(containerRemoteDatabaseEl)
|
||||||
|
.setName("Passphrase")
|
||||||
|
.setDesc("Encrypting passphrase")
|
||||||
|
.addText((text) => {
|
||||||
|
text.setPlaceholder("")
|
||||||
|
.setValue(this.plugin.settings.workingPassphrase)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.workingPassphrase = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
text.inputEl.setAttribute("type", "password");
|
||||||
|
});
|
||||||
|
phasspharase.setDisabled(!this.plugin.settings.workingEncrypt);
|
||||||
|
containerRemoteDatabaseEl.createEl("div", {
|
||||||
|
text: "If you change the passphrase, rebuilding the remote database is required. Please press 'Apply and send'. Or, If you have configured it to connect to an existing database, click 'Just apply'.",
|
||||||
|
});
|
||||||
|
const checkWorkingPassphrase = async (): Promise<boolean> => {
|
||||||
|
const settingForCheck: RemoteDBSettings = {
|
||||||
|
...this.plugin.settings,
|
||||||
|
encrypt: this.plugin.settings.workingEncrypt,
|
||||||
|
passphrase: this.plugin.settings.workingPassphrase,
|
||||||
|
};
|
||||||
|
console.dir(settingForCheck);
|
||||||
|
const db = await connectRemoteCouchDBWithSetting(settingForCheck, this.plugin.localDatabase.isMobile);
|
||||||
|
if (typeof db === "string") {
|
||||||
|
Logger("Could not connect to the database.", LOG_LEVEL.NOTICE);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
if (await checkSyncInfo(db.db)) {
|
||||||
|
// Logger("Database connected", LOG_LEVEL.NOTICE);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Logger("Failed to read remote database", LOG_LEVEL.NOTICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const applyEncryption = async (sendToServer: boolean) => {
|
||||||
|
if (this.plugin.settings.workingEncrypt && this.plugin.settings.workingPassphrase == "") {
|
||||||
|
Logger("If you enable encryption, you have to set the passphrase", LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.plugin.settings.workingEncrypt && !(await testCrypt())) {
|
||||||
|
Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(await checkWorkingPassphrase())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.plugin.settings.workingEncrypt) {
|
||||||
|
this.plugin.settings.workingPassphrase = "";
|
||||||
|
}
|
||||||
|
this.plugin.settings.liveSync = false;
|
||||||
|
this.plugin.settings.periodicReplication = false;
|
||||||
|
this.plugin.settings.syncOnSave = false;
|
||||||
|
this.plugin.settings.syncOnStart = false;
|
||||||
|
this.plugin.settings.syncOnFileOpen = false;
|
||||||
|
this.plugin.settings.encrypt = this.plugin.settings.workingEncrypt;
|
||||||
|
this.plugin.settings.passphrase = this.plugin.settings.workingPassphrase;
|
||||||
|
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
// await this.plugin.resetLocalDatabase();
|
||||||
|
if (sendToServer) {
|
||||||
|
await this.plugin.initializeDatabase(true);
|
||||||
|
await this.plugin.markRemoteLocked();
|
||||||
|
await this.plugin.tryResetRemoteDatabase();
|
||||||
|
await this.plugin.markRemoteLocked();
|
||||||
|
await this.plugin.replicateAllToServer(true);
|
||||||
|
} else {
|
||||||
|
await this.plugin.markRemoteResolved();
|
||||||
|
await this.plugin.replicate(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
new Setting(containerRemoteDatabaseEl)
|
||||||
|
.setName("Apply")
|
||||||
|
.setDesc("apply encryption settinngs, and re-initialize remote database")
|
||||||
|
.addButton((button) =>
|
||||||
|
button
|
||||||
|
.setButtonText("Apply and send")
|
||||||
|
.setWarning()
|
||||||
|
.setDisabled(false)
|
||||||
|
.setClass("sls-btn-left")
|
||||||
|
.onClick(async () => {
|
||||||
|
await applyEncryption(true);
|
||||||
|
})
|
||||||
)
|
)
|
||||||
);
|
.addButton((button) =>
|
||||||
|
button
|
||||||
|
.setButtonText("Just apply")
|
||||||
|
.setWarning()
|
||||||
|
.setDisabled(false)
|
||||||
|
.setClass("sls-btn-right")
|
||||||
|
.onClick(async () => {
|
||||||
|
await applyEncryption(false);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
new Setting(containerRemoteDatabaseEl)
|
new Setting(containerRemoteDatabaseEl)
|
||||||
.setName("Test Database Connection")
|
.setName("Test Database Connection")
|
||||||
@@ -190,6 +305,174 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
new Setting(containerRemoteDatabaseEl)
|
||||||
|
.setName("Check database configuration")
|
||||||
|
// .setDesc("Open database connection. If the remote database is not found and you have the privilege to create a database, the database will be created.")
|
||||||
|
.addButton((button) =>
|
||||||
|
button
|
||||||
|
.setButtonText("Check")
|
||||||
|
.setDisabled(false)
|
||||||
|
.onClick(async () => {
|
||||||
|
const checkConfig = async () => {
|
||||||
|
try {
|
||||||
|
const requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string, key?: string, body?: string) => {
|
||||||
|
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${username}:${password}`));
|
||||||
|
const encoded = window.btoa(utf8str);
|
||||||
|
const authHeader = "Basic " + encoded;
|
||||||
|
// const origin = "capacitor://localhost";
|
||||||
|
const transformedHeaders: Record<string, string> = { authorization: authHeader, origin: origin };
|
||||||
|
const uri = `${baseUri}/_node/_local/_config${key ? "/" + key : ""}`;
|
||||||
|
|
||||||
|
const requestParam: RequestUrlParam = {
|
||||||
|
url: uri,
|
||||||
|
method: body ? "PUT" : "GET",
|
||||||
|
headers: transformedHeaders,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
};
|
||||||
|
return await requestUrl(requestParam);
|
||||||
|
};
|
||||||
|
|
||||||
|
const r = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, window.origin);
|
||||||
|
|
||||||
|
Logger(JSON.stringify(r.json, null, 2));
|
||||||
|
|
||||||
|
const responseConfig = r.json;
|
||||||
|
|
||||||
|
const emptyDiv = createDiv();
|
||||||
|
emptyDiv.innerHTML = "<span></span>";
|
||||||
|
checkResultDiv.replaceChildren(...[emptyDiv]);
|
||||||
|
const addResult = (msg: string, classes?: string[]) => {
|
||||||
|
const tmpDiv = createDiv();
|
||||||
|
tmpDiv.addClass("ob-btn-config-fix");
|
||||||
|
if (classes) {
|
||||||
|
tmpDiv.addClasses(classes);
|
||||||
|
}
|
||||||
|
tmpDiv.innerHTML = `${msg}`;
|
||||||
|
checkResultDiv.appendChild(tmpDiv);
|
||||||
|
};
|
||||||
|
const addConfigFixButton = (title: string, key: string, value: string) => {
|
||||||
|
const tmpDiv = createDiv();
|
||||||
|
tmpDiv.addClass("ob-btn-config-fix");
|
||||||
|
tmpDiv.innerHTML = `<label>${title}</label><button>Fix</button>`;
|
||||||
|
const x = checkResultDiv.appendChild(tmpDiv);
|
||||||
|
x.querySelector("button").addEventListener("click", async () => {
|
||||||
|
console.dir({ key, value });
|
||||||
|
const res = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, undefined, key, value);
|
||||||
|
console.dir(res);
|
||||||
|
if (res.status == 200) {
|
||||||
|
Logger(`${title} successfly updated`, LOG_LEVEL.NOTICE);
|
||||||
|
checkResultDiv.removeChild(x);
|
||||||
|
checkConfig();
|
||||||
|
} else {
|
||||||
|
Logger(`${title} failed`, LOG_LEVEL.NOTICE);
|
||||||
|
Logger(res.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
addResult("---Notice---", ["ob-btn-config-head"]);
|
||||||
|
addResult(
|
||||||
|
"If the server configuration is not persistent (e.g., running on docker), the values set from here will also be volatile. Once you are able to connect, please reflect the settings in the server's local.ini.",
|
||||||
|
["ob-btn-config-info"]
|
||||||
|
);
|
||||||
|
|
||||||
|
addResult("Your configuration is dumped to Log", ["ob-btn-config-info"]);
|
||||||
|
addResult("--Config check--", ["ob-btn-config-head"]);
|
||||||
|
|
||||||
|
// Admin check
|
||||||
|
// for database creation and deletion
|
||||||
|
if (!(this.plugin.settings.couchDB_USER in responseConfig.admins)) {
|
||||||
|
addResult(`⚠ You do not have administrative privileges.`);
|
||||||
|
} else {
|
||||||
|
addResult("✔ You have administrative privileges.");
|
||||||
|
}
|
||||||
|
// HTTP user-authorization check
|
||||||
|
if (responseConfig?.chttpd?.require_valid_user != "true") {
|
||||||
|
addResult("❗ chttpd.require_valid_user looks like wrong.");
|
||||||
|
addConfigFixButton("Set chttpd.require_valid_user = true", "chttpd/require_valid_user", "true");
|
||||||
|
} else {
|
||||||
|
addResult("✔ chttpd.require_valid_user is ok.");
|
||||||
|
}
|
||||||
|
if (responseConfig?.chttpd_auth?.require_valid_user != "true") {
|
||||||
|
addResult("❗ chttpd_auth.require_valid_user looks like wrong.");
|
||||||
|
addConfigFixButton("Set chttpd_auth.require_valid_user = true", "chttpd_auth/require_valid_user", "true");
|
||||||
|
} else {
|
||||||
|
addResult("✔ chttpd_auth.require_valid_user is ok.");
|
||||||
|
}
|
||||||
|
// HTTPD check
|
||||||
|
// Check Authentication header
|
||||||
|
if (!responseConfig?.httpd["WWW-Authenticate"]) {
|
||||||
|
addResult("❗ httpd.WWW-Authenticate is missing");
|
||||||
|
addConfigFixButton("Set httpd.WWW-Authenticate", "httpd/WWW-Authenticate", 'Basic realm="couchdb"');
|
||||||
|
} else {
|
||||||
|
addResult("✔ httpd.WWW-Authenticate is ok.");
|
||||||
|
}
|
||||||
|
if (responseConfig?.httpd?.enable_cors != "true") {
|
||||||
|
addResult("❗ httpd.enable_cors is wrong");
|
||||||
|
addConfigFixButton("Set httpd.enable_cors", "httpd/enable_cors", "true");
|
||||||
|
} else {
|
||||||
|
addResult("✔ httpd.enable_cors is ok.");
|
||||||
|
}
|
||||||
|
// CORS check
|
||||||
|
// checking connectivity for mobile
|
||||||
|
if (responseConfig?.cors?.credentials != "true") {
|
||||||
|
addResult("❗ cors.credentials is wrong");
|
||||||
|
addConfigFixButton("Set cors.credentials", "cors/credentials", "true");
|
||||||
|
} else {
|
||||||
|
addResult("✔ cors.credentials is ok.");
|
||||||
|
}
|
||||||
|
const ConfiguredOrigins = ((responseConfig?.cors?.origins ?? "") + "").split(",");
|
||||||
|
if (
|
||||||
|
responseConfig?.cors?.origins == "*" ||
|
||||||
|
(ConfiguredOrigins.indexOf("app://obsidian.md") !== -1 && ConfiguredOrigins.indexOf("capacitor://localhost") !== -1 && ConfiguredOrigins.indexOf("http://localhost") !== -1)
|
||||||
|
) {
|
||||||
|
addResult("✔ cors.origins is ok.");
|
||||||
|
} else {
|
||||||
|
addResult("❗ cors.origins is wrong");
|
||||||
|
addConfigFixButton("Set cors.origins", "cors/origins", "app://obsidian.md,capacitor://localhost,http://localhost");
|
||||||
|
}
|
||||||
|
addResult("--Connection check--", ["ob-btn-config-head"]);
|
||||||
|
addResult(`Current origin:${window.location.origin}`);
|
||||||
|
|
||||||
|
// Request header check
|
||||||
|
const origins = ["app://obsidian.md", "capacitor://localhost", "http://localhost"];
|
||||||
|
for (const org of origins) {
|
||||||
|
const rr = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, org);
|
||||||
|
const responseHeaders = Object.entries(rr.headers)
|
||||||
|
.map((e) => {
|
||||||
|
e[0] = (e[0] + "").toLowerCase();
|
||||||
|
return e;
|
||||||
|
})
|
||||||
|
.reduce((obj, [key, val]) => {
|
||||||
|
obj[key] = val;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
addResult(`Origin check:${org}`);
|
||||||
|
if (responseHeaders["access-control-allow-credentials"] != "true") {
|
||||||
|
addResult("❗ CORS is not allowing credential");
|
||||||
|
} else {
|
||||||
|
addResult("✔ CORS credential OK");
|
||||||
|
}
|
||||||
|
if (responseHeaders["access-control-allow-origin"] != org) {
|
||||||
|
addResult(`❗ CORS Origin is unmatched:${origin}->${responseHeaders["access-control-allow-origin"]}`);
|
||||||
|
} else {
|
||||||
|
addResult("✔ CORS origin OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addResult("--Done--", ["ob-btn-config-haed"]);
|
||||||
|
addResult("If you have some trouble with Connection-check even though all Config-check has been passed, Please check your reverse proxy's configuration.", ["ob-btn-config-info"]);
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(`Checking configration failed`);
|
||||||
|
Logger(ex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await checkConfig();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const checkResultDiv = containerRemoteDatabaseEl.createEl("div", {
|
||||||
|
text: "",
|
||||||
|
});
|
||||||
|
|
||||||
addScreenElement("0", containerRemoteDatabaseEl);
|
addScreenElement("0", containerRemoteDatabaseEl);
|
||||||
const containerLocalDatabaseEl = containerEl.createDiv();
|
const containerLocalDatabaseEl = containerEl.createDiv();
|
||||||
containerLocalDatabaseEl.createEl("h3", { text: "Local Database configuration" });
|
containerLocalDatabaseEl.createEl("h3", { text: "Local Database configuration" });
|
||||||
@@ -233,88 +516,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
await this.plugin.garbageCollect();
|
await this.plugin.garbageCollect();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
new Setting(containerLocalDatabaseEl)
|
|
||||||
.setName("End to End Encryption")
|
|
||||||
.setDesc("Encrypting contents on the database.")
|
|
||||||
.addToggle((toggle) =>
|
|
||||||
toggle.setValue(this.plugin.settings.workingEncrypt).onChange(async (value) => {
|
|
||||||
this.plugin.settings.workingEncrypt = value;
|
|
||||||
phasspharase.setDisabled(!value);
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const phasspharase = new Setting(containerLocalDatabaseEl)
|
|
||||||
.setName("Passphrase")
|
|
||||||
.setDesc("Encrypting passphrase")
|
|
||||||
.addText((text) => {
|
|
||||||
text.setPlaceholder("")
|
|
||||||
.setValue(this.plugin.settings.workingPassphrase)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.workingPassphrase = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
});
|
|
||||||
text.inputEl.setAttribute("type", "password");
|
|
||||||
});
|
|
||||||
phasspharase.setDisabled(!this.plugin.settings.workingEncrypt);
|
|
||||||
containerLocalDatabaseEl.createEl("div", {
|
|
||||||
text: "When you change any encryption enabled or passphrase, you have to reset all databases to make sure that the last password is unused and erase encrypted data from anywhere. This operation will not lost your vault if you are fully synced.",
|
|
||||||
});
|
|
||||||
const applyEncryption = async (sendToServer: boolean) => {
|
|
||||||
if (this.plugin.settings.workingEncrypt && this.plugin.settings.workingPassphrase == "") {
|
|
||||||
Logger("If you enable encryption, you have to set the passphrase", LOG_LEVEL.NOTICE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.plugin.settings.workingEncrypt && !(await testCrypt())) {
|
|
||||||
Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.NOTICE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.plugin.settings.workingEncrypt) {
|
|
||||||
this.plugin.settings.workingPassphrase = "";
|
|
||||||
}
|
|
||||||
this.plugin.settings.liveSync = false;
|
|
||||||
this.plugin.settings.periodicReplication = false;
|
|
||||||
this.plugin.settings.syncOnSave = false;
|
|
||||||
this.plugin.settings.syncOnStart = false;
|
|
||||||
this.plugin.settings.syncOnFileOpen = false;
|
|
||||||
this.plugin.settings.encrypt = this.plugin.settings.workingEncrypt;
|
|
||||||
this.plugin.settings.passphrase = this.plugin.settings.workingPassphrase;
|
|
||||||
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.resetLocalDatabase();
|
|
||||||
if (sendToServer) {
|
|
||||||
await this.plugin.initializeDatabase(true);
|
|
||||||
await this.plugin.markRemoteLocked();
|
|
||||||
await this.plugin.tryResetRemoteDatabase();
|
|
||||||
await this.plugin.markRemoteLocked();
|
|
||||||
await this.plugin.replicateAllToServer(true);
|
|
||||||
} else {
|
|
||||||
await this.plugin.markRemoteResolved();
|
|
||||||
await this.plugin.replicate(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
new Setting(containerLocalDatabaseEl)
|
|
||||||
.setName("Apply")
|
|
||||||
.setDesc("apply encryption settinngs, and re-initialize database")
|
|
||||||
.addButton((button) =>
|
|
||||||
button
|
|
||||||
.setButtonText("Apply and send")
|
|
||||||
.setWarning()
|
|
||||||
.setDisabled(false)
|
|
||||||
.setClass("sls-btn-left")
|
|
||||||
.onClick(async () => {
|
|
||||||
await applyEncryption(true);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.addButton((button) =>
|
|
||||||
button
|
|
||||||
.setButtonText("Apply and receive")
|
|
||||||
.setWarning()
|
|
||||||
.setDisabled(false)
|
|
||||||
.setClass("sls-btn-right")
|
|
||||||
.onClick(async () => {
|
|
||||||
await applyEncryption(false);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
containerLocalDatabaseEl.createEl("div", {
|
containerLocalDatabaseEl.createEl("div", {
|
||||||
text: sanitizeHTMLToDom(`Advanced settings<br>
|
text: sanitizeHTMLToDom(`Advanced settings<br>
|
||||||
@@ -509,6 +710,24 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// new Setting(containerSyncSettingEl)
|
||||||
|
// .setName("Skip old files on sync")
|
||||||
|
// .setDesc("Skip old incoming if incoming changes older than storage.")
|
||||||
|
// .addToggle((toggle) =>
|
||||||
|
// toggle.setValue(this.plugin.settings.skipOlderFilesOnSync).onChange(async (value) => {
|
||||||
|
// this.plugin.settings.skipOlderFilesOnSync = value;
|
||||||
|
// await this.plugin.saveSettings();
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
new Setting(containerSyncSettingEl)
|
||||||
|
.setName("Check conflict only on opening file.")
|
||||||
|
.setDesc("Do not check conflict while replication")
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle.setValue(this.plugin.settings.checkConflictOnlyOnOpen).onChange(async (value) => {
|
||||||
|
this.plugin.settings.checkConflictOnlyOnOpen = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
containerSyncSettingEl.createEl("div", {
|
containerSyncSettingEl.createEl("div", {
|
||||||
text: sanitizeHTMLToDom(`Advanced settings<br>
|
text: sanitizeHTMLToDom(`Advanced settings<br>
|
||||||
If you reached the payload size limit when using IBM Cloudant, please set batch size and batch limit to a lower value.`),
|
If you reached the payload size limit when using IBM Cloudant, please set batch size and batch limit to a lower value.`),
|
||||||
@@ -655,7 +874,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const hatchWarn = containerHatchEl.createEl("div", { text: `To stop the bootup sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` });
|
const hatchWarn = containerHatchEl.createEl("div", { text: `To stop the bootup sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` });
|
||||||
hatchWarn.addClass("op-warn");
|
hatchWarn.addClass("op-warn-info");
|
||||||
const dropHistory = async (sendToServer: boolean) => {
|
const dropHistory = async (sendToServer: boolean) => {
|
||||||
this.plugin.settings.liveSync = false;
|
this.plugin.settings.liveSync = false;
|
||||||
this.plugin.settings.periodicReplication = false;
|
this.plugin.settings.periodicReplication = false;
|
||||||
@@ -688,7 +907,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
const files = this.app.vault.getFiles();
|
const files = this.app.vault.getFiles();
|
||||||
Logger("Verify and repair all files started", LOG_LEVEL.NOTICE);
|
Logger("Verify and repair all files started", LOG_LEVEL.NOTICE);
|
||||||
const notice = new Notice("", 0);
|
const notice = NewNotice("", 0);
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
i++;
|
i++;
|
||||||
@@ -714,7 +933,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
.setDisabled(false)
|
.setDisabled(false)
|
||||||
.setWarning()
|
.setWarning()
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
const notice = new Notice("", 0);
|
const notice = NewNotice("", 0);
|
||||||
Logger(`Begin sanity check`, LOG_LEVEL.INFO);
|
Logger(`Begin sanity check`, LOG_LEVEL.INFO);
|
||||||
notice.setMessage(`Begin sanity check`);
|
notice.setMessage(`Begin sanity check`);
|
||||||
await runWithLock("sancheck", true, async () => {
|
await runWithLock("sancheck", true, async () => {
|
||||||
@@ -826,6 +1045,20 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
new Setting(containerHatchEl)
|
||||||
|
.setName("Drop old encrypted database")
|
||||||
|
.setDesc("WARNING: Please use this button only when you have failed on converting old-style localdatabase at v0.10.0.")
|
||||||
|
.addButton((button) =>
|
||||||
|
button
|
||||||
|
.setButtonText("Drop")
|
||||||
|
.setWarning()
|
||||||
|
.setDisabled(false)
|
||||||
|
.onClick(async () => {
|
||||||
|
await this.plugin.resetLocalOldDatabase();
|
||||||
|
await this.plugin.initializeDatabase();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
addScreenElement("50", containerHatchEl);
|
addScreenElement("50", containerHatchEl);
|
||||||
// With great respect, thank you TfTHacker!
|
// With great respect, thank you TfTHacker!
|
||||||
// refered: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts
|
// refered: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts
|
||||||
@@ -834,7 +1067,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
const updateDisabledOfDeviceAndVaultName = () => {
|
const updateDisabledOfDeviceAndVaultName = () => {
|
||||||
vaultName.setDisabled(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic);
|
vaultName.setDisabled(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic);
|
||||||
vaultName.setTooltip(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic ? "You could not change when you enabling auto sweep." : "");
|
vaultName.setTooltip(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic ? "You could not change when you enabling auto scan." : "");
|
||||||
};
|
};
|
||||||
new Setting(containerPluginSettings).setName("Enable plugin synchronization").addToggle((toggle) =>
|
new Setting(containerPluginSettings).setName("Enable plugin synchronization").addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.usePluginSync).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.usePluginSync).onChange(async (value) => {
|
||||||
@@ -844,8 +1077,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerPluginSettings)
|
new Setting(containerPluginSettings)
|
||||||
.setName("Sweep plugins automatically")
|
.setName("Scan plugins automatically")
|
||||||
.setDesc("Sweep plugins before replicating.")
|
.setDesc("Scan plugins before replicating.")
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.autoSweepPlugins).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.autoSweepPlugins).onChange(async (value) => {
|
||||||
this.plugin.settings.autoSweepPlugins = value;
|
this.plugin.settings.autoSweepPlugins = value;
|
||||||
@@ -855,8 +1088,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerPluginSettings)
|
new Setting(containerPluginSettings)
|
||||||
.setName("Sweep plugins periodically")
|
.setName("Scan plugins periodically")
|
||||||
.setDesc("Sweep plugins each 1 minutes.")
|
.setDesc("Scan plugins each 1 minutes.")
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => {
|
||||||
this.plugin.settings.autoSweepPluginsPeriodic = value;
|
this.plugin.settings.autoSweepPluginsPeriodic = value;
|
||||||
@@ -904,8 +1137,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
const containerCorruptedDataEl = containerEl.createDiv();
|
const containerCorruptedDataEl = containerEl.createDiv();
|
||||||
|
|
||||||
containerCorruptedDataEl.createEl("h3", { text: "Corrupted data" });
|
containerCorruptedDataEl.createEl("h3", { text: "Corrupted or missing data" });
|
||||||
|
containerCorruptedDataEl.createEl("h4", { text: "Corrupted" });
|
||||||
if (Object.keys(this.plugin.localDatabase.corruptedEntries).length > 0) {
|
if (Object.keys(this.plugin.localDatabase.corruptedEntries).length > 0) {
|
||||||
const cx = containerCorruptedDataEl.createEl("div", { text: "If you have copy of these items on any device, simply edit once or twice. Or not, delete this. sorry.." });
|
const cx = containerCorruptedDataEl.createEl("div", { text: "If you have copy of these items on any device, simply edit once or twice. Or not, delete this. sorry.." });
|
||||||
for (const k in this.plugin.localDatabase.corruptedEntries) {
|
for (const k in this.plugin.localDatabase.corruptedEntries) {
|
||||||
@@ -934,6 +1167,38 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
} else {
|
} else {
|
||||||
containerCorruptedDataEl.createEl("div", { text: "There is no corrupted data." });
|
containerCorruptedDataEl.createEl("div", { text: "There is no corrupted data." });
|
||||||
}
|
}
|
||||||
|
containerCorruptedDataEl.createEl("h4", { text: "Missing or waiting" });
|
||||||
|
if (Object.keys(this.plugin.queuedFiles).length > 0) {
|
||||||
|
const cx = containerCorruptedDataEl.createEl("div", {
|
||||||
|
text: "These files have missing or waiting chunks. Perhaps almost chunks will be found in a while after replication. But if there're no chunk, you have to restore database entry from existed file by hitting the button below.",
|
||||||
|
});
|
||||||
|
const files = [...new Set([...this.plugin.queuedFiles.map((e) => e.entry._id)])];
|
||||||
|
for (const k of files) {
|
||||||
|
const xx = cx.createEl("div", { text: `${id2path(k)}` });
|
||||||
|
|
||||||
|
const ba = xx.createEl("button", { text: `Delete this` }, (e) => {
|
||||||
|
e.addEventListener("click", async () => {
|
||||||
|
await this.plugin.localDatabase.deleteDBEntry(k);
|
||||||
|
xx.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ba.addClass("mod-warning");
|
||||||
|
xx.createEl("button", { text: `Restore from file` }, (e) => {
|
||||||
|
e.addEventListener("click", async () => {
|
||||||
|
const f = await this.app.vault.getFiles().filter((e) => path2id(e.path) == k);
|
||||||
|
if (f.length == 0) {
|
||||||
|
Logger("Not found in vault", LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.plugin.updateIntoDB(f[0]);
|
||||||
|
xx.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
xx.addClass("mod-warning");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
containerCorruptedDataEl.createEl("div", { text: "There is no missing or waiting chunk." });
|
||||||
|
}
|
||||||
applyDisplayEnabled();
|
applyDisplayEnabled();
|
||||||
addScreenElement("70", containerCorruptedDataEl);
|
addScreenElement("70", containerCorruptedDataEl);
|
||||||
changeDisplay("0");
|
changeDisplay("0");
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { DevicePluginList, PluginDataEntry } from "./types";
|
import { DevicePluginList, PluginDataEntry } from "./types";
|
||||||
import { versionNumberString2Number } from "./utils";
|
import { versionNumberString2Number } from "./lib/src/utils";
|
||||||
|
|
||||||
type JudgeResult = "" | "NEWER" | "EVEN" | "EVEN_BUT_DIFFERENT" | "OLDER" | "REMOTE_ONLY";
|
type JudgeResult = "" | "NEWER" | "EVEN" | "EVEN_BUT_DIFFERENT" | "OLDER" | "REMOTE_ONLY";
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
export let plugin: ObsidianLiveSyncPlugin;
|
export let plugin: ObsidianLiveSyncPlugin;
|
||||||
let plugins: PluginDataEntry[] = [];
|
let plugins: PluginDataEntry[] = [];
|
||||||
let deviceAndPlugins: { [key: string]: PluginDataEntryDisp[] } = {};
|
let deviceAndPlugins: { [key: string]: PluginDataEntryDisp[] } = {};
|
||||||
let devicePluginList: [string, PluginDataEntryDisp[]][] = [];
|
let devicePluginList: [string, PluginDataEntryDisp[]][] = null;
|
||||||
let ownPlugins: DevicePluginList = null;
|
let ownPlugins: DevicePluginList = null;
|
||||||
let showOwnPlugins = false;
|
let showOwnPlugins = false;
|
||||||
let targetList: { [key: string]: boolean } = {};
|
let targetList: { [key: string]: boolean } = {};
|
||||||
@@ -205,58 +205,63 @@
|
|||||||
<th class="sls-plugins-tbl-device-head">Info</th>
|
<th class="sls-plugins-tbl-device-head">Info</th>
|
||||||
<th class="sls-plugins-tbl-device-head">Target</th>
|
<th class="sls-plugins-tbl-device-head">Target</th>
|
||||||
</tr>
|
</tr>
|
||||||
{#if devicePluginList.length == 0}
|
{#if !devicePluginList}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" class="sls-table-tail tcenter"> Retrieving... </td>
|
<td colspan="3" class="sls-table-tail tcenter"> Retrieving... </td>
|
||||||
</tr>
|
</tr>
|
||||||
{/if}
|
{:else if devicePluginList.length == 0}
|
||||||
{#each devicePluginList as [deviceName, devicePlugins]}
|
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" class="sls-plugins-tbl-device-head">{deviceName}</th>
|
<td colspan="3" class="sls-table-tail tcenter"> No plugins found. </td>
|
||||||
<th class="sls-plugins-tbl-device-head">
|
|
||||||
<button class="mod-cta" on:click={() => toggleAll(deviceName)}>✔</button>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
{#each devicePlugins as plugin}
|
{:else}
|
||||||
|
{#each devicePluginList as [deviceName, devicePlugins]}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="sls-table-head">{plugin.manifest.name}</td>
|
<th colspan="2" class="sls-plugins-tbl-device-head">{deviceName}</th>
|
||||||
<td class="sls-table-tail tcenter">{plugin.versionInfo}{getDispString(plugin.versionFlag)}</td>
|
<th class="sls-plugins-tbl-device-head">
|
||||||
<td class="sls-table-tail tcenter">
|
<button class="mod-cta" on:click={() => toggleAll(deviceName)}>✔</button>
|
||||||
{#if plugin.versionFlag === "EVEN" || plugin.versionFlag === ""}
|
</th>
|
||||||
-
|
|
||||||
{:else}
|
|
||||||
<div class="wrapToggle">
|
|
||||||
<div
|
|
||||||
class="checkbox-container"
|
|
||||||
class:is-enabled={targetList[plugin.deviceVaultName + "---" + plugin.manifest.id + "---plugin"]}
|
|
||||||
on:click={() => toggleTarget(plugin.deviceVaultName + "---" + plugin.manifest.id + "---plugin")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="sls-table-head">Settings</td>
|
|
||||||
<td class="sls-table-tail tcenter">{plugin.mtimeInfo}{getDispString(plugin.mtimeFlag)}</td>
|
|
||||||
<td class="sls-table-tail tcenter">
|
|
||||||
{#if plugin.mtimeFlag === "EVEN" || plugin.mtimeFlag === ""}
|
|
||||||
-
|
|
||||||
{:else}
|
|
||||||
<div class="wrapToggle">
|
|
||||||
<div
|
|
||||||
class="checkbox-container"
|
|
||||||
class:is-enabled={targetList[plugin.deviceVaultName + "---" + plugin.manifest.id + "---setting"]}
|
|
||||||
on:click={() => toggleTarget(plugin.deviceVaultName + "---" + plugin.manifest.id + "---setting")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="divider">
|
|
||||||
<th colspan="3" />
|
|
||||||
</tr>
|
</tr>
|
||||||
|
{#each devicePlugins as plugin}
|
||||||
|
<tr>
|
||||||
|
<td class="sls-table-head">{plugin.manifest.name}</td>
|
||||||
|
<td class="sls-table-tail tcenter">{plugin.versionInfo}{getDispString(plugin.versionFlag)}</td>
|
||||||
|
<td class="sls-table-tail tcenter">
|
||||||
|
{#if plugin.versionFlag === "EVEN" || plugin.versionFlag === ""}
|
||||||
|
-
|
||||||
|
{:else}
|
||||||
|
<div class="wrapToggle">
|
||||||
|
<div
|
||||||
|
class="checkbox-container"
|
||||||
|
class:is-enabled={targetList[plugin.deviceVaultName + "---" + plugin.manifest.id + "---plugin"]}
|
||||||
|
on:click={() => toggleTarget(plugin.deviceVaultName + "---" + plugin.manifest.id + "---plugin")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="sls-table-head">Settings</td>
|
||||||
|
<td class="sls-table-tail tcenter">{plugin.mtimeInfo}{getDispString(plugin.mtimeFlag)}</td>
|
||||||
|
<td class="sls-table-tail tcenter">
|
||||||
|
{#if plugin.mtimeFlag === "EVEN" || plugin.mtimeFlag === ""}
|
||||||
|
-
|
||||||
|
{:else}
|
||||||
|
<div class="wrapToggle">
|
||||||
|
<div
|
||||||
|
class="checkbox-container"
|
||||||
|
class:is-enabled={targetList[plugin.deviceVaultName + "---" + plugin.manifest.id + "---setting"]}
|
||||||
|
on:click={() => toggleTarget(plugin.deviceVaultName + "---" + plugin.manifest.id + "---setting")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="divider">
|
||||||
|
<th colspan="3" />
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
{/each}
|
{/each}
|
||||||
{/each}
|
{/if}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="ols-plugins-div-buttons">
|
<div class="ols-plugins-div-buttons">
|
||||||
@@ -266,7 +271,7 @@
|
|||||||
|
|
||||||
<div class="ols-plugins-div-buttons">
|
<div class="ols-plugins-div-buttons">
|
||||||
<button class="mod-cta" on:click={checkUpdates}>Check Updates</button>
|
<button class="mod-cta" on:click={checkUpdates}>Check Updates</button>
|
||||||
<button class="mod-cta" on:click={sweepPlugins}>Sweep installed</button>
|
<button class="mod-cta" on:click={sweepPlugins}>Scan installed</button>
|
||||||
<button class="mod-cta" on:click={applyPlugins}>Apply all</button>
|
<button class="mod-cta" on:click={applyPlugins}>Apply all</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="ols-plugins-div-buttons">-->
|
<!-- <div class="ols-plugins-div-buttons">-->
|
||||||
|
|||||||
168
src/e2ee.ts
168
src/e2ee.ts
@@ -1,168 +0,0 @@
|
|||||||
import { Logger } from "./logger";
|
|
||||||
import { LOG_LEVEL } from "./types";
|
|
||||||
|
|
||||||
export type encodedData = [encryptedData: string, iv: string, salt: string];
|
|
||||||
export type KeyBuffer = {
|
|
||||||
index: string;
|
|
||||||
key: CryptoKey;
|
|
||||||
salt: Uint8Array;
|
|
||||||
};
|
|
||||||
|
|
||||||
const KeyBuffs: KeyBuffer[] = [];
|
|
||||||
const decKeyBuffs: KeyBuffer[] = [];
|
|
||||||
|
|
||||||
const KEY_RECYCLE_COUNT = 100;
|
|
||||||
let recycleCount = KEY_RECYCLE_COUNT;
|
|
||||||
|
|
||||||
let semiStaticFieldBuffer: Uint8Array = null;
|
|
||||||
const nonceBuffer: Uint32Array = new Uint32Array(1);
|
|
||||||
|
|
||||||
export async function getKeyForEncrypt(passphrase: string): Promise<[CryptoKey, Uint8Array]> {
|
|
||||||
// For performance, the plugin reuses the key KEY_RECYCLE_COUNT times.
|
|
||||||
const f = KeyBuffs.find((e) => e.index == passphrase);
|
|
||||||
if (f) {
|
|
||||||
recycleCount--;
|
|
||||||
if (recycleCount > 0) {
|
|
||||||
return [f.key, f.salt];
|
|
||||||
}
|
|
||||||
KeyBuffs.remove(f);
|
|
||||||
recycleCount = KEY_RECYCLE_COUNT;
|
|
||||||
}
|
|
||||||
const xpassphrase = new TextEncoder().encode(passphrase);
|
|
||||||
const digest = await crypto.subtle.digest({ name: "SHA-256" }, xpassphrase);
|
|
||||||
const keyMaterial = await crypto.subtle.importKey("raw", digest, { name: "PBKDF2" }, false, ["deriveKey"]);
|
|
||||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
||||||
const key = await crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: "PBKDF2",
|
|
||||||
salt,
|
|
||||||
iterations: 100000,
|
|
||||||
hash: "SHA-256",
|
|
||||||
},
|
|
||||||
keyMaterial,
|
|
||||||
{ name: "AES-GCM", length: 256 },
|
|
||||||
false,
|
|
||||||
["encrypt"]
|
|
||||||
);
|
|
||||||
KeyBuffs.push({
|
|
||||||
index: passphrase,
|
|
||||||
key,
|
|
||||||
salt,
|
|
||||||
});
|
|
||||||
while (KeyBuffs.length > 50) {
|
|
||||||
KeyBuffs.shift();
|
|
||||||
}
|
|
||||||
return [key, salt];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getKeyForDecryption(passphrase: string, salt: Uint8Array): Promise<[CryptoKey, Uint8Array]> {
|
|
||||||
const bufKey = passphrase + uint8ArrayToHexString(salt);
|
|
||||||
const f = decKeyBuffs.find((e) => e.index == bufKey);
|
|
||||||
if (f) {
|
|
||||||
return [f.key, f.salt];
|
|
||||||
}
|
|
||||||
const xpassphrase = new TextEncoder().encode(passphrase);
|
|
||||||
const digest = await crypto.subtle.digest({ name: "SHA-256" }, xpassphrase);
|
|
||||||
const keyMaterial = await crypto.subtle.importKey("raw", digest, { name: "PBKDF2" }, false, ["deriveKey"]);
|
|
||||||
const key = await crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: "PBKDF2",
|
|
||||||
salt,
|
|
||||||
iterations: 100000,
|
|
||||||
hash: "SHA-256",
|
|
||||||
},
|
|
||||||
keyMaterial,
|
|
||||||
{ name: "AES-GCM", length: 256 },
|
|
||||||
false,
|
|
||||||
["decrypt"]
|
|
||||||
);
|
|
||||||
decKeyBuffs.push({
|
|
||||||
index: bufKey,
|
|
||||||
key,
|
|
||||||
salt,
|
|
||||||
});
|
|
||||||
while (decKeyBuffs.length > 50) {
|
|
||||||
decKeyBuffs.shift();
|
|
||||||
}
|
|
||||||
return [key, salt];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSemiStaticField(reset?: boolean) {
|
|
||||||
// return fixed field of iv.
|
|
||||||
if (semiStaticFieldBuffer != null && !reset) {
|
|
||||||
return semiStaticFieldBuffer;
|
|
||||||
}
|
|
||||||
semiStaticFieldBuffer = crypto.getRandomValues(new Uint8Array(12));
|
|
||||||
return semiStaticFieldBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNonce() {
|
|
||||||
// This is nonce, so do not send same thing.
|
|
||||||
nonceBuffer[0]++;
|
|
||||||
if (nonceBuffer[0] > 10000) {
|
|
||||||
// reset semi-static field.
|
|
||||||
getSemiStaticField(true);
|
|
||||||
}
|
|
||||||
return nonceBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function uint8ArrayToHexString(src: Uint8Array): string {
|
|
||||||
return Array.from(src)
|
|
||||||
.map((e: number): string => `00${e.toString(16)}`.slice(-2))
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
function hexStringToUint8Array(src: string): Uint8Array {
|
|
||||||
const srcArr = [...src];
|
|
||||||
const arr = srcArr.reduce((acc, _, i) => (i % 2 ? acc : [...acc, srcArr.slice(i, i + 2).join("")]), []).map((e) => parseInt(e, 16));
|
|
||||||
return Uint8Array.from(arr);
|
|
||||||
}
|
|
||||||
export async function encrypt(input: string, passphrase: string) {
|
|
||||||
const [key, salt] = await getKeyForEncrypt(passphrase);
|
|
||||||
// Create initial vector with semifixed part and incremental part
|
|
||||||
// I think it's not good against related-key attacks.
|
|
||||||
const fixedPart = getSemiStaticField();
|
|
||||||
const invocationPart = getNonce();
|
|
||||||
const iv = Uint8Array.from([...fixedPart, ...new Uint8Array(invocationPart.buffer)]);
|
|
||||||
const plainStringified: string = JSON.stringify(input);
|
|
||||||
const plainStringBuffer: Uint8Array = new TextEncoder().encode(plainStringified);
|
|
||||||
const encryptedDataArrayBuffer = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plainStringBuffer);
|
|
||||||
|
|
||||||
const encryptedData = window.btoa(Array.from(new Uint8Array(encryptedDataArrayBuffer), (char) => String.fromCharCode(char)).join(""));
|
|
||||||
|
|
||||||
//return data with iv and salt.
|
|
||||||
const response: encodedData = [encryptedData, uint8ArrayToHexString(iv), uint8ArrayToHexString(salt)];
|
|
||||||
const ret = JSON.stringify(response);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function decrypt(encryptedResult: string, passphrase: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
const [encryptedData, ivString, salt]: encodedData = JSON.parse(encryptedResult);
|
|
||||||
const [key] = await getKeyForDecryption(passphrase, hexStringToUint8Array(salt));
|
|
||||||
const iv = hexStringToUint8Array(ivString);
|
|
||||||
// decode base 64, it should increase speed and i should with in MAX_DOC_SIZE_BIN, so it won't OOM.
|
|
||||||
const encryptedDataBin = window.atob(encryptedData);
|
|
||||||
const encryptedDataArrayBuffer = Uint8Array.from(encryptedDataBin.split(""), (char) => char.charCodeAt(0));
|
|
||||||
const plainStringBuffer: ArrayBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, encryptedDataArrayBuffer);
|
|
||||||
const plainStringified = new TextDecoder().decode(plainStringBuffer);
|
|
||||||
const plain = JSON.parse(plainStringified);
|
|
||||||
return plain;
|
|
||||||
} catch (ex) {
|
|
||||||
Logger("Couldn't decode! You should wrong the passphrases", LOG_LEVEL.VERBOSE);
|
|
||||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function testCrypt() {
|
|
||||||
const src = "supercalifragilisticexpialidocious";
|
|
||||||
const encoded = await encrypt(src, "passwordTest");
|
|
||||||
const decrypted = await decrypt(encoded, "passwordTest");
|
|
||||||
if (src != decrypted) {
|
|
||||||
Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.VERBOSE);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
Logger("CRYPT LOGIC OK", LOG_LEVEL.VERBOSE);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
src/lib
Submodule
1
src/lib
Submodule
Submodule src/lib added at 4decf16d62
@@ -1,13 +0,0 @@
|
|||||||
import { LOG_LEVEL } from "./types";
|
|
||||||
|
|
||||||
// eslint-disable-next-line require-await
|
|
||||||
export let Logger: (message: any, levlel?: LOG_LEVEL) => Promise<void> = async (message, _) => {
|
|
||||||
const timestamp = new Date().toLocaleString();
|
|
||||||
const messagecontent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
|
|
||||||
const newmessage = timestamp + "->" + messagecontent;
|
|
||||||
console.log(newmessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function setLogger(loggerFun: (message: any, levlel?: LOG_LEVEL) => Promise<void>) {
|
|
||||||
Logger = loggerFun;
|
|
||||||
}
|
|
||||||
642
src/main.ts
642
src/main.ts
@@ -1,33 +1,38 @@
|
|||||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App } from "obsidian";
|
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App, FuzzySuggestModal, Setting } from "obsidian";
|
||||||
import { diff_match_patch } from "diff-match-patch";
|
import { diff_match_patch } from "diff-match-patch";
|
||||||
|
|
||||||
|
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID } from "./lib/src/types";
|
||||||
|
import { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList } from "./types";
|
||||||
import {
|
import {
|
||||||
EntryDoc,
|
base64ToString,
|
||||||
LoadedEntry,
|
arrayBufferToBase64,
|
||||||
ObsidianLiveSyncSettings,
|
base64ToArrayBuffer,
|
||||||
diff_check_result,
|
isValidPath,
|
||||||
diff_result_leaf,
|
versionNumberString2Number,
|
||||||
EntryBody,
|
runWithLock,
|
||||||
PluginDataEntry,
|
shouldBeIgnored,
|
||||||
LOG_LEVEL,
|
getProcessingCounts,
|
||||||
VER,
|
setLockNotifier,
|
||||||
PERIODIC_PLUGIN_SWEEP,
|
isPlainText,
|
||||||
DEFAULT_SETTINGS,
|
setNoticeClass,
|
||||||
PluginList,
|
NewNotice,
|
||||||
DevicePluginList,
|
allSettledWithConcurrencyLimit,
|
||||||
diff_result,
|
getLocks,
|
||||||
FLAGMD_REDFLAG,
|
} from "./lib/src/utils";
|
||||||
} from "./types";
|
import { Logger, setLogger } from "./lib/src/logger";
|
||||||
import { base64ToString, arrayBufferToBase64, base64ToArrayBuffer, isValidPath, versionNumberString2Number, id2path, path2id, runWithLock, shouldBeIgnored, getProcessingCounts, setLockNotifier, isPlainText } from "./utils";
|
|
||||||
import { Logger, setLogger } from "./logger";
|
|
||||||
import { LocalPouchDB } from "./LocalPouchDB";
|
import { LocalPouchDB } from "./LocalPouchDB";
|
||||||
import { LogDisplayModal } from "./LogDisplayModal";
|
import { LogDisplayModal } from "./LogDisplayModal";
|
||||||
import { ConflictResolveModal } from "./ConflictResolveModal";
|
import { ConflictResolveModal } from "./ConflictResolveModal";
|
||||||
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
import PluginPane from "./PluginPane.svelte";
|
import PluginPane from "./PluginPane.svelte";
|
||||||
|
import { id2path, path2id } from "./utils";
|
||||||
|
import { decrypt, encrypt } from "./lib/src/e2ee_v2";
|
||||||
|
|
||||||
|
const isDebug = false;
|
||||||
|
setNoticeClass(Notice);
|
||||||
class PluginDialogModal extends Modal {
|
class PluginDialogModal extends Modal {
|
||||||
plugin: ObsidianLiveSyncPlugin;
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
logEl: HTMLDivElement;
|
logEl: HTMLDivElement;
|
||||||
@@ -56,6 +61,108 @@ class PluginDialogModal extends Modal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class InputStringDialog extends Modal {
|
||||||
|
result: string | false = false;
|
||||||
|
onSubmit: (result: string | boolean) => void;
|
||||||
|
title: string;
|
||||||
|
key: string;
|
||||||
|
placeholder: string;
|
||||||
|
isManuallyClosed = false;
|
||||||
|
|
||||||
|
constructor(app: App, title: string, key: string, placeholder: string, onSubmit: (result: string | false) => void) {
|
||||||
|
super(app);
|
||||||
|
this.onSubmit = onSubmit;
|
||||||
|
this.title = title;
|
||||||
|
this.placeholder = placeholder;
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen() {
|
||||||
|
const { contentEl } = this;
|
||||||
|
|
||||||
|
contentEl.createEl("h1", { text: this.title });
|
||||||
|
|
||||||
|
new Setting(contentEl).setName(this.key).addText((text) =>
|
||||||
|
text.onChange((value) => {
|
||||||
|
this.result = value;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
new Setting(contentEl).addButton((btn) =>
|
||||||
|
btn
|
||||||
|
.setButtonText("Ok")
|
||||||
|
.setCta()
|
||||||
|
.onClick(() => {
|
||||||
|
this.isManuallyClosed = true;
|
||||||
|
this.close();
|
||||||
|
})
|
||||||
|
).addButton((btn) =>
|
||||||
|
btn
|
||||||
|
.setButtonText("Cancel")
|
||||||
|
.setCta()
|
||||||
|
.onClick(() => {
|
||||||
|
this.close();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
const { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
if (this.isManuallyClosed) {
|
||||||
|
this.onSubmit(this.result);
|
||||||
|
} else {
|
||||||
|
this.onSubmit(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class PopoverYesNo extends FuzzySuggestModal<string> {
|
||||||
|
app: App;
|
||||||
|
callback: (e: string) => void = () => { };
|
||||||
|
|
||||||
|
constructor(app: App, note: string, callback: (e: string) => void) {
|
||||||
|
super(app);
|
||||||
|
this.app = app;
|
||||||
|
this.setPlaceholder("y/n) " + note);
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
getItems(): string[] {
|
||||||
|
return ["yes", "no"];
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemText(item: string): string {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void {
|
||||||
|
// debugger;
|
||||||
|
this.callback(item);
|
||||||
|
this.callback = null;
|
||||||
|
}
|
||||||
|
onClose(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.callback != null) {
|
||||||
|
this.callback("");
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
|
||||||
|
return new Promise((res) => {
|
||||||
|
const popover = new PopoverYesNo(app, message, (result) => res(result as "yes" | "no"));
|
||||||
|
popover.open();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const askString = (app: App, title: string, key: string, placeholder: string): Promise<string | false> => {
|
||||||
|
return new Promise((res) => {
|
||||||
|
const dialog = new InputStringDialog(app, title, key, placeholder, (result) => res(result));
|
||||||
|
dialog.open();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default class ObsidianLiveSyncPlugin extends Plugin {
|
export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||||
settings: ObsidianLiveSyncSettings;
|
settings: ObsidianLiveSyncSettings;
|
||||||
localDatabase: LocalPouchDB;
|
localDatabase: LocalPouchDB;
|
||||||
@@ -64,6 +171,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
statusBar2: HTMLElement;
|
statusBar2: HTMLElement;
|
||||||
suspended: boolean;
|
suspended: boolean;
|
||||||
deviceAndVaultName: string;
|
deviceAndVaultName: string;
|
||||||
|
isMobile = false;
|
||||||
|
|
||||||
setInterval(handler: () => any, timeout?: number): number {
|
setInterval(handler: () => any, timeout?: number): number {
|
||||||
const timer = window.setInterval(handler, timeout);
|
const timer = window.setInterval(handler, timeout);
|
||||||
@@ -93,6 +201,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
const lsname = "obsidian-live-sync-ver" + this.app.vault.getName();
|
const lsname = "obsidian-live-sync-ver" + this.app.vault.getName();
|
||||||
const last_version = localStorage.getItem(lsname);
|
const last_version = localStorage.getItem(lsname);
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
|
//@ts-ignore
|
||||||
|
if (this.app.isMobile) {
|
||||||
|
this.isMobile = true;
|
||||||
|
this.settings.disableRequestURI = true;
|
||||||
|
}
|
||||||
if (last_version && Number(last_version) < VER) {
|
if (last_version && Number(last_version) < VER) {
|
||||||
this.settings.liveSync = false;
|
this.settings.liveSync = false;
|
||||||
this.settings.syncOnSave = false;
|
this.settings.syncOnSave = false;
|
||||||
@@ -155,50 +268,157 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
|
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
|
||||||
|
|
||||||
this.app.workspace.onLayoutReady(async () => {
|
this.app.workspace.onLayoutReady(async () => {
|
||||||
try {
|
if (this.localDatabase.isReady)
|
||||||
if (this.isRedFlagRaised()) {
|
try {
|
||||||
this.settings.batchSave = false;
|
if (this.isRedFlagRaised()) {
|
||||||
this.settings.liveSync = false;
|
this.settings.batchSave = false;
|
||||||
this.settings.periodicReplication = false;
|
this.settings.liveSync = false;
|
||||||
this.settings.syncOnSave = false;
|
this.settings.periodicReplication = false;
|
||||||
this.settings.syncOnStart = false;
|
this.settings.syncOnSave = false;
|
||||||
this.settings.syncOnFileOpen = false;
|
this.settings.syncOnStart = false;
|
||||||
this.settings.autoSweepPlugins = false;
|
this.settings.syncOnFileOpen = false;
|
||||||
this.settings.usePluginSync = false;
|
this.settings.autoSweepPlugins = false;
|
||||||
this.settings.suspendFileWatching = true;
|
this.settings.usePluginSync = false;
|
||||||
await this.saveSettings();
|
this.settings.suspendFileWatching = true;
|
||||||
await this.openDatabase();
|
await this.saveSettings();
|
||||||
const warningMessage = "The red flag is raised! The whole initialize steps are skipped, and any file changes are not captured.";
|
await this.openDatabase();
|
||||||
Logger(warningMessage, LOG_LEVEL.NOTICE);
|
const warningMessage = "The red flag is raised! The whole initialize steps are skipped, and any file changes are not captured.";
|
||||||
this.setStatusBarText(warningMessage);
|
Logger(warningMessage, LOG_LEVEL.NOTICE);
|
||||||
} else {
|
this.setStatusBarText(warningMessage);
|
||||||
if (this.settings.suspendFileWatching) {
|
} else {
|
||||||
Logger("'Suspend file watching' turned on. Are you sure this is what you intended? Every modification on the vault will be ignored.", LOG_LEVEL.NOTICE);
|
if (this.settings.suspendFileWatching) {
|
||||||
|
Logger("'Suspend file watching' turned on. Are you sure this is what you intended? Every modification on the vault will be ignored.", LOG_LEVEL.NOTICE);
|
||||||
|
}
|
||||||
|
const isInitalized = await this.initializeDatabase();
|
||||||
|
if (!isInitalized) {
|
||||||
|
//TODO:stop all sync.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await this.initializeDatabase();
|
await this.realizeSettingSyncMode();
|
||||||
|
this.registerWatchEvents();
|
||||||
|
if (this.settings.syncOnStart) {
|
||||||
|
this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
Logger("Error while loading Self-hosted LiveSync", LOG_LEVEL.NOTICE);
|
||||||
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
}
|
}
|
||||||
await this.realizeSettingSyncMode();
|
});
|
||||||
this.registerWatchEvents();
|
const configURIBase = "obsidian://setuplivesync?settings=";
|
||||||
if (this.settings.syncOnStart) {
|
this.addCommand({
|
||||||
await this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
|
id: "livesync-copysetupuri",
|
||||||
|
name: "Copy setup URI (beta)",
|
||||||
|
callback: async () => {
|
||||||
|
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "Passphrase", "");
|
||||||
|
if (encryptingPassphrase === false) return;
|
||||||
|
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(this.settings), encryptingPassphrase));
|
||||||
|
const uri = `${configURIBase}${encryptedSetting}`;
|
||||||
|
await navigator.clipboard.writeText(uri);
|
||||||
|
Logger("Setup URI copied to clipboard", LOG_LEVEL.NOTICE);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.addCommand({
|
||||||
|
id: "livesync-opensetupuri",
|
||||||
|
name: "Open setup URI (beta)",
|
||||||
|
callback: async () => {
|
||||||
|
const setupURI = await askString(this.app, "Set up manually", "Set up URI", `${configURIBase}aaaaa`);
|
||||||
|
if (setupURI === false) return;
|
||||||
|
if (!setupURI.startsWith(`${configURIBase}`)) {
|
||||||
|
Logger("Set up URI looks wrong.", LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const config = decodeURIComponent(setupURI.substring(configURIBase.length));
|
||||||
|
console.dir(config)
|
||||||
|
await setupwizard(config);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const setupwizard = async (confString: string) => {
|
||||||
|
try {
|
||||||
|
const oldConf = JSON.parse(JSON.stringify(this.settings));
|
||||||
|
const encryptingPassphrase = await askString(this.app, "Passphrase", "Passphrase for your settings", "");
|
||||||
|
if (encryptingPassphrase === false) return;
|
||||||
|
const newconf = await JSON.parse(await decrypt(confString, encryptingPassphrase));
|
||||||
|
if (newconf) {
|
||||||
|
const result = await askYesNo(this.app, "Importing LiveSync's conf, OK?");
|
||||||
|
if (result == "yes") {
|
||||||
|
const newSettingW = Object.assign({}, this.settings, newconf);
|
||||||
|
// stopping once.
|
||||||
|
this.localDatabase.closeReplication();
|
||||||
|
this.settings.suspendFileWatching = true;
|
||||||
|
console.dir(newSettingW);
|
||||||
|
const keepLocalDB = await askYesNo(this.app, "Keep local DB?");
|
||||||
|
const keepRemoteDB = await askYesNo(this.app, "Keep remote DB?");
|
||||||
|
if (keepLocalDB == "yes" && keepRemoteDB == "yes") {
|
||||||
|
// nothing to do. so peaceful.
|
||||||
|
this.settings = newSettingW;
|
||||||
|
await this.saveSettings();
|
||||||
|
const replicate = await askYesNo(this.app, "Unlock and replicate?");
|
||||||
|
if (replicate == "yes") {
|
||||||
|
await this.replicate(true);
|
||||||
|
await this.markRemoteUnlocked();
|
||||||
|
}
|
||||||
|
Logger("Configuration loaded.", LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (keepLocalDB == "no" && keepRemoteDB == "no") {
|
||||||
|
const reset = await askYesNo(this.app, "Drop everything?");
|
||||||
|
if (reset != "yes") {
|
||||||
|
Logger("Cancelled", LOG_LEVEL.NOTICE);
|
||||||
|
this.settings = oldConf;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let initDB;
|
||||||
|
await this.saveSettings();
|
||||||
|
if (keepLocalDB == "no") {
|
||||||
|
this.resetLocalOldDatabase();
|
||||||
|
this.resetLocalDatabase();
|
||||||
|
this.localDatabase.initializeDatabase();
|
||||||
|
const rebuild = await askYesNo(this.app, "Rebuild the database?");
|
||||||
|
if (rebuild == "yes") {
|
||||||
|
initDB = this.initializeDatabase(true);
|
||||||
|
} else {
|
||||||
|
this.markRemoteResolved();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (keepRemoteDB == "no") {
|
||||||
|
await this.tryResetRemoteDatabase();
|
||||||
|
await this.markRemoteLocked();
|
||||||
|
}
|
||||||
|
if (keepLocalDB == "no" || keepRemoteDB == "no") {
|
||||||
|
const replicate = await askYesNo(this.app, "Replicate once?");
|
||||||
|
if (replicate == "yes") {
|
||||||
|
if (initDB != null) {
|
||||||
|
await initDB;
|
||||||
|
}
|
||||||
|
await this.replicate(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger("Configuration loaded.", LOG_LEVEL.NOTICE);
|
||||||
|
} else {
|
||||||
|
Logger("Cancelled.", LOG_LEVEL.NOTICE);
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger("Error while loading Self-hosted LiveSync", LOG_LEVEL.NOTICE);
|
Logger("Couldn't parse or decrypt configuration uri.", LOG_LEVEL.NOTICE);
|
||||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
|
||||||
|
await setupwizard(conf.settings);
|
||||||
});
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-replicate",
|
id: "livesync-replicate",
|
||||||
name: "Replicate now",
|
name: "Replicate now",
|
||||||
callback: () => {
|
callback: async () => {
|
||||||
this.replicate();
|
await this.replicate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-dump",
|
id: "livesync-dump",
|
||||||
name: "Dump informations of this doc ",
|
name: "Dump informations of this doc ",
|
||||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||||
this.localDatabase.disposeHashCache();
|
|
||||||
this.localDatabase.getDBEntry(view.file.path, {}, true, false);
|
this.localDatabase.getDBEntry(view.file.path, {}, true, false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -287,15 +507,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
this.hidePluginSyncModal();
|
this.hidePluginSyncModal();
|
||||||
this.localDatabase.onunload();
|
if (this.localDatabase != null) {
|
||||||
|
this.localDatabase.onunload();
|
||||||
|
}
|
||||||
if (this.gcTimerHandler != null) {
|
if (this.gcTimerHandler != null) {
|
||||||
clearTimeout(this.gcTimerHandler);
|
clearTimeout(this.gcTimerHandler);
|
||||||
this.gcTimerHandler = null;
|
this.gcTimerHandler = null;
|
||||||
}
|
}
|
||||||
this.clearPeriodicSync();
|
this.clearPeriodicSync();
|
||||||
this.clearPluginSweep();
|
this.clearPluginSweep();
|
||||||
this.localDatabase.closeReplication();
|
if (this.localDatabase != null) {
|
||||||
this.localDatabase.close();
|
this.localDatabase.closeReplication();
|
||||||
|
this.localDatabase.close();
|
||||||
|
}
|
||||||
window.removeEventListener("visibilitychange", this.watchWindowVisiblity);
|
window.removeEventListener("visibilitychange", this.watchWindowVisiblity);
|
||||||
Logger("unloading plugin");
|
Logger("unloading plugin");
|
||||||
}
|
}
|
||||||
@@ -306,11 +530,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
const vaultName = this.app.vault.getName();
|
const vaultName = this.app.vault.getName();
|
||||||
Logger("Open Database...");
|
Logger("Open Database...");
|
||||||
this.localDatabase = new LocalPouchDB(this.settings, vaultName);
|
//@ts-ignore
|
||||||
|
const isMobile = this.app.isMobile;
|
||||||
|
this.localDatabase = new LocalPouchDB(this.settings, vaultName, isMobile);
|
||||||
this.localDatabase.updateInfo = () => {
|
this.localDatabase.updateInfo = () => {
|
||||||
this.refreshStatusText();
|
this.refreshStatusText();
|
||||||
};
|
};
|
||||||
await this.localDatabase.initializeDatabase();
|
return await this.localDatabase.initializeDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
async garbageCollect() {
|
async garbageCollect() {
|
||||||
@@ -321,6 +547,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||||
this.settings.workingEncrypt = this.settings.encrypt;
|
this.settings.workingEncrypt = this.settings.encrypt;
|
||||||
this.settings.workingPassphrase = this.settings.passphrase;
|
this.settings.workingPassphrase = this.settings.passphrase;
|
||||||
|
// Delete this feature to avoid problems on mobile.
|
||||||
|
this.settings.disableRequestURI = true;
|
||||||
|
// Temporary disabled
|
||||||
|
// TODO: If a new GC is created, a new default value must be created.
|
||||||
|
this.settings.gcDelay = 0;
|
||||||
|
|
||||||
const lsname = "obsidian-live-sync-vaultanddevicename-" + this.app.vault.getName();
|
const lsname = "obsidian-live-sync-vaultanddevicename-" + this.app.vault.getName();
|
||||||
if (this.settings.deviceAndVaultName != "") {
|
if (this.settings.deviceAndVaultName != "") {
|
||||||
if (!localStorage.getItem(lsname)) {
|
if (!localStorage.getItem(lsname)) {
|
||||||
@@ -365,7 +597,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.registerEvent(this.app.vault.on("modify", this.watchVaultChange));
|
this.registerEvent(this.app.vault.on("modify", this.watchVaultChange));
|
||||||
this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete));
|
this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete));
|
||||||
this.registerEvent(this.app.vault.on("rename", this.watchVaultRename));
|
this.registerEvent(this.app.vault.on("rename", this.watchVaultRename));
|
||||||
this.registerEvent(this.app.vault.on("create", this.watchVaultChange));
|
this.registerEvent(this.app.vault.on("create", this.watchVaultCreate));
|
||||||
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
||||||
window.addEventListener("visibilitychange", this.watchWindowVisiblity);
|
window.addEventListener("visibilitychange", this.watchWindowVisiblity);
|
||||||
}
|
}
|
||||||
@@ -389,10 +621,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
await this.sweepPlugin(false);
|
await this.sweepPlugin(false);
|
||||||
}
|
}
|
||||||
if (this.settings.liveSync) {
|
if (this.settings.liveSync) {
|
||||||
await this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
|
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
|
||||||
}
|
}
|
||||||
if (this.settings.syncOnStart) {
|
if (this.settings.syncOnStart) {
|
||||||
await this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
|
this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
|
||||||
}
|
}
|
||||||
if (this.settings.periodicReplication) {
|
if (this.settings.periodicReplication) {
|
||||||
this.setPeriodicSync();
|
this.setPeriodicSync();
|
||||||
@@ -408,11 +640,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
async watchWorkspaceOpenAsync(file: TFile) {
|
async watchWorkspaceOpenAsync(file: TFile) {
|
||||||
await this.applyBatchChange();
|
await this.applyBatchChange();
|
||||||
if (file == null) return;
|
if (file == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.settings.syncOnFileOpen && !this.suspended) {
|
if (this.settings.syncOnFileOpen && !this.suspended) {
|
||||||
await this.replicate();
|
await this.replicate();
|
||||||
}
|
}
|
||||||
this.localDatabase.disposeHashCache();
|
|
||||||
await this.showIfConflicted(file);
|
await this.showIfConflicted(file);
|
||||||
this.gcHook();
|
this.gcHook();
|
||||||
}
|
}
|
||||||
@@ -449,7 +682,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
async applyBatchChange() {
|
async applyBatchChange() {
|
||||||
if (!this.settings.batchSave || this.batchFileChange.length == 0) {
|
if (!this.settings.batchSave || this.batchFileChange.length == 0) {
|
||||||
return [];
|
return;
|
||||||
}
|
}
|
||||||
return await runWithLock("batchSave", false, async () => {
|
return await runWithLock("batchSave", false, async () => {
|
||||||
const batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[];
|
const batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[];
|
||||||
@@ -467,7 +700,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.refreshStatusText();
|
this.refreshStatusText();
|
||||||
return await Promise.all(promises);
|
await allSettledWithConcurrencyLimit(promises, 3);
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,7 +718,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
// When save is delayed, it should be cancelled.
|
// When save is delayed, it should be cancelled.
|
||||||
this.batchFileChange = this.batchFileChange.filter((e) => e == file.path);
|
this.batchFileChange = this.batchFileChange.filter((e) => e == file.path);
|
||||||
if (this.settings.suspendFileWatching) return;
|
if (this.settings.suspendFileWatching) return;
|
||||||
this.watchVaultDeleteAsync(file).then(() => {});
|
this.watchVaultDeleteAsync(file).then(() => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
async watchVaultDeleteAsync(file: TAbstractFile) {
|
async watchVaultDeleteAsync(file: TAbstractFile) {
|
||||||
@@ -513,7 +747,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
watchVaultRename(file: TAbstractFile, oldFile: any) {
|
watchVaultRename(file: TAbstractFile, oldFile: any) {
|
||||||
if (this.settings.suspendFileWatching) return;
|
if (this.settings.suspendFileWatching) return;
|
||||||
this.watchVaultRenameAsync(file, oldFile).then(() => {});
|
this.watchVaultRenameAsync(file, oldFile).then(() => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilePath(file: TAbstractFile): string {
|
getFilePath(file: TAbstractFile): string {
|
||||||
@@ -569,8 +803,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
//--> Basic document Functions
|
//--> Basic document Functions
|
||||||
notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {};
|
notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {};
|
||||||
|
|
||||||
|
lastLog = "";
|
||||||
// eslint-disable-next-line require-await
|
// eslint-disable-next-line require-await
|
||||||
async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO) {
|
async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO) {
|
||||||
|
if (level == LOG_LEVEL.DEBUG && !isDebug) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (level < LOG_LEVEL.INFO && this.settings && this.settings.lessInformationInLog) {
|
if (level < LOG_LEVEL.INFO && this.settings && this.settings.lessInformationInLog) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -584,6 +822,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
this.logMessage = [].concat(this.logMessage).concat([newmessage]).slice(-100);
|
this.logMessage = [].concat(this.logMessage).concat([newmessage]).slice(-100);
|
||||||
console.log(valutName + ":" + newmessage);
|
console.log(valutName + ":" + newmessage);
|
||||||
|
this.setStatusBarText(null, messagecontent.substring(0, 30));
|
||||||
|
// if (message instanceof Error) {
|
||||||
|
// console.trace(message);
|
||||||
|
// }
|
||||||
|
|
||||||
if (level >= LOG_LEVEL.NOTICE) {
|
if (level >= LOG_LEVEL.NOTICE) {
|
||||||
if (messagecontent in this.notifies) {
|
if (messagecontent in this.notifies) {
|
||||||
@@ -702,7 +944,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async doc2storate_modify(docEntry: EntryBody, file: TFile, force?: boolean) {
|
async doc2storage_modify(docEntry: EntryBody, file: TFile, force?: boolean) {
|
||||||
const pathSrc = id2path(docEntry._id);
|
const pathSrc = id2path(docEntry._id);
|
||||||
if (shouldBeIgnored(pathSrc)) {
|
if (shouldBeIgnored(pathSrc)) {
|
||||||
return;
|
return;
|
||||||
@@ -781,13 +1023,120 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
} else if (targetFile instanceof TFile) {
|
} else if (targetFile instanceof TFile) {
|
||||||
const doc = change;
|
const doc = change;
|
||||||
const file = targetFile;
|
const file = targetFile;
|
||||||
await this.doc2storate_modify(doc, file);
|
await this.doc2storage_modify(doc, file);
|
||||||
this.queueConflictedCheck(file);
|
if (!this.settings.checkConflictOnlyOnOpen) {
|
||||||
|
this.queueConflictedCheck(file);
|
||||||
|
} else {
|
||||||
|
const af = app.workspace.getActiveFile();
|
||||||
|
if (af && af.path == file.path) {
|
||||||
|
this.queueConflictedCheck(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger(`${id2path(change._id)} is already exist as the folder`);
|
Logger(`${id2path(change._id)} is already exist as the folder`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queuedFiles: {
|
||||||
|
entry: EntryBody;
|
||||||
|
missingChildren: string[];
|
||||||
|
timeout?: number;
|
||||||
|
done?: boolean;
|
||||||
|
warned?: boolean;
|
||||||
|
}[] = [];
|
||||||
|
chunkWaitTimeout = 60000;
|
||||||
|
|
||||||
|
async saveQueuedFiles() {
|
||||||
|
const saveData = JSON.stringify(this.queuedFiles.filter((e) => !e.done).map((e) => e.entry._id));
|
||||||
|
const lsname = "obsidian-livesync-queuefiles-" + this.app.vault.getName();
|
||||||
|
localStorage.setItem(lsname, saveData);
|
||||||
|
}
|
||||||
|
async loadQueuedFiles() {
|
||||||
|
const lsname = "obsidian-livesync-queuefiles-" + this.app.vault.getName();
|
||||||
|
const ids = JSON.parse(localStorage.getItem(lsname) || "[]") as string[];
|
||||||
|
const ret = await this.localDatabase.localDatabase.allDocs({ keys: ids, include_docs: true });
|
||||||
|
for (const doc of ret.rows) {
|
||||||
|
if (doc.doc && !this.queuedFiles.some((e) => e.entry._id == doc.doc._id)) {
|
||||||
|
await this.parseIncomingDoc(doc.doc as PouchDB.Core.ExistingDocument<EntryBody & PouchDB.Core.AllDocsMeta>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async procQueuedFiles() {
|
||||||
|
await runWithLock("procQueue", false, async () => {
|
||||||
|
this.saveQueuedFiles();
|
||||||
|
for (const queue of this.queuedFiles) {
|
||||||
|
if (queue.done) continue;
|
||||||
|
const now = new Date().getTime();
|
||||||
|
if (queue.missingChildren.length == 0) {
|
||||||
|
queue.done = true;
|
||||||
|
if (isValidPath(id2path(queue.entry._id))) {
|
||||||
|
Logger(`Applying ${queue.entry._id} (${queue.entry._rev}) change...`);
|
||||||
|
await this.handleDBChanged(queue.entry);
|
||||||
|
}
|
||||||
|
} else if (now > queue.timeout) {
|
||||||
|
if (!queue.warned) Logger(`Timed out: ${queue.entry._id} could not collect ${queue.missingChildren.length} chunks. plugin keeps watching, but you have to check the file after the replication.`, LOG_LEVEL.NOTICE);
|
||||||
|
queue.warned = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.queuedFiles = this.queuedFiles.filter((e) => !e.done);
|
||||||
|
this.saveQueuedFiles();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
parseIncomingChunk(chunk: PouchDB.Core.ExistingDocument<EntryDoc>) {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
let isNewFileCompleted = false;
|
||||||
|
|
||||||
|
for (const queue of this.queuedFiles) {
|
||||||
|
if (queue.done) continue;
|
||||||
|
if (queue.missingChildren.indexOf(chunk._id) !== -1) {
|
||||||
|
queue.missingChildren = queue.missingChildren.filter((e) => e != chunk._id);
|
||||||
|
queue.timeout = now + this.chunkWaitTimeout;
|
||||||
|
}
|
||||||
|
if (queue.missingChildren.length == 0) {
|
||||||
|
for (const e of this.queuedFiles) {
|
||||||
|
if (e.entry._id == queue.entry._id && e.entry.mtime < queue.entry.mtime) {
|
||||||
|
e.done = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isNewFileCompleted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNewFileCompleted) this.procQueuedFiles();
|
||||||
|
}
|
||||||
|
async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) {
|
||||||
|
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
|
||||||
|
if (skipOldFile) {
|
||||||
|
const info = this.app.vault.getAbstractFileByPath(id2path(doc._id));
|
||||||
|
|
||||||
|
if (info && info instanceof TFile) {
|
||||||
|
const localMtime = ~~((info as TFile).stat.mtime / 1000);
|
||||||
|
const docMtime = ~~(doc.mtime / 1000);
|
||||||
|
//TODO: some margin required.
|
||||||
|
if (localMtime >= docMtime) {
|
||||||
|
Logger(`${doc._id} Skipped, older than storage.`, LOG_LEVEL.VERBOSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const newQueue = {
|
||||||
|
entry: doc,
|
||||||
|
missingChildren: [] as string[],
|
||||||
|
timeout: now + this.chunkWaitTimeout,
|
||||||
|
};
|
||||||
|
if ("children" in doc) {
|
||||||
|
const c = await this.localDatabase.localDatabase.allDocs({ keys: doc.children, include_docs: false });
|
||||||
|
const missing = c.rows.filter((e) => "error" in e).map((e) => e.key);
|
||||||
|
Logger(`${doc._id}(${doc._rev}) Queued (waiting ${missing.length} items)`, LOG_LEVEL.VERBOSE);
|
||||||
|
newQueue.missingChildren = missing;
|
||||||
|
this.queuedFiles.push(newQueue);
|
||||||
|
} else {
|
||||||
|
this.queuedFiles.push(newQueue);
|
||||||
|
}
|
||||||
|
this.saveQueuedFiles();
|
||||||
|
this.procQueuedFiles();
|
||||||
|
}
|
||||||
periodicSyncHandler: number = null;
|
periodicSyncHandler: number = null;
|
||||||
|
|
||||||
//---> Sync
|
//---> Sync
|
||||||
@@ -801,11 +1150,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (change._id.startsWith("h:")) {
|
if (change._id.startsWith("h:")) {
|
||||||
|
await this.parseIncomingChunk(change);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (change._id == SYNCINFO_ID) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") {
|
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") {
|
||||||
Logger("replication change arrived", LOG_LEVEL.VERBOSE);
|
await this.parseIncomingDoc(change);
|
||||||
await this.handleDBChanged(change);
|
continue;
|
||||||
}
|
}
|
||||||
if (change.type == "versioninfo") {
|
if (change.type == "versioninfo") {
|
||||||
if (change.version > VER) {
|
if (change.version > VER) {
|
||||||
@@ -847,7 +1200,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
a.addEventListener("click", () => this.showPluginSyncModal());
|
a.addEventListener("click", () => this.showPluginSyncModal());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
new Notice(fragment, 10000);
|
NewNotice(fragment, 10000);
|
||||||
} else {
|
} else {
|
||||||
Logger("Everything is up to date.", LOG_LEVEL.NOTICE);
|
Logger("Everything is up to date.", LOG_LEVEL.NOTICE);
|
||||||
}
|
}
|
||||||
@@ -902,7 +1255,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
await this.sweepPlugin(false);
|
await this.sweepPlugin(false);
|
||||||
}
|
}
|
||||||
if (this.settings.liveSync) {
|
if (this.settings.liveSync) {
|
||||||
await this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
|
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
|
||||||
this.refreshStatusText();
|
this.refreshStatusText();
|
||||||
}
|
}
|
||||||
this.setPeriodicSync();
|
this.setPeriodicSync();
|
||||||
@@ -942,41 +1295,81 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
waiting = " " + this.batchFileChange.map((e) => "🛫").join("");
|
waiting = " " + this.batchFileChange.map((e) => "🛫").join("");
|
||||||
waiting = waiting.replace(/(🛫){10}/g, "🚀");
|
waiting = waiting.replace(/(🛫){10}/g, "🚀");
|
||||||
}
|
}
|
||||||
|
let queued = "";
|
||||||
|
const queue = Object.entries(this.queuedFiles).filter((e) => !e[1].warned);
|
||||||
|
const queuedCount = queue.length;
|
||||||
|
|
||||||
|
if (queuedCount) {
|
||||||
|
const pieces = queue.map((e) => e[1].missingChildren).reduce((prev, cur) => prev + cur.length, 0);
|
||||||
|
queued = ` 🧩 ${queuedCount} (${pieces})`;
|
||||||
|
}
|
||||||
const procs = getProcessingCounts();
|
const procs = getProcessingCounts();
|
||||||
const procsDisp = procs == 0 ? "" : ` ⏳${procs}`;
|
const procsDisp = procs == 0 ? "" : ` ⏳${procs}`;
|
||||||
const message = `Sync:${w} ↑${sent} ↓${arrived}${waiting}${procsDisp}`;
|
const message = `Sync:${w} ↑${sent} ↓${arrived}${waiting}${procsDisp}${queued}`;
|
||||||
this.setStatusBarText(message);
|
const locks = getLocks();
|
||||||
|
const pendingTask = locks.pending.length
|
||||||
|
? "\nPending: " +
|
||||||
|
Object.entries(locks.pending.reduce((p, c) => ({ ...p, [c]: (p[c] ?? 0) + 1 }), {} as { [key: string]: number }))
|
||||||
|
.map((e) => `${e[0]}${e[1] == 1 ? "" : `(${e[1]})`}`)
|
||||||
|
.join(", ")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const runningTask = locks.running.length
|
||||||
|
? "\nRunning: " +
|
||||||
|
Object.entries(locks.running.reduce((p, c) => ({ ...p, [c]: (p[c] ?? 0) + 1 }), {} as { [key: string]: number }))
|
||||||
|
.map((e) => `${e[0]}${e[1] == 1 ? "" : `(${e[1]})`}`)
|
||||||
|
.join(", ")
|
||||||
|
: "";
|
||||||
|
this.setStatusBarText(message + pendingTask + runningTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatusBarText(message: string) {
|
logHideTimer: NodeJS.Timeout = null;
|
||||||
if (this.lastMessage != message) {
|
setStatusBarText(message: string = null, log: string = null) {
|
||||||
this.statusBar.setText(message);
|
if (!this.statusBar) return;
|
||||||
|
const newMsg = typeof message == "string" ? message : this.lastMessage;
|
||||||
|
const newLog = typeof log == "string" ? log : this.lastLog;
|
||||||
|
if (`${this.lastMessage}-${this.lastLog}` != `${newMsg}-${newLog}`) {
|
||||||
|
this.statusBar.setText(newMsg.split("\n")[0]);
|
||||||
|
|
||||||
if (this.settings.showStatusOnEditor) {
|
if (this.settings.showStatusOnEditor) {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
root.style.setProperty("--slsmessage", '"' + message + '"');
|
root.style.setProperty("--slsmessage", '"' + (newMsg + "\n" + newLog).split("\n").join("\\a ") + '"');
|
||||||
} else {
|
} else {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
root.style.setProperty("--slsmessage", '""');
|
root.style.setProperty("--slsmessage", '""');
|
||||||
}
|
}
|
||||||
this.lastMessage = message;
|
if (this.logHideTimer != null) {
|
||||||
|
clearTimeout(this.logHideTimer);
|
||||||
|
}
|
||||||
|
this.logHideTimer = setTimeout(() => this.setStatusBarText(null, ""), 3000);
|
||||||
|
this.lastMessage = newMsg;
|
||||||
|
this.lastLog = newLog;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateStatusBarText() { }
|
||||||
|
|
||||||
async replicate(showMessage?: boolean) {
|
async replicate(showMessage?: boolean) {
|
||||||
if (this.settings.versionUpFlash != "") {
|
if (this.settings.versionUpFlash != "") {
|
||||||
new Notice("Open settings and check message, please.");
|
NewNotice("Open settings and check message, please.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.applyBatchChange();
|
await this.applyBatchChange();
|
||||||
if (this.settings.autoSweepPlugins) {
|
if (this.settings.autoSweepPlugins) {
|
||||||
await this.sweepPlugin(false);
|
await this.sweepPlugin(false);
|
||||||
}
|
}
|
||||||
await this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
|
await this.loadQueuedFiles();
|
||||||
|
this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializeDatabase(showingNotice?: boolean) {
|
async initializeDatabase(showingNotice?: boolean) {
|
||||||
await this.openDatabase();
|
if (await this.openDatabase()) {
|
||||||
await this.syncAllFiles(showingNotice);
|
if (this.localDatabase.isReady) {
|
||||||
|
await this.syncAllFiles(showingNotice);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async replicateAllToServer(showingNotice?: boolean) {
|
async replicateAllToServer(showingNotice?: boolean) {
|
||||||
@@ -1002,8 +1395,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
// synchronize all files between database and storage.
|
// synchronize all files between database and storage.
|
||||||
let notice: Notice = null;
|
let notice: Notice = null;
|
||||||
if (showingNotice) {
|
if (showingNotice) {
|
||||||
notice = new Notice("Initializing", 0);
|
notice = NewNotice("Initializing", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filesStorage = this.app.vault.getFiles();
|
const filesStorage = this.app.vault.getFiles();
|
||||||
const filesStorageName = filesStorage.map((e) => e.path);
|
const filesStorageName = filesStorage.map((e) => e.path);
|
||||||
const wf = await this.localDatabase.localDatabase.allDocs();
|
const wf = await this.localDatabase.localDatabase.allDocs();
|
||||||
@@ -1024,12 +1418,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
Logger(procedurename);
|
Logger(procedurename);
|
||||||
let i = 0;
|
let i = 0;
|
||||||
// let lastTicks = performance.now() + 2000;
|
// let lastTicks = performance.now() + 2000;
|
||||||
|
let workProcs = 0;
|
||||||
const procs = objects.map(async (e) => {
|
const procs = objects.map(async (e) => {
|
||||||
try {
|
try {
|
||||||
|
workProcs++;
|
||||||
await callback(e);
|
await callback(e);
|
||||||
i++;
|
i++;
|
||||||
if (i % 25 == 0) {
|
if (i % 25 == 0) {
|
||||||
const notify = `${procedurename} : ${i}/${count}`;
|
const notify = `${procedurename} : ${workProcs}/${count} (Pending:${workProcs})`;
|
||||||
if (notice != null) notice.setMessage(notify);
|
if (notice != null) notice.setMessage(notify);
|
||||||
Logger(notify);
|
Logger(notify);
|
||||||
this.setStatusBarText(notify);
|
this.setStatusBarText(notify);
|
||||||
@@ -1037,27 +1433,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(`Error while ${procedurename}`, LOG_LEVEL.NOTICE);
|
Logger(`Error while ${procedurename}`, LOG_LEVEL.NOTICE);
|
||||||
Logger(ex);
|
Logger(ex);
|
||||||
|
} finally {
|
||||||
|
workProcs--;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
|
||||||
if (!Promise.allSettled) {
|
await allSettledWithConcurrencyLimit(procs, 10);
|
||||||
await Promise.all(
|
Logger(`${procedurename} done.`);
|
||||||
procs.map((p) =>
|
|
||||||
p
|
|
||||||
.then((value) => ({
|
|
||||||
status: "fulfilled",
|
|
||||||
value,
|
|
||||||
}))
|
|
||||||
.catch((reason) => ({
|
|
||||||
status: "rejected",
|
|
||||||
reason,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
await Promise.allSettled(procs);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
await runAll("UPDATE DATABASE", onlyInStorage, async (e) => {
|
await runAll("UPDATE DATABASE", onlyInStorage, async (e) => {
|
||||||
Logger(`Update into ${e.path}`);
|
Logger(`Update into ${e.path}`);
|
||||||
@@ -1329,7 +1711,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
const file = targetFile;
|
const file = targetFile;
|
||||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||||
if (doc === false) return;
|
if (doc === false) return;
|
||||||
await this.doc2storate_modify(doc, file, force);
|
await this.doc2storage_modify(doc, file, force);
|
||||||
} else {
|
} else {
|
||||||
Logger(`target files:${filename} is exists as the folder`);
|
Logger(`target files:${filename} is exists as the folder`);
|
||||||
//something went wrong..
|
//something went wrong..
|
||||||
@@ -1343,23 +1725,30 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
const storageMtime = ~~(file.stat.mtime / 1000);
|
const storageMtime = ~~(file.stat.mtime / 1000);
|
||||||
const docMtime = ~~(doc.mtime / 1000);
|
const docMtime = ~~(doc.mtime / 1000);
|
||||||
if (storageMtime > docMtime) {
|
const dK = `${file.path}-diff`;
|
||||||
//newer local file.
|
const isLastDiff = (await this.localDatabase.kvDB.get<{ storageMtime: number; docMtime: number }>(dK)) || { storageMtime: 0, docMtime: 0 };
|
||||||
Logger("STORAGE -> DB :" + file.path);
|
if (isLastDiff.docMtime == docMtime && isLastDiff.storageMtime == storageMtime) {
|
||||||
Logger(`${storageMtime} > ${docMtime}`);
|
// Logger("CHECKED :" + file.path, LOG_LEVEL.VERBOSE);
|
||||||
await this.updateIntoDB(file);
|
|
||||||
} else if (storageMtime < docMtime) {
|
|
||||||
//newer database file.
|
|
||||||
Logger("STORAGE <- DB :" + file.path);
|
|
||||||
Logger(`${storageMtime} < ${docMtime}`);
|
|
||||||
const docx = await this.localDatabase.getDBEntry(file.path, null, false, false);
|
|
||||||
if (docx != false) {
|
|
||||||
await this.doc2storate_modify(docx, file);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Logger("EVEN :" + file.path, LOG_LEVEL.VERBOSE);
|
if (storageMtime > docMtime) {
|
||||||
// Logger(`${storageMtime} = ${docMtime}`, LOG_LEVEL.VERBOSE);
|
//newer local file.
|
||||||
//eq.case
|
Logger("STORAGE -> DB :" + file.path);
|
||||||
|
Logger(`${storageMtime} > ${docMtime}`);
|
||||||
|
await this.updateIntoDB(file);
|
||||||
|
} else if (storageMtime < docMtime) {
|
||||||
|
//newer database file.
|
||||||
|
Logger("STORAGE <- DB :" + file.path);
|
||||||
|
Logger(`${storageMtime} < ${docMtime}`);
|
||||||
|
const docx = await this.localDatabase.getDBEntry(file.path, null, false, false);
|
||||||
|
if (docx != false) {
|
||||||
|
await this.doc2storage_modify(docx, file);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Logger("EVEN :" + file.path, LOG_LEVEL.VERBOSE);
|
||||||
|
// Logger(`${storageMtime} = ${docMtime}`, LOG_LEVEL.VERBOSE);
|
||||||
|
//eq.case
|
||||||
|
}
|
||||||
|
await this.localDatabase.kvDB.set(dK, { storageMtime, docMtime });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1404,6 +1793,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
});
|
});
|
||||||
if (isNotChanged) return;
|
if (isNotChanged) return;
|
||||||
await this.localDatabase.putDBEntry(d);
|
await this.localDatabase.putDBEntry(d);
|
||||||
|
this.queuedFiles = this.queuedFiles.map((e) => ({ ...e, ...(e.entry._id == d._id ? { done: true } : {}) }));
|
||||||
|
|
||||||
Logger("put database:" + fullpath + "(" + datatype + ") ");
|
Logger("put database:" + fullpath + "(" + datatype + ") ");
|
||||||
if (this.settings.syncOnSave && !this.suspended) {
|
if (this.settings.syncOnSave && !this.suspended) {
|
||||||
@@ -1430,6 +1820,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
async resetLocalDatabase() {
|
async resetLocalDatabase() {
|
||||||
await this.localDatabase.resetDatabase();
|
await this.localDatabase.resetDatabase();
|
||||||
}
|
}
|
||||||
|
async resetLocalOldDatabase() {
|
||||||
|
await this.localDatabase.resetLocalOldDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
async tryResetRemoteDatabase() {
|
async tryResetRemoteDatabase() {
|
||||||
await this.localDatabase.tryResetRemoteDatabase(this.settings);
|
await this.localDatabase.tryResetRemoteDatabase(this.settings);
|
||||||
@@ -1461,24 +1854,21 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
async sweepPlugin(showMessage = false) {
|
async sweepPlugin(showMessage = false) {
|
||||||
if (!this.settings.usePluginSync) return;
|
if (!this.settings.usePluginSync) return;
|
||||||
await runWithLock("sweepplugin", false, async () => {
|
if (!this.localDatabase.isReady) return;
|
||||||
|
await runWithLock("sweepplugin", true, async () => {
|
||||||
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
||||||
if (!this.settings.encrypt) {
|
|
||||||
Logger("You have to encrypt the database to use plugin setting sync.", LOG_LEVEL.NOTICE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.deviceAndVaultName) {
|
if (!this.deviceAndVaultName) {
|
||||||
Logger("You have to set your device and vault name.", LOG_LEVEL.NOTICE);
|
Logger("You have to set your device and vault name.", LOG_LEVEL.NOTICE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Logger("Sweeping plugins", logLevel);
|
Logger("Scanning plugins", logLevel);
|
||||||
const db = this.localDatabase.localDatabase;
|
const db = this.localDatabase.localDatabase;
|
||||||
const oldDocs = await db.allDocs({
|
const oldDocs = await db.allDocs({
|
||||||
startkey: `ps:${this.deviceAndVaultName}-`,
|
startkey: `ps:${this.deviceAndVaultName}-`,
|
||||||
endkey: `ps:${this.deviceAndVaultName}.`,
|
endkey: `ps:${this.deviceAndVaultName}.`,
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
});
|
});
|
||||||
Logger("OLD DOCS.", LOG_LEVEL.VERBOSE);
|
// Logger("OLD DOCS.", LOG_LEVEL.VERBOSE);
|
||||||
// sweep current plugin.
|
// sweep current plugin.
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const pl = this.app.plugins;
|
const pl = this.app.plugins;
|
||||||
@@ -1543,7 +1933,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
return e.doc;
|
return e.doc;
|
||||||
});
|
});
|
||||||
await db.bulkDocs(delDocs);
|
await db.bulkDocs(delDocs);
|
||||||
Logger(`Sweep plugin done.`, logLevel);
|
Logger(`Scan plugin done.`, logLevel);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
src/pouchdb-browser.ts
Normal file
4
src/pouchdb-browser.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { PouchDB as PouchDB_ } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js";
|
||||||
|
|
||||||
|
const Pouch: PouchDB.Static = PouchDB_;
|
||||||
|
export { Pouch as PouchDB };
|
||||||
217
src/types.ts
217
src/types.ts
@@ -1,154 +1,7 @@
|
|||||||
// docs should be encoded as base64, so 1 char -> 1 bytes
|
|
||||||
// and cloudant limitation is 1MB , we use 900kb;
|
|
||||||
|
|
||||||
import { PluginManifest } from "obsidian";
|
import { PluginManifest } from "obsidian";
|
||||||
import * as PouchDB from "pouchdb";
|
import { DatabaseEntry } from "./lib/src/types";
|
||||||
|
|
||||||
export const MAX_DOC_SIZE = 1000; // for .md file, but if delimiters exists. use that before.
|
export interface PluginDataEntry extends DatabaseEntry {
|
||||||
export const MAX_DOC_SIZE_BIN = 102400; // 100kb
|
|
||||||
export const VER = 10;
|
|
||||||
|
|
||||||
export const RECENT_MOFIDIED_DOCS_QTY = 30;
|
|
||||||
export const LEAF_WAIT_TIMEOUT = 90000; // in synchronization, waiting missing leaf time out.
|
|
||||||
export const LOG_LEVEL = {
|
|
||||||
VERBOSE: 1,
|
|
||||||
INFO: 10,
|
|
||||||
NOTICE: 100,
|
|
||||||
URGENT: 1000,
|
|
||||||
} as const;
|
|
||||||
export type LOG_LEVEL = typeof LOG_LEVEL[keyof typeof LOG_LEVEL];
|
|
||||||
export const VERSIONINFO_DOCID = "obsydian_livesync_version";
|
|
||||||
export const MILSTONE_DOCID = "_local/obsydian_livesync_milestone";
|
|
||||||
export const NODEINFO_DOCID = "_local/obsydian_livesync_nodeinfo";
|
|
||||||
|
|
||||||
export interface ObsidianLiveSyncSettings {
|
|
||||||
couchDB_URI: string;
|
|
||||||
couchDB_USER: string;
|
|
||||||
couchDB_PASSWORD: string;
|
|
||||||
couchDB_DBNAME: string;
|
|
||||||
liveSync: boolean;
|
|
||||||
syncOnSave: boolean;
|
|
||||||
syncOnStart: boolean;
|
|
||||||
syncOnFileOpen: boolean;
|
|
||||||
savingDelay: number;
|
|
||||||
lessInformationInLog: boolean;
|
|
||||||
gcDelay: number;
|
|
||||||
versionUpFlash: string;
|
|
||||||
minimumChunkSize: number;
|
|
||||||
longLineThreshold: number;
|
|
||||||
showVerboseLog: boolean;
|
|
||||||
suspendFileWatching: boolean;
|
|
||||||
trashInsteadDelete: boolean;
|
|
||||||
periodicReplication: boolean;
|
|
||||||
periodicReplicationInterval: number;
|
|
||||||
encrypt: boolean;
|
|
||||||
passphrase: string;
|
|
||||||
workingEncrypt: boolean;
|
|
||||||
workingPassphrase: string;
|
|
||||||
doNotDeleteFolder: boolean;
|
|
||||||
resolveConflictsByNewerFile: boolean;
|
|
||||||
batchSave: boolean;
|
|
||||||
deviceAndVaultName: string;
|
|
||||||
usePluginSettings: boolean;
|
|
||||||
showOwnPlugins: boolean;
|
|
||||||
showStatusOnEditor: boolean;
|
|
||||||
usePluginSync: boolean;
|
|
||||||
autoSweepPlugins: boolean;
|
|
||||||
autoSweepPluginsPeriodic: boolean;
|
|
||||||
notifyPluginOrSettingUpdated: boolean;
|
|
||||||
checkIntegrityOnSave: boolean;
|
|
||||||
batch_size: number;
|
|
||||||
batches_limit: number;
|
|
||||||
useHistory: boolean;
|
|
||||||
disableRequestURI: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
|
|
||||||
couchDB_URI: "",
|
|
||||||
couchDB_USER: "",
|
|
||||||
couchDB_PASSWORD: "",
|
|
||||||
couchDB_DBNAME: "",
|
|
||||||
liveSync: false,
|
|
||||||
syncOnSave: false,
|
|
||||||
syncOnStart: false,
|
|
||||||
savingDelay: 200,
|
|
||||||
lessInformationInLog: false,
|
|
||||||
gcDelay: 300,
|
|
||||||
versionUpFlash: "",
|
|
||||||
minimumChunkSize: 20,
|
|
||||||
longLineThreshold: 250,
|
|
||||||
showVerboseLog: false,
|
|
||||||
suspendFileWatching: false,
|
|
||||||
trashInsteadDelete: true,
|
|
||||||
periodicReplication: false,
|
|
||||||
periodicReplicationInterval: 60,
|
|
||||||
syncOnFileOpen: false,
|
|
||||||
encrypt: false,
|
|
||||||
passphrase: "",
|
|
||||||
workingEncrypt: false,
|
|
||||||
workingPassphrase: "",
|
|
||||||
doNotDeleteFolder: false,
|
|
||||||
resolveConflictsByNewerFile: false,
|
|
||||||
batchSave: false,
|
|
||||||
deviceAndVaultName: "",
|
|
||||||
usePluginSettings: false,
|
|
||||||
showOwnPlugins: false,
|
|
||||||
showStatusOnEditor: false,
|
|
||||||
usePluginSync: false,
|
|
||||||
autoSweepPlugins: false,
|
|
||||||
autoSweepPluginsPeriodic: false,
|
|
||||||
notifyPluginOrSettingUpdated: false,
|
|
||||||
checkIntegrityOnSave: false,
|
|
||||||
batch_size: 250,
|
|
||||||
batches_limit: 40,
|
|
||||||
useHistory: false,
|
|
||||||
disableRequestURI: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PERIODIC_PLUGIN_SWEEP = 60;
|
|
||||||
|
|
||||||
export interface Entry {
|
|
||||||
_id: string;
|
|
||||||
data: string;
|
|
||||||
_rev?: string;
|
|
||||||
ctime: number;
|
|
||||||
mtime: number;
|
|
||||||
size: number;
|
|
||||||
_deleted?: boolean;
|
|
||||||
_conflicts?: string[];
|
|
||||||
type?: "notes";
|
|
||||||
}
|
|
||||||
export interface NewEntry {
|
|
||||||
_id: string;
|
|
||||||
children: string[];
|
|
||||||
_rev?: string;
|
|
||||||
ctime: number;
|
|
||||||
mtime: number;
|
|
||||||
size: number;
|
|
||||||
_deleted?: boolean;
|
|
||||||
_conflicts?: string[];
|
|
||||||
NewNote: true;
|
|
||||||
type: "newnote";
|
|
||||||
}
|
|
||||||
export interface PlainEntry {
|
|
||||||
_id: string;
|
|
||||||
children: string[];
|
|
||||||
_rev?: string;
|
|
||||||
ctime: number;
|
|
||||||
mtime: number;
|
|
||||||
size: number;
|
|
||||||
_deleted?: boolean;
|
|
||||||
NewNote: true;
|
|
||||||
_conflicts?: string[];
|
|
||||||
type: "plain";
|
|
||||||
}
|
|
||||||
export type LoadedEntry = Entry & {
|
|
||||||
children: string[];
|
|
||||||
datatype: "plain" | "newnote";
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface PluginDataEntry {
|
|
||||||
_id: string;
|
|
||||||
deviceVaultName: string;
|
deviceVaultName: string;
|
||||||
mtime: number;
|
mtime: number;
|
||||||
manifest: PluginManifest;
|
manifest: PluginManifest;
|
||||||
@@ -157,73 +10,10 @@ export interface PluginDataEntry {
|
|||||||
styleCss?: string;
|
styleCss?: string;
|
||||||
// it must be encrypted.
|
// it must be encrypted.
|
||||||
dataJson?: string;
|
dataJson?: string;
|
||||||
_rev?: string;
|
|
||||||
_deleted?: boolean;
|
|
||||||
_conflicts?: string[];
|
_conflicts?: string[];
|
||||||
type: "plugin";
|
type: "plugin";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntryLeaf {
|
|
||||||
_id: string;
|
|
||||||
data: string;
|
|
||||||
_deleted?: boolean;
|
|
||||||
type: "leaf";
|
|
||||||
_rev?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EntryVersionInfo {
|
|
||||||
_id: typeof VERSIONINFO_DOCID;
|
|
||||||
_rev?: string;
|
|
||||||
type: "versioninfo";
|
|
||||||
version: number;
|
|
||||||
_deleted?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EntryMilestoneInfo {
|
|
||||||
_id: typeof MILSTONE_DOCID;
|
|
||||||
_rev?: string;
|
|
||||||
type: "milestoneinfo";
|
|
||||||
_deleted?: boolean;
|
|
||||||
created: number;
|
|
||||||
accepted_nodes: string[];
|
|
||||||
locked: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EntryNodeInfo {
|
|
||||||
_id: typeof NODEINFO_DOCID;
|
|
||||||
_rev?: string;
|
|
||||||
_deleted?: boolean;
|
|
||||||
type: "nodeinfo";
|
|
||||||
nodeid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EntryBody = Entry | NewEntry | PlainEntry;
|
|
||||||
export type EntryDoc = EntryBody | LoadedEntry | EntryLeaf | EntryVersionInfo | EntryMilestoneInfo | EntryNodeInfo;
|
|
||||||
|
|
||||||
export type diff_result_leaf = {
|
|
||||||
rev: string;
|
|
||||||
data: string;
|
|
||||||
ctime: number;
|
|
||||||
mtime: number;
|
|
||||||
};
|
|
||||||
export type dmp_result = Array<[number, string]>;
|
|
||||||
|
|
||||||
export type diff_result = {
|
|
||||||
left: diff_result_leaf;
|
|
||||||
right: diff_result_leaf;
|
|
||||||
diff: dmp_result;
|
|
||||||
};
|
|
||||||
export type diff_check_result = boolean | diff_result;
|
|
||||||
|
|
||||||
export type Credential = {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EntryDocResponse = EntryDoc & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta;
|
|
||||||
|
|
||||||
export type DatabaseConnectingStatus = "STARTED" | "NOT_CONNECTED" | "PAUSED" | "CONNECTED" | "COMPLETED" | "CLOSED" | "ERRORED";
|
|
||||||
|
|
||||||
export interface PluginList {
|
export interface PluginList {
|
||||||
[key: string]: PluginDataEntry[];
|
[key: string]: PluginDataEntry[];
|
||||||
}
|
}
|
||||||
@@ -231,5 +21,4 @@ export interface PluginList {
|
|||||||
export interface DevicePluginList {
|
export interface DevicePluginList {
|
||||||
[key: string]: PluginDataEntry;
|
[key: string]: PluginDataEntry;
|
||||||
}
|
}
|
||||||
|
export const PERIODIC_PLUGIN_SWEEP = 60;
|
||||||
export const FLAGMD_REDFLAG = "redflag.md";
|
|
||||||
|
|||||||
243
src/utils.ts
243
src/utils.ts
@@ -1,249 +1,14 @@
|
|||||||
import { normalizePath } from "obsidian";
|
import { normalizePath } from "obsidian";
|
||||||
import { Logger } from "./logger";
|
|
||||||
import { FLAGMD_REDFLAG, LOG_LEVEL } from "./types";
|
|
||||||
|
|
||||||
export function arrayBufferToBase64(buffer: ArrayBuffer): Promise<string> {
|
import { path2id_base, id2path_base } from "./lib/src/utils";
|
||||||
return new Promise((res) => {
|
|
||||||
const blob = new Blob([buffer], { type: "application/octet-binary" });
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = function (evt) {
|
|
||||||
const dataurl = evt.target.result.toString();
|
|
||||||
res(dataurl.substr(dataurl.indexOf(",") + 1));
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(blob);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function base64ToString(base64: string): string {
|
|
||||||
try {
|
|
||||||
const binary_string = window.atob(base64);
|
|
||||||
const len = binary_string.length;
|
|
||||||
const bytes = new Uint8Array(len);
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
bytes[i] = binary_string.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return new TextDecoder().decode(bytes);
|
|
||||||
} catch (ex) {
|
|
||||||
return base64;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
|
||||||
try {
|
|
||||||
const binary_string = window.atob(base64);
|
|
||||||
const len = binary_string.length;
|
|
||||||
const bytes = new Uint8Array(len);
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
bytes[i] = binary_string.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return bytes.buffer;
|
|
||||||
} catch (ex) {
|
|
||||||
try {
|
|
||||||
return new Uint16Array(
|
|
||||||
[].map.call(base64, function (c: string) {
|
|
||||||
return c.charCodeAt(0);
|
|
||||||
})
|
|
||||||
).buffer;
|
|
||||||
} catch (ex2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const escapeStringToHTML = (str: string) => {
|
|
||||||
if (!str) return "";
|
|
||||||
return str.replace(/[<>&"'`]/g, (match) => {
|
|
||||||
const escape: any = {
|
|
||||||
"<": "<",
|
|
||||||
">": ">",
|
|
||||||
"&": "&",
|
|
||||||
'"': """,
|
|
||||||
"'": "'",
|
|
||||||
"`": "`",
|
|
||||||
};
|
|
||||||
return escape[match];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export function resolveWithIgnoreKnownError<T>(p: Promise<T>, def: T): Promise<T> {
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
p.then(res).catch((ex) => ((ex.status && ex.status == 404) || (ex.message && ex.message == "Request Error:404") ? res(def) : rej(ex)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidPath(filename: string): boolean {
|
|
||||||
// eslint-disable-next-line no-control-regex
|
|
||||||
const regex = /[\u0000-\u001f]|[\\":?<>|*#]/g;
|
|
||||||
let x = filename.replace(regex, "_");
|
|
||||||
const win = /(\\|\/)(COM\d|LPT\d|CON|PRN|AUX|NUL|CLOCK$)($|\.)/gi;
|
|
||||||
const sx = (x = x.replace(win, "/_"));
|
|
||||||
return sx == filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shouldBeIgnored(filename: string): boolean {
|
|
||||||
if (filename == FLAGMD_REDFLAG) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function versionNumberString2Number(version: string): number {
|
|
||||||
return version // "1.23.45"
|
|
||||||
.split(".") // 1 23 45
|
|
||||||
.reverse() // 45 23 1
|
|
||||||
.map((e, i) => ((e as any) / 1) * 1000 ** i) // 45 23000 1000000
|
|
||||||
.reduce((prev, current) => prev + current, 0); // 1023045
|
|
||||||
}
|
|
||||||
|
|
||||||
export const delay = (ms: number): Promise<void> => {
|
|
||||||
return new Promise((res) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
res();
|
|
||||||
}, ms);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// For backward compatibility, using the path for determining id.
|
// For backward compatibility, using the path for determining id.
|
||||||
// Only CouchDB nonacceptable ID (that starts with an underscore) has been prefixed with "/".
|
// Only CouchDB nonacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||||
// The first slash will be deleted when the path is normalized.
|
// The first slash will be deleted when the path is normalized.
|
||||||
export function path2id(filename: string): string {
|
export function path2id(filename: string): string {
|
||||||
let x = normalizePath(filename);
|
const x = normalizePath(filename);
|
||||||
if (x.startsWith("_")) x = "/" + x;
|
return path2id_base(x);
|
||||||
return x;
|
|
||||||
}
|
}
|
||||||
export function id2path(filename: string): string {
|
export function id2path(filename: string): string {
|
||||||
return normalizePath(filename);
|
return id2path_base(normalizePath(filename));
|
||||||
}
|
|
||||||
|
|
||||||
const runningProcs: string[] = [];
|
|
||||||
const pendingProcs: { [key: string]: (() => Promise<void>)[] } = {};
|
|
||||||
function objectToKey(key: any): string {
|
|
||||||
if (typeof key === "string") return key;
|
|
||||||
const keys = Object.keys(key).sort((a, b) => a.localeCompare(b));
|
|
||||||
return keys.map((e) => e + objectToKey(key[e])).join(":");
|
|
||||||
}
|
|
||||||
export function getProcessingCounts() {
|
|
||||||
let count = 0;
|
|
||||||
for (const v in pendingProcs) {
|
|
||||||
count += pendingProcs[v].length;
|
|
||||||
}
|
|
||||||
count += runningProcs.length;
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
let externalNotifier: () => void = () => {};
|
|
||||||
let notifyTimer: number = null;
|
|
||||||
export function setLockNotifier(fn: () => void) {
|
|
||||||
externalNotifier = fn;
|
|
||||||
}
|
|
||||||
function notifyLock() {
|
|
||||||
if (notifyTimer != null) {
|
|
||||||
window.clearTimeout(notifyTimer);
|
|
||||||
}
|
|
||||||
notifyTimer = window.setTimeout(() => {
|
|
||||||
externalNotifier();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
// Just run async/await as like transacion ISOLATION SERIALIZABLE
|
|
||||||
export function runWithLock<T>(key: unknown, ignoreWhenRunning: boolean, proc: () => Promise<T>): Promise<T> {
|
|
||||||
// Logger(`Lock:${key}:enter`, LOG_LEVEL.VERBOSE);
|
|
||||||
const lockKey = typeof key === "string" ? key : objectToKey(key);
|
|
||||||
const handleNextProcs = () => {
|
|
||||||
if (typeof pendingProcs[lockKey] === "undefined") {
|
|
||||||
//simply unlock
|
|
||||||
runningProcs.remove(lockKey);
|
|
||||||
notifyLock();
|
|
||||||
// Logger(`Lock:${lockKey}:released`, LOG_LEVEL.VERBOSE);
|
|
||||||
} else {
|
|
||||||
Logger(`Lock:${lockKey}:left ${pendingProcs[lockKey].length}`, LOG_LEVEL.VERBOSE);
|
|
||||||
let nextProc = null;
|
|
||||||
nextProc = pendingProcs[lockKey].shift();
|
|
||||||
notifyLock();
|
|
||||||
if (nextProc) {
|
|
||||||
// left some
|
|
||||||
nextProc()
|
|
||||||
.then()
|
|
||||||
.catch((err) => {
|
|
||||||
Logger(err);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (pendingProcs && lockKey in pendingProcs && pendingProcs[lockKey].length == 0) {
|
|
||||||
delete pendingProcs[lockKey];
|
|
||||||
notifyLock();
|
|
||||||
}
|
|
||||||
queueMicrotask(() => {
|
|
||||||
handleNextProcs();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (pendingProcs && lockKey in pendingProcs && pendingProcs[lockKey].length == 0) {
|
|
||||||
delete pendingProcs[lockKey];
|
|
||||||
notifyLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (runningProcs.contains(lockKey)) {
|
|
||||||
if (ignoreWhenRunning) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (typeof pendingProcs[lockKey] === "undefined") {
|
|
||||||
pendingProcs[lockKey] = [];
|
|
||||||
}
|
|
||||||
let responderRes: (value: T | PromiseLike<T>) => void;
|
|
||||||
let responderRej: (reason?: unknown) => void;
|
|
||||||
const responder = new Promise<T>((res, rej) => {
|
|
||||||
responderRes = res;
|
|
||||||
responderRej = rej;
|
|
||||||
//wait for subproc resolved
|
|
||||||
});
|
|
||||||
const subproc = () =>
|
|
||||||
new Promise<void>((res, rej) => {
|
|
||||||
proc()
|
|
||||||
.then((v) => {
|
|
||||||
// Logger(`Lock:${key}:processed`, LOG_LEVEL.VERBOSE);
|
|
||||||
handleNextProcs();
|
|
||||||
responderRes(v);
|
|
||||||
res();
|
|
||||||
})
|
|
||||||
.catch((reason) => {
|
|
||||||
Logger(`Lock:${key}:rejected`, LOG_LEVEL.VERBOSE);
|
|
||||||
handleNextProcs();
|
|
||||||
rej(reason);
|
|
||||||
responderRej(reason);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
pendingProcs[lockKey].push(subproc);
|
|
||||||
notifyLock();
|
|
||||||
// Logger(`Lock:${lockKey}:queud:left${pendingProcs[lockKey].length}`, LOG_LEVEL.VERBOSE);
|
|
||||||
return responder;
|
|
||||||
} else {
|
|
||||||
runningProcs.push(lockKey);
|
|
||||||
notifyLock();
|
|
||||||
// Logger(`Lock:${lockKey}:aqquired`, LOG_LEVEL.VERBOSE);
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
proc()
|
|
||||||
.then((v) => {
|
|
||||||
handleNextProcs();
|
|
||||||
res(v);
|
|
||||||
})
|
|
||||||
.catch((reason) => {
|
|
||||||
handleNextProcs();
|
|
||||||
rej(reason);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isPlainText(filename: string): boolean {
|
|
||||||
if (filename.endsWith(".md")) return true;
|
|
||||||
if (filename.endsWith(".txt")) return true;
|
|
||||||
if (filename.endsWith(".svg")) return true;
|
|
||||||
if (filename.endsWith(".html")) return true;
|
|
||||||
if (filename.endsWith(".csv")) return true;
|
|
||||||
if (filename.endsWith(".css")) return true;
|
|
||||||
if (filename.endsWith(".js")) return true;
|
|
||||||
if (filename.endsWith(".xml")) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Logger } from "./logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { LOG_LEVEL, VER, VERSIONINFO_DOCID, EntryVersionInfo, EntryDoc } from "./types";
|
import { LOG_LEVEL, VER, VERSIONINFO_DOCID, EntryVersionInfo, EntryDoc, RemoteDBSettings, SYNCINFO_ID, SyncInfo } from "./lib/src/types";
|
||||||
import { resolveWithIgnoreKnownError } from "./utils";
|
import { enableEncryption, resolveWithIgnoreKnownError } from "./lib/src/utils";
|
||||||
import { PouchDB } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js";
|
import { PouchDB } from "./pouchdb-browser";
|
||||||
import { requestUrl, RequestUrlParam } from "obsidian";
|
import { requestUrl, RequestUrlParam, RequestUrlResponse } from "obsidian";
|
||||||
|
|
||||||
export const isValidRemoteCouchDBURI = (uri: string): boolean => {
|
export const isValidRemoteCouchDBURI = (uri: string): boolean => {
|
||||||
if (uri.startsWith("https://")) return true;
|
if (uri.startsWith("https://")) return true;
|
||||||
@@ -13,9 +13,35 @@ let last_post_successed = false;
|
|||||||
export const getLastPostFailedBySize = () => {
|
export const getLastPostFailedBySize = () => {
|
||||||
return !last_post_successed;
|
return !last_post_successed;
|
||||||
};
|
};
|
||||||
|
const fetchByAPI = async (request: RequestUrlParam): Promise<RequestUrlResponse> => {
|
||||||
|
const ret = await requestUrl(request);
|
||||||
|
if (ret.status - (ret.status % 100) !== 200) {
|
||||||
|
const er: Error & { status?: number } = new Error(`Request Error:${ret.status}`);
|
||||||
|
if (ret.json) {
|
||||||
|
er.message = ret.json.reason;
|
||||||
|
er.name = `${ret.json.error ?? ""}:${ret.json.message ?? ""}`;
|
||||||
|
}
|
||||||
|
er.status = ret.status;
|
||||||
|
throw er;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
export const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }, disableRequestURI: boolean): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> => {
|
export const connectRemoteCouchDBWithSetting = (settings: RemoteDBSettings, isMobile: boolean) =>
|
||||||
|
connectRemoteCouchDB(
|
||||||
|
settings.couchDB_URI + (settings.couchDB_DBNAME == "" ? "" : "/" + settings.couchDB_DBNAME),
|
||||||
|
{
|
||||||
|
username: settings.couchDB_USER,
|
||||||
|
password: settings.couchDB_PASSWORD,
|
||||||
|
},
|
||||||
|
settings.disableRequestURI || isMobile,
|
||||||
|
settings.encrypt ? settings.passphrase : settings.encrypt
|
||||||
|
);
|
||||||
|
|
||||||
|
const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }, disableRequestURI: boolean, passphrase: string | boolean): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> => {
|
||||||
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
|
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
|
||||||
|
if (uri.toLowerCase() != uri) return "Remote URI and database name cound not contain capital letters.";
|
||||||
|
if (uri.indexOf(" ") !== -1) return "Remote URI and database name cound not contain spaces.";
|
||||||
let authHeader = "";
|
let authHeader = "";
|
||||||
if (auth.username && auth.password) {
|
if (auth.username && auth.password) {
|
||||||
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${auth.username}:${auth.password}`));
|
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${auth.username}:${auth.password}`));
|
||||||
@@ -28,7 +54,6 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
|
|||||||
adapter: "http",
|
adapter: "http",
|
||||||
auth,
|
auth,
|
||||||
fetch: async function (url: string | Request, opts: RequestInit) {
|
fetch: async function (url: string | Request, opts: RequestInit) {
|
||||||
let size_ok = true;
|
|
||||||
let size = "";
|
let size = "";
|
||||||
const localURL = url.toString().substring(uri.length);
|
const localURL = url.toString().substring(uri.length);
|
||||||
const method = opts.method ?? "GET";
|
const method = opts.method ?? "GET";
|
||||||
@@ -36,7 +61,6 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
|
|||||||
const opts_length = opts.body.toString().length;
|
const opts_length = opts.body.toString().length;
|
||||||
if (opts_length > 1024 * 1024 * 10) {
|
if (opts_length > 1024 * 1024 * 10) {
|
||||||
// over 10MB
|
// over 10MB
|
||||||
size_ok = false;
|
|
||||||
if (uri.contains(".cloudantnosqldb.")) {
|
if (uri.contains(".cloudantnosqldb.")) {
|
||||||
last_post_successed = false;
|
last_post_successed = false;
|
||||||
Logger("This request should fail on IBM Cloudant.", LOG_LEVEL.VERBOSE);
|
Logger("This request should fail on IBM Cloudant.", LOG_LEVEL.VERBOSE);
|
||||||
@@ -65,16 +89,13 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await requestUrl(requestParam);
|
const r = await fetchByAPI(requestParam);
|
||||||
if (method == "POST" || method == "PUT") {
|
if (method == "POST" || method == "PUT") {
|
||||||
last_post_successed = r.status - (r.status % 100) == 200;
|
last_post_successed = r.status - (r.status % 100) == 200;
|
||||||
} else {
|
} else {
|
||||||
last_post_successed = true;
|
last_post_successed = true;
|
||||||
}
|
}
|
||||||
if (r.status - (r.status % 100) !== 200) {
|
Logger(`HTTP:${method}${size} to:${localURL} -> ${r.status}`, LOG_LEVEL.DEBUG);
|
||||||
throw new Error(`Request Error:${r.status}`);
|
|
||||||
}
|
|
||||||
Logger(`HTTP:${method}${size} to:${localURL} -> ${r.status}`, LOG_LEVEL.VERBOSE);
|
|
||||||
|
|
||||||
return new Response(r.arrayBuffer, {
|
return new Response(r.arrayBuffer, {
|
||||||
headers: r.headers,
|
headers: r.headers,
|
||||||
@@ -83,7 +104,8 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
|
|||||||
});
|
});
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
|
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
|
||||||
if (!size_ok && (method == "POST" || method == "PUT")) {
|
// limit only in bulk_docs.
|
||||||
|
if (url.toString().indexOf("_bulk_docs") !== -1) {
|
||||||
last_post_successed = false;
|
last_post_successed = false;
|
||||||
}
|
}
|
||||||
Logger(ex);
|
Logger(ex);
|
||||||
@@ -100,11 +122,12 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
|
|||||||
} else {
|
} else {
|
||||||
last_post_successed = true;
|
last_post_successed = true;
|
||||||
}
|
}
|
||||||
Logger(`HTTP:${method}${size} to:${localURL} -> ${responce.status}`, LOG_LEVEL.VERBOSE);
|
Logger(`HTTP:${method}${size} to:${localURL} -> ${responce.status}`, LOG_LEVEL.DEBUG);
|
||||||
return responce;
|
return responce;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
|
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
|
||||||
if (!size_ok && (method == "POST" || method == "PUT")) {
|
// limit only in bulk_docs.
|
||||||
|
if (url.toString().indexOf("_bulk_docs") !== -1) {
|
||||||
last_post_successed = false;
|
last_post_successed = false;
|
||||||
}
|
}
|
||||||
Logger(ex);
|
Logger(ex);
|
||||||
@@ -113,7 +136,11 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
|
|||||||
// return await fetch(url, opts);
|
// return await fetch(url, opts);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const db: PouchDB.Database<EntryDoc> = new PouchDB<EntryDoc>(uri, conf);
|
const db: PouchDB.Database<EntryDoc> = new PouchDB<EntryDoc>(uri, conf);
|
||||||
|
if (passphrase && typeof passphrase === "string") {
|
||||||
|
enableEncryption(db, passphrase);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const info = await db.info();
|
const info = await db.info();
|
||||||
return { db: db, info: info };
|
return { db: db, info: info };
|
||||||
@@ -146,7 +173,7 @@ export const checkRemoteVersion = async (db: PouchDB.Database, migrate: (from: n
|
|||||||
if (version == barrier) return true;
|
if (version == barrier) return true;
|
||||||
return false;
|
return false;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
if ((ex.status && ex.status == 404) || (ex.message && ex.message == "Request Error:404")) {
|
if (ex.status && ex.status == 404) {
|
||||||
if (await bumpRemoteVersion(db)) {
|
if (await bumpRemoteVersion(db)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -169,3 +196,32 @@ export const bumpRemoteVersion = async (db: PouchDB.Database, barrier: number =
|
|||||||
await db.put(vi);
|
await db.put(vi);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkSyncInfo = async (db: PouchDB.Database): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const syncinfo = (await db.get(SYNCINFO_ID)) as SyncInfo;
|
||||||
|
console.log(syncinfo);
|
||||||
|
// if we could decrypt the doc, it must be ok.
|
||||||
|
return true;
|
||||||
|
} catch (ex) {
|
||||||
|
if (ex.status && ex.status == 404) {
|
||||||
|
const randomStrSrc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
const temp = [...Array(30)]
|
||||||
|
.map((e) => Math.floor(Math.random() * randomStrSrc.length))
|
||||||
|
.map((e) => randomStrSrc[e])
|
||||||
|
.join("");
|
||||||
|
const newSyncInfo: SyncInfo = {
|
||||||
|
_id: SYNCINFO_ID,
|
||||||
|
type: "syncinfo",
|
||||||
|
data: temp,
|
||||||
|
};
|
||||||
|
if (await db.put(newSyncInfo)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
console.dir(ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
86
styles.css
86
styles.css
@@ -1,50 +1,82 @@
|
|||||||
.added {
|
.added {
|
||||||
color: black;
|
color: var(--text-on-accent);
|
||||||
background-color: white;
|
background-color: var(--text-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.normal {
|
.normal {
|
||||||
color: lightgray;
|
color: var(--text-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.deleted {
|
.deleted {
|
||||||
color: white;
|
color: var(--text-on-accent);
|
||||||
background-color: black;
|
background-color: var(--text-muted);
|
||||||
/* text-decoration: line-through; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-scrollable {
|
.op-scrollable {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
/* min-height: 280px; */
|
/* min-height: 280px; */
|
||||||
max-height: 280px;
|
max-height: 280px;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-pre {
|
.op-pre {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-warn {
|
.op-warn {
|
||||||
border: 1px solid salmon;
|
border: 1px solid salmon;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.op-warn::before {
|
||||||
|
content: "Warning";
|
||||||
|
font-weight: bold;
|
||||||
|
color: salmon;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-warn-info {
|
||||||
|
border: 1px solid rgb(255, 209, 81);
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-warn-info::before {
|
||||||
|
content: "Notice";
|
||||||
|
font-weight: bold;
|
||||||
|
color: rgb(255, 209, 81);
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.syncstatusbar {
|
.syncstatusbar {
|
||||||
-webkit-filter: grayscale(100%);
|
-webkit-filter: grayscale(100%);
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tcenter {
|
.tcenter {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-plugins-wrap {
|
.sls-plugins-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-plugins-tbl {
|
.sls-plugins-tbl {
|
||||||
border: 1px solid var(--background-modifier-border);
|
border: 1px solid var(--background-modifier-border);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 80%;
|
max-height: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider th {
|
.divider th {
|
||||||
border-top: 1px solid var(--background-modifier-border);
|
border-top: 1px solid var(--background-modifier-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .sls-table-head{
|
/* .sls-table-head{
|
||||||
width:50%;
|
width:50%;
|
||||||
}
|
}
|
||||||
@@ -56,9 +88,11 @@
|
|||||||
.sls-btn-left {
|
.sls-btn-left {
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-btn-right {
|
.sls-btn-right {
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-hidden {
|
.sls-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -66,9 +100,12 @@
|
|||||||
:root {
|
:root {
|
||||||
--slsmessage: "";
|
--slsmessage: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-wrap::before,
|
.CodeMirror-wrap::before,
|
||||||
.cm-s-obsidian > .cm-editor::before {
|
.cm-s-obsidian>.cm-editor::before {
|
||||||
content: var(--slsmessage);
|
content: var(--slsmessage);
|
||||||
|
text-align: right;
|
||||||
|
white-space: pre-wrap;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
/* border:1px solid --background-modifier-border; */
|
/* border:1px solid --background-modifier-border; */
|
||||||
@@ -84,12 +121,15 @@
|
|||||||
.CodeMirror-wrap::before {
|
.CodeMirror-wrap::before {
|
||||||
right: 0px;
|
right: 0px;
|
||||||
}
|
}
|
||||||
.cm-s-obsidian > .cm-editor::before {
|
|
||||||
|
.cm-s-obsidian>.cm-editor::before {
|
||||||
right: 16px;
|
right: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-setting-tab {
|
.sls-setting-tab {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.sls-setting-menu-btn {
|
div.sls-setting-menu-btn {
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
background-color: var(--background-secondary-alt);
|
background-color: var(--background-secondary-alt);
|
||||||
@@ -110,8 +150,9 @@ div.sls-setting-menu-btn {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
/* width: 100%; */
|
/* width: 100%; */
|
||||||
}
|
}
|
||||||
.sls-setting-tab:hover ~ div.sls-setting-menu-btn,
|
|
||||||
.sls-setting-tab:checked ~ div.sls-setting-menu-btn {
|
.sls-setting-tab:hover~div.sls-setting-menu-btn,
|
||||||
|
.sls-setting-tab:checked~div.sls-setting-menu-btn {
|
||||||
background-color: var(--interactive-accent);
|
background-color: var(--interactive-accent);
|
||||||
color: var(--text-on-accent);
|
color: var(--text-on-accent);
|
||||||
}
|
}
|
||||||
@@ -122,14 +163,17 @@ div.sls-setting-menu-btn {
|
|||||||
/* flex-wrap: wrap; */
|
/* flex-wrap: wrap; */
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-setting-label {
|
.sls-setting-label {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-collapsed {
|
.setting-collapsed {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-plugins-tbl-buttons {
|
.sls-plugins-tbl-buttons {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@@ -138,13 +182,16 @@ div.sls-setting-menu-btn {
|
|||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-plugins-tbl-device-head {
|
.sls-plugins-tbl-device-head {
|
||||||
background-color: var(--background-secondary-alt);
|
background-color: var(--background-secondary-alt);
|
||||||
color: var(--text-accent);
|
color: var(--text-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-flex {
|
.op-flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-flex input {
|
.op-flex input {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@@ -164,11 +211,30 @@ div.sls-setting-menu-btn {
|
|||||||
color: var(--text-on-accent);
|
color: var(--text-on-accent);
|
||||||
background-color: var(--text-accent);
|
background-color: var(--text-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-normal {
|
.history-normal {
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-deleted {
|
.history-deleted {
|
||||||
color: var(--text-on-accent);
|
color: var(--text-on-accent);
|
||||||
background-color: var(--text-muted);
|
background-color: var(--text-muted);
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ob-btn-config-fix label {
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ob-btn-config-info {
|
||||||
|
border: 1px solid salmon;
|
||||||
|
padding: 2px;
|
||||||
|
margin: 1px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ob-btn-config-head {
|
||||||
|
padding: 2px;
|
||||||
|
margin: 1px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"types": ["svelte", "node"],
|
|
||||||
// "importsNotUsedAsValues": "error",
|
// "importsNotUsedAsValues": "error",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"alwaysStrict": true,
|
"alwaysStrict": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user