Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f96cc6b82 | ||
|
|
8c8f5d045f | ||
|
|
40cf8be890 | ||
|
|
6b03dbbe75 | ||
|
|
74425f75d2 | ||
|
|
ac7c622466 | ||
|
|
4b32365694 | ||
|
|
728edac283 | ||
|
|
ab9c0190bb | ||
|
|
5a7610d411 | ||
|
|
4691ae1463 | ||
|
|
0923ac3d85 | ||
|
|
ca100d6d9d | ||
|
|
bc373d4359 | ||
|
|
4038b683fe | ||
|
|
5e7b44d35a | ||
|
|
d04be6813b | ||
|
|
8e578e2100 | ||
|
|
55fcdfe18f | ||
|
|
66f2fea2f4 | ||
|
|
beb7bf6fb9 | ||
|
|
34791114e5 | ||
|
|
de5cdf507d | ||
|
|
83209f3923 | ||
|
|
b14ecdb205 | ||
|
|
21362adb5b | ||
|
|
f8c1474700 | ||
|
|
b35052a485 | ||
|
|
c367d35e09 | ||
|
|
2a5078cdbb | ||
|
|
8112a07210 | ||
|
|
c9daa1b47d | ||
|
|
73ac93e8c5 | ||
|
|
8d2b9eff37 |
1
.gitignore
vendored
@@ -12,3 +12,4 @@ main.js
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
.vscode
|
||||
|
||||
39
README.md
@@ -40,41 +40,9 @@ First, get your database ready. IBM Cloudant is preferred for testing. Or you ca
|
||||
|
||||
Note: More information about alternative hosting methods needed! Currently, [using fly.io](https://github.com/vrtmrz/obsidian-livesync/discussions/85) is being discussed.
|
||||
|
||||
### First device
|
||||
### Configure the plugin
|
||||
|
||||
1. Install the plugin on your device.
|
||||
2. Configure remote database infomation.
|
||||
1. Fill your server's information into the `Remote Database configuration` pane.
|
||||
2. Enabling `End to End Encryption` is recommended. After entering a passphrase, click `Apply`.
|
||||
3. Click `Test Database Connection` and make sure that the plugin says `Connected to (your-database-name)`.
|
||||
4. Click `Check database configuration` and make sure all tests have passed.
|
||||
3. Configure when should the synchronization happen in `Sync Settings` tab. (You can also leave them for later)
|
||||
1. If you want to synchronize in real-time, enable `LiveSync`.
|
||||
2. Or, set up the synchronization as you like. By default, none of the settings are enabled, meaning you would need to manually trigger the synchronization process.
|
||||
3. Additional configurations are also here. I recommend enabling `Use Trash for deleted files`, but you can also leave all configurations as-is.
|
||||
4. Configure miscellaneous features.
|
||||
1. Enabling `Show staus inside editor` shows status at the top-right corner of the editor while in editing mode. (Recommended)
|
||||
5. Go back to the editor. Wait for the initial scan to complete.
|
||||
6. When the status no longer changes and shows a ⏹️ for COMPLETED (No ⏳ and 🧩 icons), you are ready to synchronize with the server.
|
||||
7. Press the replicate icon on the Ribbon or run `Replicate now` from the command palette. This will send all your data to the server.
|
||||
8. Open command palette, run `Copy setup URI`, and set a passphrase. This will export your configuration to clipboard as a link for you to import into your other devices.
|
||||
|
||||
**IMPORTANT: BE CAREFUL NOT TO SHARE THIS LINK. THE URI CONTAINS ALL YOUR CREDENTIALS.** (even though nobody could read them without the passphrase)
|
||||
|
||||
### Subsequent Devices
|
||||
|
||||
Note: If we are going to synchronize with a non-empty vault, the modification dates and times of the files must match between them. Otherwise, extra transfers may occur or files may become corrupted.
|
||||
For simplicity, we strongly recommend that we sync to an empty vault.
|
||||
|
||||
1. Install the plug-in.
|
||||
2. Open the link that you have exported from the first device.
|
||||
3. The plug-in will ask you whether you are sure to apply the configurations. Answer `Yes`, then follow these instructions:
|
||||
1. Answer `Yes` to `Keep local DB?`.
|
||||
*Note: If you start with an existing vault, you have to answer `No` to this question and also answer `No` to `Rebuild the database?`.*
|
||||
2. Answer `Yes` to `Keep remote DB?`.
|
||||
3. Answer `Yes` to `Replicate once?`.
|
||||
Then, all your settings should be successfully imported from the first device.
|
||||
4. Your notes should get synchronized soon.
|
||||
See [Quick setup guide](doccs/../docs/quick_setup.md)
|
||||
|
||||
## Something looks corrupted...
|
||||
|
||||
@@ -114,8 +82,7 @@ If you have deleted or renamed files, please wait until ⏳ icon disappeared.
|
||||
- There are no 'exclude_folders' like configurations.
|
||||
- While synchronizing, files are compared by their modification time and the older ones will be overwritten by the newer ones. Then plugin checks for conflicts and if a merge is needed, a dialog will open.
|
||||
- Rarely, a file in the database could be corrupted. The plugin will not write to local storage when a file looks corrupted. If a local version of the file is on your device, the corruption could be fixed by editing the local file and synchronizing it. But if the file does not exist on any of your devices, then it can not be rescued. In this case you can delete these items from the settings dialog.
|
||||
- If your database looks corrupted, try "Drop History". Usually, It is the easiest way.
|
||||
- To stop the bootup sequence (eg. for fixing problems on databases), you can put a `redflag.md` file at the root of your vault.
|
||||
- To stop the boot up sequence (eg. for fixing problems on databases), you can put a `redflag.md` file at the root of your vault.
|
||||
- Q: Database is growing, how can I shrink it down?
|
||||
A: each of the docs is saved with their past 100 revisions for detecting and resolving conflicts. Picturing that one device has been offline for a while, and comes online again. The device has to compare its notes with the remotely saved ones. If there exists a historic revision in which the note used to be identical, it could be updated safely (like git fast-forward). Even if that is not in revision histories, we only have to check the differences after the revision that both devices commonly have. This is like 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)
|
||||
|
||||
@@ -113,7 +113,6 @@ Note: 正在征集更多搭建方法!目前在讨论的有 [使用 fly.io](htt
|
||||
- 没有类似“exclude_folders”的配置。
|
||||
- 同步时,文件按修改时间进行比较,较旧的将被较新的文件覆盖。然后插件检查冲突,如果需要合并,将打开一个对话框。
|
||||
- 数据库中的文件在罕见情况下可能会损坏。当接收到的文件看起来已损坏时,插件不会将其写入本地存储。如果您的设备上有文件的本地版本,则可以通过编辑本地文件并进行同步来覆盖损坏的版本。但是,如果您的任何设备上都不存在该文件,则无法挽救该文件。在这种情况下,您可以从设置对话框中删除这些损坏的文件。
|
||||
- 如果您的数据库看起来已损坏,请尝试 "Drop History"(“删除历史记录”)。通常,这是最简单的方法。
|
||||
- 要阻止插件的启动流程(例如,为了修复数据库问题),您可以在 vault 的根目录创建一个 "redflag.md" 文件。
|
||||
- 问:数据库在增长,我该如何缩小它?
|
||||
答:每个文档都保存了过去 100 次修订,用于检测和解决冲突。想象一台设备已经离线一段时间,然后再次上线。设备必须将其笔记与远程保存的笔记进行比较。如果存在曾经相同的历史修订,则可以安全地直接更新这个文件(和 git 的快进原理一样)。即使文件不在修订历史中,我们也只需检查两个设备上该文件的公有修订版本之后的差异。这就像 git 的冲突解决方法。所以,如果想从根本上解决数据库太大的问题,我们像构建一个扩大版的 git repo 一样去重新设计数据库。
|
||||
|
||||
22
README_ja.md
@@ -6,7 +6,7 @@
|
||||
**公式のSyncとは互換性はありません**
|
||||

|
||||
|
||||
**ほぼ動くようになってきましたが、Vaultのバックアップは確実に取得してください**
|
||||
**インストールする前に、Vaultのバックアップを確実に取得してください**
|
||||
|
||||
[英語版](./README.md)
|
||||
|
||||
@@ -30,28 +30,17 @@ NDAや類似の契約や義務、倫理を守る必要のある、研究者、
|
||||
これは、Vaultをクラウド管理下のフォルダに置くことも含みます。(例えば、iCloudの管理フォルダ内に入れたり)。
|
||||
- ⚠️このプラグインは、端末間でのノートの反映を目的として作成されました。バックアップ等が目的ではありません。そのため、バックアップは必ず別のソリューションで行うようにしてください。
|
||||
- ストレージの空き容量が枯渇した場合、データベースが破損することがあります。
|
||||
- 隠しファイルやObsidisanが認識できないファイルを編集した場合、そのファイルは削除されることがあります。
|
||||
|
||||
|
||||
# 補足
|
||||
|
||||
- レプリケーションなどでファイルがリモートデバイスから削除された場合、受信したデバイスでも、ファイルの削除が反映されます。
|
||||
- その際、Self-hosted LiveSyncは、フォルダが空になった際に、フォルダをデフォルトでは残しません。残す場合はオプションから設定してください。
|
||||
- LiveSyncはモバイルではバッテリーをかなり消費します。
|
||||
- モバイル端末からは、非httpsのエンドポイント、または独自CAが発行した証明書でホストされているhttpsのサーバーには接続できません。
|
||||
- 除外フォルダのような設定はありません。
|
||||
|
||||
|
||||
# このプラグインの使い方
|
||||
|
||||
1. Community Pluginsから、Self-holsted LiveSyncと検索しインストールするか、このリポジトリのReleasesから`main.js`, `manifest.json`, `style.css` をダウンロードしvaultの中の`.obsidian/plugins/obsidian-livesync`に入れて、Obsidianを再起動してください。
|
||||
2. サーバーを確保します。IBM Cloudantがお手軽かつ堅牢で便利です。完全にセルフホストする際にはお持ちのサーバーにCouchDBをインストールする必要があります。詳しくは下記を参照してください
|
||||
2. サーバーをセットアップします。IBM Cloudantがお手軽かつ堅牢で便利です。完全にセルフホストする際にはお持ちのサーバーにCouchDBをインストールする必要があります。詳しくは下記を参照してください
|
||||
1. [IBM Cloudantのセットアップ](docs/setup_cloudant_ja.md)
|
||||
2. [独自のCouchDBのセットアップ](docs/setup_own_server_ja.md)
|
||||
3. サーバー情報を入力します。初回のみ、Obsidianを再起動することをオススメします。
|
||||
設定内容の詳細は[このプラグインの設定](docs/settings_ja.md)を参照してください。
|
||||
|
||||
4. お好きな同期方法を選んで、利用を開始してください。
|
||||
備考: IBM Cloudantのアカウント登録が出来ないケースがあるようです。代替を探していて、今 [using fly.io](https://github.com/vrtmrz/obsidian-livesync/discussions/85)を検討しています。
|
||||
|
||||
1. [Quick setup](docs/quick_setup_ja.md)から、セットアップウィザード使ってセットアップしてください。
|
||||
|
||||
# テストサーバー
|
||||
|
||||
@@ -84,7 +73,6 @@ Self-hosted LiveSync用にWebClipperも作りました。Chrome Web Storeから
|
||||
# さらなる補足
|
||||
- ファイルは同期された後、タイムスタンプを比較して新しければいったん新しい方で上書きされます。その後、衝突が発生したかによって、マージが行われます。
|
||||
- まれにファイルが破損することがあります。破損したファイルに関してはディスクへの反映を試みないため、実際には使用しているデバイスには少し古いファイルが残っていることが多いです。そのファイルを再度更新してもらうと、データベースが更新されて問題なくなるケースがあります。ファイルがどの端末にも存在しない場合は、設定画面から、削除できます。
|
||||
- データベースが変。そういうときは、いったんデータベースをDrop Historyのapply and sendで再初期化してみてください。だいたい直ります。
|
||||
- データベースの復旧中に再起動した場合など、うまくローカルデータベースを修正できない際には、Vaultのトップに`redflag.md`というファイルを置いてください。起動時のシーケンスがスキップされます。
|
||||
- データベースが大きくなってきてるんだけど、小さくできる?→各ノートは、それぞれの古い100リビジョンとともに保存されています。例えば、しばらくオフラインだったあるデバイスが、久しぶりに同期したと想定してみてください。そのとき、そのデバイスは最新とは少し異なるリビジョンを持ってるはずです。その場合でも、リモートのリビジョン履歴にリモートのものが存在した場合、安全にマージできます。もしリビジョン履歴に存在しなかった場合、確認しなければいけない差分も、対象を存在して持っている共通のリビジョン以降のみに絞れます。ちょうどGitのような方法で、衝突を解決している形になるのです。そのため、肥大化したリポジトリの解消と同様に、本質的にデータベースを小さくしたい場合は、データベースの作り直しが必要です。
|
||||
- その他の技術的なお話は、[技術的な内容](docs/tech_info_ja.md)に書いてあります。
|
||||
|
||||
97
docs/quick_setup.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Quick setup
|
||||
The Setup wizard has been implemented since v0.15.0. This simplifies the initial set-up.
|
||||
|
||||
Note: The subsequent devices should be set up using the `Copy setup URI` and `Open setup URI`.
|
||||
|
||||
## How to open and use wizard
|
||||
Open from `🪄 Setup wizard` in the setting dialogue. If there is no configuration or no synchronisation settings have been activated, it should already be open.
|
||||
|
||||

|
||||
|
||||
### Discard the existing configuration and set up
|
||||
If you have made any settings, this button allows you to discard them all before setting up.
|
||||
|
||||
### Do not discard the existing configuration and set up
|
||||
Simply reconfigure. Be careful. In wizard mode, you cannot see all configuration items, even if they have been configured.
|
||||
|
||||
Pressing `Next` on any of these will put the configuration dialog into wizard mode.
|
||||
|
||||
### Wizard mode
|
||||
|
||||

|
||||
|
||||
We can set it up step by step.
|
||||
|
||||
## Remote Database configuration
|
||||
|
||||
### Remote database configuration
|
||||
|
||||
Enter the information in the database we have set up.
|
||||
|
||||

|
||||
|
||||
### End to End Encryption
|
||||
|
||||

|
||||
|
||||
If End to End encryption is enabled, the possibility of a third party who does not know the Passphrase being able to read the contents of the Remote database in the event that they are leaked is reduced. So we strongly recommend to enable it.
|
||||
Encryption is based on 256-bit AES-GCM.
|
||||
This setting can be disabled if you are inside a closed network and it is clear that you will not be accessed by third parties.
|
||||
|
||||
### Test database connectionとCheck database configuraion
|
||||
|
||||
Here we can check the status of the connection to the database and the database settings.
|
||||
|
||||

|
||||
|
||||
#### Test Database Connection
|
||||
Check whether we can connect to the database. If it fails, there are a number of reasons, but once you have done the `Check database configuration`, check if it fails there too.
|
||||
|
||||
#### Check database configuration
|
||||
|
||||
Check the database settings and fix any deficiencies on the spot.
|
||||
|
||||

|
||||
|
||||
This item may vary depending on the connection. In the above case, press all three Fix buttons.
|
||||
If the Fix buttons disappear and all become check marks, we are done.
|
||||
|
||||

|
||||
|
||||
### Next
|
||||
Go to the Local Database configuration.
|
||||
|
||||
### Discard exist database and proceed
|
||||
Discard the contents of the Remote database and go to the Local Database configuration.
|
||||
|
||||
## Local Database confiuration
|
||||
|
||||

|
||||
|
||||
Configure the local database. If we already have a Vaults with Self-hosted LiveSync installed and having same directory name as currently we are setting up, please specify a different suffix than the Vault you have already set up here.
|
||||
|
||||
## Miscellaneous
|
||||
Finally, finish the miscellaneous configurations and select a preset for synchronisation.
|
||||
|
||||

|
||||
|
||||
The `Show status inside editor` can be enabled to your liking. If enabled, the status is displayed in the top right-hand corner of the editor.
|
||||
|
||||

|
||||
|
||||
From Presets, select the synchronisation method we want to use and `Apply` to initialise and build the local and remote databases as required.
|
||||
If `All done!' is displayed, we are done. Automatically, `Copy setup URI` will open and we will be asked for a passphrase to encrypt the `Setup URI`.
|
||||
|
||||

|
||||
|
||||
Set the passphrase as you like.
|
||||
The Setup URI will be copied to the clipboard, which you can then transfer to the second and subsequent devices in some way.
|
||||
|
||||
# How to set up the second and subsequent units
|
||||
After installing Self-hosted LiveSync on the device, select `Open setup URI` from the command palette and enter the setup URI you transferred. Afterwards, enter your passphrase and a setup wizard will open.
|
||||
Answer the following.
|
||||
|
||||
- `Yes` to `Importing LiveSync's conf, OK?`
|
||||
- `Set it up as secondary or subsequent device` to `How would you like to set it up?`.
|
||||
|
||||
Then, The configuration will now take effect and replication will start. Your files will be synchronised soon!
|
||||
94
docs/quick_setup_ja.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Quick setup
|
||||
v0.15.0からSetup wizardが実装されました。これで、初回セットアップがシンプルになります。
|
||||
※なお、次のデバイスからは、`Copy setup URI`と`Open setup URI`を使ってセットアップしてください。
|
||||
|
||||
|
||||
## Wizardの使い方
|
||||
`🪄 Setup wizard` から開きます。もしセットアップされていなかったり、同期設定が何も有効になっていない場合はデフォルトで開いています。
|
||||
|
||||

|
||||
|
||||
### Discard the existing configuration and set up
|
||||
今設定されている内容をいったん全部消してから、ウィザードを始めます。
|
||||
|
||||
### Do not discard the existing configuration and set up
|
||||
今の設定を消さずにウィザードを開始します。
|
||||
たとえ設定されていたとしても、ウィザードモードではすべての設定を見ることができません。
|
||||
|
||||
いずれかのNextを押すと、設定画面がウィザードモードになります。
|
||||
|
||||
### Wizardモード
|
||||
|
||||

|
||||
|
||||
順番に設定を行っていきます。
|
||||
|
||||
## Remote Database configuration
|
||||
|
||||
### Remote databaseの設定
|
||||
セットアップしたデータベースの情報を入力していきます。
|
||||
|
||||

|
||||
|
||||
これらはデータベースをセットアップした際に決めた情報です。
|
||||
|
||||
### End to End暗号化の設定
|
||||
|
||||

|
||||
|
||||
End to End暗号化を有効にした場合、万が一Remote databaseの内容が流出してもPassphraseを知らない第三者にそれを読まれる可能性が低くなります。そのため、有効化を強く推奨します。
|
||||
暗号化は256bitのAES-GCMを採用しています。
|
||||
この設定は、あなたが閉じたネットワークの内側にいて、かつ第三者からアクセスされない事が明確な場合には無効にできます。
|
||||
|
||||
### Test database connectionとCheck database configuraion
|
||||
ここで、データベースへの接続状況と、データベース設定を確認します。
|
||||

|
||||
|
||||
#### Test Database Connection
|
||||
データベースに接続出来るか自体を確認します。失敗する場合はいくつか理由がありますが、一度Check database configurationを行ってそちらでも失敗するか確認してください。
|
||||
|
||||
#### Check database configuration
|
||||
データベースの設定を確認し、不備がある場合はその場で修正します。
|
||||
|
||||

|
||||
|
||||
この項目は接続先によって異なる場合があります。上記の場合、みっつのFixボタンを順にすべて押してください。
|
||||
Fixボタンがなくなり、すべてチェックマークになれば完了です。
|
||||
|
||||

|
||||
|
||||
### Next
|
||||
次へ進みます
|
||||
|
||||
### Discard exist database and proceed
|
||||
すでにRemote databaseがある場合、Remote databaseの内容を破棄してから次へ進みます
|
||||
|
||||
## Local Database confiuration
|
||||

|
||||
ローカルのデータベースを設定します。もし、すでにSelf-hosted LiveSyncをインストールしたVaultがあり、そのVaultと同じデータベース名を使用している場合は、ここですでに設定したVaultとは異なるsuffixを指定してください。
|
||||
|
||||
## Miscellaneous
|
||||
最後にその他の設定を行います。
|
||||
|
||||

|
||||
|
||||
`Show status inside editor`はお好みで有効化してください。有効にするとエディターの右上にステータスが表示されます。
|
||||
|
||||

|
||||
|
||||
Presetsから、使用する同期方法を選び`Apply`を行うと、必要に応じてローカル・リモートのデータベースを初期化・構築します。
|
||||
All done! と表示されれば完了です。自動的に、`Copy setup URI`が開き、`Setup URI`を暗号化するパスフレーズを聞かれます。
|
||||
|
||||

|
||||
|
||||
お好みのパスフレーズを設定してください。
|
||||
クリップボードにSetup URIが保存されますので、これを2台目以降のデバイスに何らかの方法で転送してください。
|
||||
|
||||
# 2台目以降の設定方法
|
||||
2台目の端末にSelf-hosted LiveSyncをインストールしたあと、コマンドパレットから`Open setup URI`を選択し、転送したsetup URIを入力します。その後、パスフレーズを入力するとセットアップ用のウィザードが開きます。
|
||||
下記のように答えてください。
|
||||
|
||||
- `Importing LiveSync's conf, OK?` に `Yes`
|
||||
- `How would you like to set it up?` に `Set it up as secondary or subsequent device`
|
||||
|
||||
これで設定が反映され、レプリケーションが開始されます。
|
||||
@@ -10,7 +10,7 @@ If you feel something, please feel free to inform me.
|
||||
| 🛰️ | [Remote Database Configurations](#remote-database-configurations) |
|
||||
| 📦 | [Local Database Configurations](#local-database-configurations) |
|
||||
| ⚙️ | [General Settings](#general-settings) |
|
||||
| 🔁 | [Sync Settings](#sync-settings) |
|
||||
| 🔁 | [Sync Settings](#sync-settings) |
|
||||
| 🔧 | [Miscellaneous](#miscellaneous) |
|
||||
| 🧰 | [Hatch](#miscellaneous) |
|
||||
| 🔌 | [Plugin and its settings](#plugin-and-its-settings) |
|
||||
@@ -74,14 +74,6 @@ If you have something troubled with other devices, you can protect the vault and
|
||||
Delay database update until raise replication, open another file, window visibility changed, or file events except for file modification.
|
||||
This option can not be used with LiveSync at the same time.
|
||||
|
||||
### Garbage check
|
||||
This plugin saves the file by splitting it into chunks to speed replication up and keep low bandwidth.
|
||||
|
||||
They share the chunk if you use the same paragraph in some notes. And if you change the file, only the paragraph you changed is transferred with metadata of the file. And I know that editing notes are not so straight. Sometimes paragraphs will be back into an old phrase. In these cases, we do not have to transfer the chunk again if the chunk will not be deleted. So all chunks will be reused.
|
||||
|
||||
As the side effect of this, you can see history the file.
|
||||
|
||||
The check will show the number of chunks used or retained. If there are so many retained chunks, you can rebuild the database.
|
||||
|
||||
### Fetch rebuilt DB.
|
||||
If one device rebuilds or locks the remote database, every other device will be locked out from the remote database until it fetches rebuilt DB.
|
||||
@@ -242,31 +234,6 @@ But, there's no problem even if you leave it as it is.
|
||||
### Verify and repair all files
|
||||
read all files in the vault, and update them into the database if there's diff or could not read from the database.
|
||||
|
||||
### Sanity check
|
||||
Make sure that all the files on the local database have all chunks.
|
||||
|
||||
### Drop history
|
||||
Drop all histories on the local database and the remote database, and initialize When synchronization time has been prolonged to the new device or new vault, or database size became to be much larger. Try this.
|
||||
|
||||
Note: When CouchDB deletes entries, to merge confliction, there left old entries as deleted data before compaction. After compaction has been run, deleted data are become "tombstone". "tombstone" uses less disk, But still use some.
|
||||
|
||||
It's the specification, to shrink database size from the root, re-initialization is required, even it's explicit or implicit.
|
||||
|
||||
Same as a setting passphrase, database locking is also performed.
|
||||
|
||||
|
||||
- Drop and send (Same as "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.
|
||||
|
||||
- Drop and receive (Same as "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.
|
||||
|
||||
|
||||
### Suspend file watching
|
||||
If enable this option, Self-hosted LiveSync dismisses every file change or deletes the event.
|
||||
|
||||
@@ -274,15 +241,6 @@ From here, these commands are used inside applying encryption passphrases or dro
|
||||
|
||||
Usually, doesn't use it so much. But sometimes it could be handy.
|
||||
|
||||
### Reset remote database
|
||||
Discard the data stored in the remote database.
|
||||
|
||||
### Reset local database
|
||||
Discard the data stored in the local database.
|
||||
|
||||
### Initialize local database again
|
||||
Discard the data stored in the local database and initialize and create the database from the files on storage.
|
||||
|
||||
## Plugins and settings (beta)
|
||||
|
||||
### Enable plugin synchronization
|
||||
|
||||
@@ -70,22 +70,6 @@ End to End 暗号化を行うに当たって、異なるパスフレーズで暗
|
||||
- ファイルの修正以外のファイル関連イベント
|
||||
このオプションはLiveSyncと同時には使用できません。
|
||||
|
||||
### Auto Garbage Collection delay
|
||||
Self-hosted LiveSyncはノートの変更時、ノートをmarkdownの構造を鑑みてチャンクに分割し、ファイルの情報と更新があったチャンクのみ保存していきます。この際、古いチャンクの削除は行いません。
|
||||
そのため、使わなくなったチャンクをどこかのタイミングで消去する必要があります。
|
||||
ただし、このチャンクはチャンクの内容から作成されるため、同一の内容からは同一のチャンクが作成され、同じノートだけではなく、すべてのノートから共有されます。これによってデータベースの使用容量とデバイス‐サーバー間での転送量を削減しています。
|
||||
執筆を繰り返す上で、元の文書に戻したりすることもあるため、一概に「すぐに不要になる」とは言い切れません。そこで、プラグインはObsidianを開いたまま操作しなくなってからこの設定値秒後、まとめて使用していないチャンクを削除します。
|
||||
この処理をGarbage Collectionと呼んでいます。
|
||||
この作業はすべてのファイル変更の反映を停止して一気に行う必要があります。そのため、一時的にObsidianの動作がかなり重くなります。
|
||||
|
||||
|
||||
Obsidianでのファイル操作が終わってから指定秒数が経過した際に実行されます。
|
||||
デフォルト値は300秒です。
|
||||
※ごく初期は30秒でした。初期から使用されている方は、是非300秒ぐらいまで伸ばしてください。ストレスが違います。
|
||||
|
||||
### Manual Garbage Collect
|
||||
上記のGarbage Collectionを手動で行います。
|
||||
|
||||
### minimum chunk size と LongLine threshold
|
||||
チャンクの分割についての設定です。
|
||||
Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk size文字確保した上で、できるだけ効率的に同期できるよう、ノートを分割してチャンクを作成します。
|
||||
@@ -223,16 +207,6 @@ Self-hosted LiveSyncはPouchDBを使用し、リモートと[このプロトコ
|
||||
### Verify and repair all files
|
||||
Vault内のファイルを全て読み込み直し、もし差分があったり、データベースから正常に読み込めなかったものに関して、データベースに反映します。
|
||||
|
||||
### Sanity check
|
||||
ローカルデータベースに保存されている全てのファイルが正しくチャンクを持っていることを確認します。
|
||||
|
||||
### Drop history
|
||||
データベースに記録されている履歴を削除し、データベースを初期化します。
|
||||
新しい端末や新しいVaultへの同期にやたらと時間がかかったり、データベースサイズが肥大化したりしてきた際に使用してください。
|
||||
備考:CouchDBは、データを削除する際、衝突の解決のために、削除した痕跡を保存します。そのため、Garbage Collectしていたとしても、データは必ず増え続けます。
|
||||
パスフレーズ設定と同様に、完全に同期されているのであれば、データを失う可能性は低いです。
|
||||
また、同様にデータベースのロック等の処理も行われます。
|
||||
|
||||
- Drop and send
|
||||
デバイスとリモートのデータベースを破棄し、ロックしてからデバイスのファイルでデータベースを構築後、リモートに上書きします。
|
||||
- Drop and receive
|
||||
@@ -246,20 +220,6 @@ Vault内のファイルを全て読み込み直し、もし差分があったり
|
||||
### Suspend file watching
|
||||
ファイルの更新の監視を止めます。
|
||||
|
||||
|
||||
これ以降の操作は、暗号化設定のApplyや、Drop Historyで行われる処理を手動で行うためのオプションです。
|
||||
|
||||
あまり使用することはありませんがいざというときに使用します。
|
||||
|
||||
### reset remote database
|
||||
リモートのデータベースを破棄します。
|
||||
|
||||
### reset local database
|
||||
ローカルのデータベースを破棄します。
|
||||
|
||||
### initialize local database again
|
||||
デバイスのデータベースを破棄し、実ファイルから再度データベースを構築します。
|
||||
|
||||
### Corrupted data
|
||||

|
||||
|
||||
|
||||
@@ -32,16 +32,18 @@ max_age = 3600
|
||||
|
||||
Make `local.ini` and run with docker run like this, you can launch the CouchDB.
|
||||
```
|
||||
$ docker run --rm -it -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v .local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb
|
||||
$ docker run --rm -it -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /path/to/local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb
|
||||
```
|
||||
*Remember to replace the path with the path to your local.ini*
|
||||
Note: At this time, the file owner of local.ini became 5984:5984. It's the limitation docker image. please change the owner before editing local.ini again.
|
||||
|
||||
If you could confirm that Self-hosted LiveSync can sync with the server, launch docker image as background as you like.
|
||||
|
||||
example)
|
||||
Example to run docker in detached mode:
|
||||
```
|
||||
$ docker run -d --restart always -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v .local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb
|
||||
$ docker run -d --restart always -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /path/to/local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb
|
||||
```
|
||||
*Remember to replace the path with the path to your local.ini*
|
||||
|
||||
## Access from mobile device
|
||||
If you want to access Self-hosted LiveSync from mobile devices, you need a valid SSL certificate.
|
||||
|
||||
@@ -29,7 +29,7 @@ esbuild
|
||||
external: ["obsidian", "electron", ...builtins],
|
||||
format: "cjs",
|
||||
watch: !prod,
|
||||
target: "es2015",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
|
||||
BIN
images/quick_setup_1.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
images/quick_setup_10.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
images/quick_setup_2.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
images/quick_setup_3.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
images/quick_setup_4.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
images/quick_setup_5.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
images/quick_setup_6.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
images/quick_setup_7.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
images/quick_setup_8.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
images/quick_setup_9_1.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
images/quick_setup_9_2.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.14.2",
|
||||
"version": "0.15.7",
|
||||
"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.",
|
||||
"author": "vorotamoroz",
|
||||
|
||||
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.14.2",
|
||||
"version": "0.15.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.14.2",
|
||||
"version": "0.15.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"diff-match-patch": "^1.0.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.14.2",
|
||||
"version": "0.15.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.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
|
||||
1
pouchdb-browser-webpack/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
node_modules
|
||||
@@ -1,4 +0,0 @@
|
||||
# PouchDB-browser
|
||||
|
||||
Just webpacked.
|
||||
(Rollup couldn't pack pouchdb-browser into browser bundle)
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "pouchdb-browser-webpack",
|
||||
"version": "1.0.0",
|
||||
"description": "pouchdb-browser webpack",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "webpack --mode=production --node-env=production",
|
||||
"build:dev": "webpack --mode=development",
|
||||
"build:prod": "webpack --mode=production --node-env=production",
|
||||
"watch": "webpack --watch"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"pouchdb-browser": "^7.3.0",
|
||||
"transform-pouch": "^2.0.0",
|
||||
"pouchdb-find": "^7.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack": "^5.58.1",
|
||||
"webpack-cli": "^4.9.0"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
// This module just webpacks pouchdb-browser
|
||||
// import * as PouchDB_src from "pouchdb-browser";
|
||||
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 };
|
||||
@@ -1,30 +0,0 @@
|
||||
// Generated using webpack-cli https://github.com/webpack/webpack-cli
|
||||
|
||||
const path = require("path");
|
||||
|
||||
const isProduction = process.env.NODE_ENV == "production";
|
||||
|
||||
const config = {
|
||||
entry: "./src/index.js",
|
||||
output: {
|
||||
filename: "pouchdb-browser.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
library: {
|
||||
type: "module",
|
||||
},
|
||||
},
|
||||
experiments: {
|
||||
outputModule: true,
|
||||
},
|
||||
plugins: [],
|
||||
module: {},
|
||||
};
|
||||
|
||||
module.exports = () => {
|
||||
if (isProduction) {
|
||||
config.mode = "production";
|
||||
} else {
|
||||
config.mode = "development";
|
||||
}
|
||||
return config;
|
||||
};
|
||||
@@ -165,7 +165,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
const leaf = app.workspace.getLeaf(false);
|
||||
await leaf.openFile(targetFile);
|
||||
} else {
|
||||
Logger("The file cound not view on the editor", LOG_LEVEL.NOTICE)
|
||||
Logger("The file could not view on the editor", LOG_LEVEL.NOTICE)
|
||||
}
|
||||
}
|
||||
buttons.createEl("button", { text: "Back to this revision" }, (e) => {
|
||||
@@ -173,7 +173,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
e.addEventListener("click", async () => {
|
||||
const pathToWrite = this.file.startsWith("i:") ? this.file.substring("i:".length) : this.file;
|
||||
if (!isValidPath(pathToWrite)) {
|
||||
Logger("Path is not vaild to write content.", LOG_LEVEL.INFO);
|
||||
Logger("Path is not valid to write content.", LOG_LEVEL.INFO);
|
||||
}
|
||||
if (this.currentDoc?.datatype == "plain") {
|
||||
await this.app.vault.adapter.write(pathToWrite, this.currentDoc.data);
|
||||
|
||||
1478
src/LocalPouchDB.ts
@@ -1,9 +1,9 @@
|
||||
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl, TextAreaComponent, MarkdownRenderer } from "obsidian";
|
||||
import { EntryDoc, LOG_LEVEL, RemoteDBSettings } from "./lib/src/types";
|
||||
import { DEFAULT_SETTINGS, LOG_LEVEL, RemoteDBSettings } from "./lib/src/types";
|
||||
import { path2id, id2path } from "./utils";
|
||||
import { delay, runWithLock, versionNumberString2Number } from "./lib/src/utils";
|
||||
import { delay, versionNumberString2Number } from "./lib/src/utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { checkSyncInfo, connectRemoteCouchDBWithSetting } from "./utils_couchdb";
|
||||
import { checkSyncInfo } from "./lib/src/utils_couchdb.js";
|
||||
import { testCrypt } from "./lib/src/e2ee_v2";
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
|
||||
@@ -15,7 +15,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
async testConnection(): Promise<void> {
|
||||
const db = await connectRemoteCouchDBWithSetting(this.plugin.settings, this.plugin.localDatabase.isMobile);
|
||||
const db = await this.plugin.localDatabase.connectRemoteCouchDBWithSetting(this.plugin.settings, this.plugin.localDatabase.isMobile);
|
||||
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);
|
||||
return;
|
||||
@@ -28,6 +28,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl("h2", { text: "Settings for Self-hosted LiveSync." });
|
||||
containerEl.addClass("sls-setting");
|
||||
containerEl.removeClass("isWizard");
|
||||
|
||||
|
||||
const w = containerEl.createDiv("");
|
||||
const screenElements: { [key: string]: HTMLElement[] } = {};
|
||||
@@ -39,17 +42,19 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
};
|
||||
w.addClass("sls-setting-menu");
|
||||
w.innerHTML = `
|
||||
<label class='sls-setting-label selected'><input type='radio' name='disp' value='100' class='sls-setting-tab' checked><div class='sls-setting-menu-btn'>💬</div></label>
|
||||
<label class='sls-setting-label'><input type='radio' name='disp' value='0' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🛰️</div></label>
|
||||
<label class='sls-setting-label'><input type='radio' name='disp' value='10' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>📦</div></label>
|
||||
<label class='sls-setting-label'><input type='radio' name='disp' value='20' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>⚙️</div></label>
|
||||
<label class='sls-setting-label'><input type='radio' name='disp' value='30' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🔁</div></label>
|
||||
<label class='sls-setting-label'><input type='radio' name='disp' value='40' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🔧</div></label>
|
||||
<label class='sls-setting-label'><input type='radio' name='disp' value='50' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🧰</div></label>
|
||||
<label class='sls-setting-label'><input type='radio' name='disp' value='60' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🔌</div></label>
|
||||
<label class='sls-setting-label'><input type='radio' name='disp' value='70' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🚑</div></label>
|
||||
<label class='sls-setting-label c-100 wizardHidden'><input type='radio' name='disp' value='100' class='sls-setting-tab'><div class='sls-setting-menu-btn'>💬</div></label>
|
||||
<label class='sls-setting-label c-110'><input type='radio' name='disp' value='110' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🪄</div></label>
|
||||
<label class='sls-setting-label c-0'><input type='radio' name='disp' value='0' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🛰️</div></label>
|
||||
|
||||
<label class='sls-setting-label c-10'><input type='radio' name='disp' value='10' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>📦</div></label>
|
||||
<label class='sls-setting-label c-20 wizardHidden'><input type='radio' name='disp' value='20' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>⚙️</div></label>
|
||||
<label class='sls-setting-label c-30 wizardHidden'><input type='radio' name='disp' value='30' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🔁</div></label>
|
||||
<label class='sls-setting-label c-40'><input type='radio' name='disp' value='40' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🔧</div></label>
|
||||
<label class='sls-setting-label c-50 wizardHidden'><input type='radio' name='disp' value='50' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🧰</div></label>
|
||||
<label class='sls-setting-label c-60 wizardHidden'><input type='radio' name='disp' value='60' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🔌</div></label>
|
||||
<label class='sls-setting-label c-70 wizardHidden'><input type='radio' name='disp' value='70' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🚑</div></label>
|
||||
`;
|
||||
const menutabs = w.querySelectorAll(".sls-setting-label");
|
||||
const menuTabs = w.querySelectorAll(".sls-setting-label");
|
||||
const changeDisplay = (screen: string) => {
|
||||
for (const k in screenElements) {
|
||||
if (k == screen) {
|
||||
@@ -58,12 +63,22 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
screenElements[k].forEach((element) => element.addClass("setting-collapsed"));
|
||||
}
|
||||
}
|
||||
w.querySelectorAll(`.sls-setting-label`).forEach((element) => {
|
||||
element.removeClass("selected");
|
||||
(element.querySelector("input[type=radio]") as HTMLInputElement).checked = false;
|
||||
});
|
||||
console.log(`.sls-setting-label.c-${screen}`)
|
||||
w.querySelectorAll(`.sls-setting-label.c-${screen}`).forEach((element) => {
|
||||
console.log(element)
|
||||
element.addClass("selected");
|
||||
(element.querySelector("input[type=radio]") as HTMLInputElement).checked = true;
|
||||
});
|
||||
};
|
||||
menutabs.forEach((element) => {
|
||||
menuTabs.forEach((element) => {
|
||||
const e = element.querySelector(".sls-setting-tab");
|
||||
if (!e) return;
|
||||
e.addEventListener("change", (event) => {
|
||||
menutabs.forEach((element) => element.removeClass("selected"));
|
||||
menuTabs.forEach((element) => element.removeClass("selected"));
|
||||
changeDisplay((event.currentTarget as HTMLInputElement).value);
|
||||
element.addClass("selected");
|
||||
});
|
||||
@@ -97,12 +112,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
|
||||
addScreenElement("100", containerInformationEl);
|
||||
const containerRemoteDatabaseEl = containerEl.createDiv();
|
||||
containerRemoteDatabaseEl.createEl("h3", { text: "Remote Database configuration" });
|
||||
const syncWarn = containerRemoteDatabaseEl.createEl("div", { text: `These settings are kept locked while any synchronization options are enabled. Disable these options in the "Sync Settings" tab to unlock.` });
|
||||
syncWarn.addClass("op-warn-info");
|
||||
syncWarn.addClass("sls-hidden");
|
||||
|
||||
const isAnySyncEnabled = (): boolean => {
|
||||
if (this.plugin.settings.liveSync) return true;
|
||||
if (this.plugin.settings.periodicReplication) return true;
|
||||
@@ -113,6 +122,76 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
if (this.plugin.localDatabase.syncStatus == "PAUSED") return true;
|
||||
return false;
|
||||
};
|
||||
let inWizard = false;
|
||||
|
||||
const setupWizardEl = containerEl.createDiv();
|
||||
setupWizardEl.createEl("h3", { text: "Setup wizard" });
|
||||
new Setting(setupWizardEl)
|
||||
.setName("Discard the existing configuration and set up")
|
||||
.addButton((text) => {
|
||||
text.setButtonText("Next").onClick(async () => {
|
||||
if (JSON.stringify(this.plugin.settings) != JSON.stringify(DEFAULT_SETTINGS)) {
|
||||
this.plugin.localDatabase.closeReplication();
|
||||
this.plugin.settings = { ...DEFAULT_SETTINGS };
|
||||
this.plugin.saveSettings();
|
||||
|
||||
Logger("Configuration has been flushed, please open it again", LOG_LEVEL.NOTICE)
|
||||
// @ts-ignore
|
||||
this.plugin.app.setting.close()
|
||||
} else {
|
||||
containerEl.addClass("isWizard");
|
||||
applyDisplayEnabled();
|
||||
inWizard = true;
|
||||
changeDisplay("0")
|
||||
}
|
||||
})
|
||||
})
|
||||
new Setting(setupWizardEl)
|
||||
.setName("Do not discard the existing configuration and set up again")
|
||||
.addButton((text) => {
|
||||
text.setButtonText("Next").onClick(async () => {
|
||||
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.localDatabase.closeReplication();
|
||||
await this.plugin.saveSettings();
|
||||
containerEl.addClass("isWizard");
|
||||
applyDisplayEnabled();
|
||||
inWizard = true;
|
||||
changeDisplay("0")
|
||||
|
||||
})
|
||||
})
|
||||
const infoWarnForSubsequent = setupWizardEl.createEl("div", { text: `To set up second or subsequent device, please use 'Copy setup URI' and 'Open setup URI'` });
|
||||
infoWarnForSubsequent.addClass("op-warn-info");
|
||||
new Setting(setupWizardEl)
|
||||
.setName("Copy setup URI")
|
||||
.addButton((text) => {
|
||||
text.setButtonText("Copy setup URI").onClick(() => {
|
||||
// @ts-ignore
|
||||
this.plugin.app.commands.executeCommandById("obsidian-livesync:livesync-copysetupuri")
|
||||
|
||||
})
|
||||
})
|
||||
.addButton((text) => {
|
||||
text.setButtonText("Open setup URI").onClick(() => {
|
||||
// @ts-ignore
|
||||
this.plugin.app.commands.executeCommandById("obsidian-livesync:livesync-opensetupuri")
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
addScreenElement("110", setupWizardEl);
|
||||
|
||||
const containerRemoteDatabaseEl = containerEl.createDiv();
|
||||
containerRemoteDatabaseEl.createEl("h3", { text: "Remote Database configuration" });
|
||||
const syncWarn = containerRemoteDatabaseEl.createEl("div", { text: `These settings are kept locked while any synchronization options are enabled. Disable these options in the "Sync Settings" tab to unlock.` });
|
||||
syncWarn.addClass("op-warn-info");
|
||||
syncWarn.addClass("sls-hidden");
|
||||
|
||||
|
||||
const applyDisplayEnabled = () => {
|
||||
if (isAnySyncEnabled()) {
|
||||
dbSettings.forEach((e) => {
|
||||
@@ -200,24 +279,37 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
.setDesc("Encrypt contents on the remote database. If you use the plugin's synchronization 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();
|
||||
if (inWizard) {
|
||||
this.plugin.settings.encrypt = value;
|
||||
passphrase.setDisabled(!value);
|
||||
await this.plugin.saveSettings();
|
||||
} else {
|
||||
this.plugin.settings.workingEncrypt = value;
|
||||
passphrase.setDisabled(!value);
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
})
|
||||
);
|
||||
const phasspharase = new Setting(containerRemoteDatabaseEl)
|
||||
|
||||
const passphrase = new Setting(containerRemoteDatabaseEl)
|
||||
.setName("Passphrase")
|
||||
.setDesc("Encrypting passphrase. If you change the passphrase of a existing database, overwriting the remote database is strongly recommended.")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.workingPassphrase)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.workingPassphrase = value;
|
||||
await this.plugin.saveSettings();
|
||||
if (inWizard) {
|
||||
this.plugin.settings.passphrase = value;
|
||||
await this.plugin.saveSettings();
|
||||
} else {
|
||||
this.plugin.settings.workingPassphrase = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
});
|
||||
text.inputEl.setAttribute("type", "password");
|
||||
});
|
||||
phasspharase.setDisabled(!this.plugin.settings.workingEncrypt);
|
||||
passphrase.setDisabled(!this.plugin.settings.workingEncrypt);
|
||||
|
||||
const checkWorkingPassphrase = async (): Promise<boolean> => {
|
||||
const settingForCheck: RemoteDBSettings = {
|
||||
...this.plugin.settings,
|
||||
@@ -225,7 +317,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
passphrase: this.plugin.settings.workingPassphrase,
|
||||
};
|
||||
console.dir(settingForCheck);
|
||||
const db = await connectRemoteCouchDBWithSetting(settingForCheck, this.plugin.localDatabase.isMobile);
|
||||
const db = await this.plugin.localDatabase.connectRemoteCouchDBWithSetting(settingForCheck, this.plugin.localDatabase.isMobile);
|
||||
if (typeof db === "string") {
|
||||
Logger("Could not connect to the database.", LOG_LEVEL.NOTICE);
|
||||
return false;
|
||||
@@ -263,7 +355,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
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();
|
||||
@@ -278,6 +369,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
new Setting(containerRemoteDatabaseEl)
|
||||
.setName("Apply")
|
||||
.setDesc("Apply encryption settings")
|
||||
.setClass("wizardHidden")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Apply")
|
||||
@@ -285,9 +377,20 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
.setDisabled(false)
|
||||
.setClass("sls-btn-right")
|
||||
.onClick(async () => {
|
||||
await applyEncryption(false);
|
||||
await applyEncryption(true);
|
||||
})
|
||||
);
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Apply w/o rebuilding")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.setClass("sls-btn-right")
|
||||
.onClick(async () => {
|
||||
await applyEncryption(false);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
const rebuildDB = async (method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice") => {
|
||||
this.plugin.settings.liveSync = false;
|
||||
@@ -324,6 +427,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
new Setting(containerRemoteDatabaseEl)
|
||||
.setName("Overwrite remote database")
|
||||
.setDesc("Overwrite remote database with local DB and passphrase.")
|
||||
.setClass("wizardHidden")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Send")
|
||||
@@ -338,6 +442,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
new Setting(containerRemoteDatabaseEl)
|
||||
.setName("Rebuild everything")
|
||||
.setDesc("Rebuild local and remote database with local files.")
|
||||
.setClass("wizardHidden")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Rebuild")
|
||||
@@ -417,7 +522,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
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);
|
||||
Logger(`${title} successfully updated`, LOG_LEVEL.NOTICE);
|
||||
checkResultDiv.removeChild(x);
|
||||
checkConfig();
|
||||
} else {
|
||||
@@ -469,6 +574,22 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
} else {
|
||||
addResult("✔ httpd.enable_cors is ok.");
|
||||
}
|
||||
// If the server is not cloudant, configure request size
|
||||
if (!this.plugin.settings.couchDB_URI.contains(".cloudantnosqldb.")) {
|
||||
// REQUEST SIZE
|
||||
if (Number(responseConfig?.chttpd?.max_http_request_size ?? 0) < 4294967296) {
|
||||
addResult("❗ chttpd.max_http_request_size is low)");
|
||||
addConfigFixButton("Set chttpd.max_http_request_size", "chttpd/max_http_request_size", "4294967296");
|
||||
} else {
|
||||
addResult("✔ chttpd.max_http_request_size is ok.");
|
||||
}
|
||||
if (Number(responseConfig?.couchdb?.max_document_size ?? 0) < 50000000) {
|
||||
addResult("❗ couchdb.max_document_size is low)");
|
||||
addConfigFixButton("Set couchdb.max_document_size", "couchdb/max_document_size", "50000000");
|
||||
} else {
|
||||
addResult("✔ couchdb.max_document_size is ok.");
|
||||
}
|
||||
}
|
||||
// CORS check
|
||||
// checking connectivity for mobile
|
||||
if (responseConfig?.cors?.credentials != "true") {
|
||||
@@ -515,10 +636,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
addResult("✔ CORS origin OK");
|
||||
}
|
||||
}
|
||||
addResult("--Done--", ["ob-btn-config-haed"]);
|
||||
addResult("--Done--", ["ob-btn-config-head"]);
|
||||
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(`Checking configuration failed`);
|
||||
Logger(ex);
|
||||
}
|
||||
};
|
||||
@@ -532,6 +653,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
new Setting(containerRemoteDatabaseEl)
|
||||
.setName("Lock remote database")
|
||||
.setDesc("Lock remote database to prevent synchronization with other devices.")
|
||||
.setClass("wizardHidden")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Lock")
|
||||
@@ -541,6 +663,50 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
await this.plugin.markRemoteLocked();
|
||||
})
|
||||
);
|
||||
let rebuildRemote = false;
|
||||
|
||||
new Setting(containerRemoteDatabaseEl)
|
||||
.setName("")
|
||||
.setClass("wizardOnly")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Next")
|
||||
.setClass("mod-cta")
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
if (!this.plugin.settings.encrypt) {
|
||||
this.plugin.settings.passphrase = "";
|
||||
}
|
||||
if (this.plugin.settings.couchDB_URI.contains(".cloudantnosqldb.")) {
|
||||
this.plugin.settings.customChunkSize = 0;
|
||||
} else {
|
||||
this.plugin.settings.customChunkSize = 100;
|
||||
}
|
||||
rebuildRemote = false;
|
||||
changeDisplay("10")
|
||||
})
|
||||
);
|
||||
new Setting(containerRemoteDatabaseEl)
|
||||
.setName("")
|
||||
.setClass("wizardOnly")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Discard exist database and proceed")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
if (!this.plugin.settings.encrypt) {
|
||||
this.plugin.settings.passphrase = "";
|
||||
}
|
||||
if (this.plugin.settings.couchDB_URI.contains(".cloudantnosqldb.")) {
|
||||
this.plugin.settings.customChunkSize = 0;
|
||||
} else {
|
||||
this.plugin.settings.customChunkSize = 100;
|
||||
}
|
||||
rebuildRemote = true;
|
||||
changeDisplay("10")
|
||||
})
|
||||
);
|
||||
addScreenElement("0", containerRemoteDatabaseEl);
|
||||
const containerLocalDatabaseEl = containerEl.createDiv();
|
||||
containerLocalDatabaseEl.createEl("h3", { text: "Local Database configuration" });
|
||||
@@ -548,6 +714,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
new Setting(containerLocalDatabaseEl)
|
||||
.setName("Batch database update")
|
||||
.setDesc("Delay all changes, save once before replication or opening another file.")
|
||||
.setClass("wizardHidden")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.batchSave).onChange(async (value) => {
|
||||
if (value && this.plugin.settings.liveSync) {
|
||||
@@ -560,18 +727,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerLocalDatabaseEl).setName("Garbage check").addButton((button) =>
|
||||
button
|
||||
.setButtonText("Check now")
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await this.plugin.garbageCheck();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerLocalDatabaseEl)
|
||||
.setName("Fetch rebuilt DB")
|
||||
.setDesc("Restore or reconstruct local database from remote database.")
|
||||
.setClass("wizardHidden")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Fetch")
|
||||
@@ -583,47 +742,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
})
|
||||
)
|
||||
|
||||
containerLocalDatabaseEl.createEl("div", {
|
||||
text: sanitizeHTMLToDom(`Advanced settings<br>
|
||||
Configuration of how LiveSync makes chunks from the file.`),
|
||||
});
|
||||
new Setting(containerLocalDatabaseEl)
|
||||
.setName("Minimum chunk size")
|
||||
.setDesc("(letters), minimum chunk size.")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.minimumChunkSize + "")
|
||||
.onChange(async (value) => {
|
||||
let v = Number(value);
|
||||
if (isNaN(v) || v < 10 || v > 1000) {
|
||||
v = 10;
|
||||
}
|
||||
this.plugin.settings.minimumChunkSize = v;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.setAttribute("type", "number");
|
||||
});
|
||||
|
||||
new Setting(containerLocalDatabaseEl)
|
||||
.setName("LongLine Threshold")
|
||||
.setDesc("(letters), If the line is longer than this, make the line to chunk")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.longLineThreshold + "")
|
||||
.onChange(async (value) => {
|
||||
let v = Number(value);
|
||||
if (isNaN(v) || v < 10 || v > 1000) {
|
||||
v = 10;
|
||||
}
|
||||
this.plugin.settings.longLineThreshold = v;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.setAttribute("type", "number");
|
||||
});
|
||||
let newDatabaseName = this.plugin.settings.additionalSuffixOfDatabaseName + "";
|
||||
new Setting(containerLocalDatabaseEl)
|
||||
.setName("Database suffix")
|
||||
.setDesc("Set unique name for using same vault name on different directory.")
|
||||
.setDesc("Optional: Set unique name for using same vault name on different directory.")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("")
|
||||
.setValue(newDatabaseName)
|
||||
@@ -644,7 +767,17 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
await this.plugin.initializeDatabase();
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(containerLocalDatabaseEl)
|
||||
.setName("")
|
||||
.setClass("wizardOnly")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Next")
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
changeDisplay("40");
|
||||
})
|
||||
);
|
||||
|
||||
addScreenElement("10", containerLocalDatabaseEl);
|
||||
const containerGeneralSettingsEl = containerEl.createDiv();
|
||||
@@ -670,6 +803,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
);
|
||||
new Setting(containerGeneralSettingsEl)
|
||||
.setName("Delete metadata of deleted files.")
|
||||
.setClass("wizardHidden")
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.plugin.settings.deleteMetadataOfDeletedFiles).onChange(async (value) => {
|
||||
this.plugin.settings.deleteMetadataOfDeletedFiles = value;
|
||||
@@ -681,6 +815,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
addScreenElement("20", containerGeneralSettingsEl);
|
||||
const containerSyncSettingEl = containerEl.createDiv();
|
||||
containerSyncSettingEl.createEl("h3", { text: "Sync Settings" });
|
||||
containerSyncSettingEl.addClass("wizardHidden")
|
||||
|
||||
if (this.plugin.settings.versionUpFlash != "") {
|
||||
const c = containerSyncSettingEl.createEl("div", { text: this.plugin.settings.versionUpFlash });
|
||||
@@ -965,7 +1100,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Chunk size")
|
||||
.setDesc("Customize chunk size for binary files (0.1MBytes). This cannot be increased when using IBM Cloudant.")
|
||||
@@ -982,6 +1116,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
});
|
||||
text.inputEl.setAttribute("type", "number");
|
||||
});
|
||||
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Read chunks online.")
|
||||
.setDesc("If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.")
|
||||
@@ -1047,24 +1182,16 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new Setting(containerMiscellaneousEl)
|
||||
.setName("Check integrity on saving")
|
||||
.setDesc("Check database integrity on saving to database")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.checkIntegrityOnSave).onChange(async (value) => {
|
||||
this.plugin.settings.checkIntegrityOnSave = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
let currentPrest = "NONE";
|
||||
let currentPreset = "NONE";
|
||||
|
||||
new Setting(containerMiscellaneousEl)
|
||||
.setName("Presets")
|
||||
.setDesc("Apply preset configuration")
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOptions({ NONE: "", LIVESYNC: "LiveSync", PERIODIC: "Periodic w/ batch", DISABLE: "Disable all sync" })
|
||||
.setValue(currentPrest)
|
||||
.onChange((value) => (currentPrest = value))
|
||||
.setValue(currentPreset)
|
||||
.onChange((value) => (currentPreset = value))
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
@@ -1072,7 +1199,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
.setDisabled(false)
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
if (currentPrest == "") {
|
||||
if (currentPreset == "") {
|
||||
Logger("Select any preset.", LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
@@ -1082,10 +1209,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
this.plugin.settings.syncOnSave = false;
|
||||
this.plugin.settings.syncOnStart = false;
|
||||
this.plugin.settings.syncOnFileOpen = false;
|
||||
if (currentPrest == "LIVESYNC") {
|
||||
if (currentPreset == "LIVESYNC") {
|
||||
this.plugin.settings.liveSync = true;
|
||||
Logger("Synchronization setting configured as LiveSync.", LOG_LEVEL.NOTICE);
|
||||
} else if (currentPrest == "PERIODIC") {
|
||||
} else if (currentPreset == "PERIODIC") {
|
||||
this.plugin.settings.batchSave = true;
|
||||
this.plugin.settings.periodicReplication = true;
|
||||
this.plugin.settings.syncOnSave = false;
|
||||
@@ -1097,9 +1224,32 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
this.plugin.saveSettings();
|
||||
await this.plugin.realizeSettingSyncMode();
|
||||
if (inWizard) {
|
||||
// @ts-ignore
|
||||
this.plugin.app.setting.close()
|
||||
await this.plugin.resetLocalDatabase();
|
||||
await this.plugin.initializeDatabase(true);
|
||||
if (rebuildRemote) {
|
||||
await this.plugin.markRemoteLocked();
|
||||
await this.plugin.tryResetRemoteDatabase();
|
||||
await this.plugin.markRemoteLocked();
|
||||
await this.plugin.markRemoteResolved();
|
||||
}
|
||||
await this.plugin.replicate(true);
|
||||
|
||||
Logger("All done! Please set up subsequent devices with 'Copy setup URI' and 'Open setup URI'.", LOG_LEVEL.NOTICE);
|
||||
// @ts-ignore
|
||||
this.plugin.app.commands.executeCommandById("obsidian-livesync:livesync-copysetupuri")
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
);
|
||||
|
||||
const infoApply = containerMiscellaneousEl.createEl("div", { text: `To finish setup, please select one of the presets` });
|
||||
infoApply.addClass("op-warn-info");
|
||||
infoApply.addClass("wizardOnly")
|
||||
|
||||
addScreenElement("40", containerMiscellaneousEl);
|
||||
|
||||
const containerHatchEl = containerEl.createDiv();
|
||||
@@ -1133,7 +1283,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
c.addClass("op-warn");
|
||||
}
|
||||
}
|
||||
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 boot up sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` });
|
||||
hatchWarn.addClass("op-warn-info");
|
||||
|
||||
new Setting(containerHatchEl)
|
||||
@@ -1163,36 +1313,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
Logger("done", LOG_LEVEL.NOTICE, "verify");
|
||||
})
|
||||
);
|
||||
new Setting(containerHatchEl)
|
||||
.setName("Sanity check")
|
||||
.setDesc("Verify")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Sanity check")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
// const notice = NewNotice("", 0);
|
||||
Logger(`Begin sanity check`, LOG_LEVEL.NOTICE, "sancheck");
|
||||
await runWithLock("sancheck", true, async () => {
|
||||
const db = this.plugin.localDatabase.localDatabase;
|
||||
const wf = await db.allDocs();
|
||||
const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:") && !e.id.startsWith("ps:") && e.id != "obsydian_livesync_version").map((e) => e.id);
|
||||
let count = 0;
|
||||
for (const id of filesDatabase) {
|
||||
count++;
|
||||
Logger(`${count}/${filesDatabase.length}\n${id2path(id)}`, LOG_LEVEL.NOTICE, "sancheck");
|
||||
const w = await db.get<EntryDoc>(id);
|
||||
if (!(await this.plugin.localDatabase.sanCheck(w))) {
|
||||
Logger(`The file ${id2path(id)} missing child(ren)`, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
}
|
||||
});
|
||||
Logger(`Done`, LOG_LEVEL.NOTICE, "sancheck");
|
||||
// Logger("done", LOG_LEVEL.NOTICE);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
new Setting(containerHatchEl)
|
||||
.setName("Suspend file watching")
|
||||
@@ -1204,40 +1324,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
})
|
||||
);
|
||||
|
||||
containerHatchEl.createEl("div", {
|
||||
text: sanitizeHTMLToDom(`Advanced buttons<br>
|
||||
These buttons could break your database easily.`),
|
||||
});
|
||||
new Setting(containerHatchEl)
|
||||
.setName("Reset remote database")
|
||||
.setDesc("Reset remote database, this affects only database. If you replicate again, remote database will restored by local database.")
|
||||
.setName("Discard local database to reset or uninstall Self-hosted LiveSync")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Reset")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
await this.plugin.tryResetRemoteDatabase();
|
||||
})
|
||||
);
|
||||
new Setting(containerHatchEl)
|
||||
.setName("Reset local database")
|
||||
.setDesc("Reset local database, this affects only database. If you replicate again, local database will restored by remote database.")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Reset")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
await this.plugin.resetLocalDatabase();
|
||||
})
|
||||
);
|
||||
new Setting(containerHatchEl)
|
||||
.setName("Initialize local database again")
|
||||
.setDesc("WARNING: Reset local database and reconstruct by storage data. It affects local database, but if you replicate remote as is, remote data will be merged or corrupted.")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("INITIALIZE")
|
||||
.setButtonText("Discard")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
@@ -1246,23 +1337,9 @@ 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);
|
||||
// With great respect, thank you TfTHacker!
|
||||
// refered: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts
|
||||
// Refer: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts
|
||||
const containerPluginSettings = containerEl.createDiv();
|
||||
containerPluginSettings.createEl("h3", { text: "Plugins and settings (beta)" });
|
||||
|
||||
@@ -1403,9 +1480,17 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
applyDisplayEnabled();
|
||||
addScreenElement("70", containerCorruptedDataEl);
|
||||
if (lastVersion != this.plugin.settings.lastReadUpdates) {
|
||||
changeDisplay("100");
|
||||
if (JSON.stringify(this.plugin.settings) != JSON.stringify(DEFAULT_SETTINGS)) {
|
||||
changeDisplay("100");
|
||||
} else {
|
||||
changeDisplay("110")
|
||||
}
|
||||
} else {
|
||||
changeDisplay("0");
|
||||
if (isAnySyncEnabled()) {
|
||||
changeDisplay("0");
|
||||
} else {
|
||||
changeDisplay("110")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export class PopoverSelectString extends FuzzySuggestModal<string> {
|
||||
constructor(app: App, note: string, placeholder: string | null, getItemsFun: () => string[], callback: (e: string) => void) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
this.setPlaceholder(placeholder ?? "y/n) " + note);
|
||||
this.setPlaceholder((placeholder ?? "y/n) ") + note);
|
||||
if (getItemsFun) this.getItemsFun = getItemsFun;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
2
src/lib
658
src/main.ts
@@ -1,4 +0,0 @@
|
||||
import { PouchDB as PouchDB_ } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js";
|
||||
|
||||
const Pouch: PouchDB.Static = PouchDB_;
|
||||
export { Pouch as PouchDB };
|
||||
@@ -3,7 +3,7 @@ import { normalizePath } from "obsidian";
|
||||
import { path2id_base, id2path_base } from "./lib/src/utils";
|
||||
|
||||
// For backward compatibility, using the path for determining id.
|
||||
// Only CouchDB nonacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||
// The first slash will be deleted when the path is normalized.
|
||||
export function path2id(filename: string): string {
|
||||
const x = normalizePath(filename);
|
||||
@@ -63,7 +63,7 @@ export async function memoIfNotExist<T>(key: string, func: () => T | Promise<T>)
|
||||
}
|
||||
return memos[key] as T;
|
||||
}
|
||||
export function retriveMemoObject<T>(key: string): T | false {
|
||||
export function retrieveMemoObject<T>(key: string): T | false {
|
||||
if (key in memos) {
|
||||
return memos[key];
|
||||
} else {
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { LOG_LEVEL, VER, VERSIONINFO_DOCID, EntryVersionInfo, EntryDoc, RemoteDBSettings, SYNCINFO_ID, SyncInfo } from "./lib/src/types";
|
||||
import { enableEncryption, resolveWithIgnoreKnownError } from "./lib/src/utils";
|
||||
import { PouchDB } from "./pouchdb-browser";
|
||||
import { requestUrl, RequestUrlParam, RequestUrlResponse } from "obsidian";
|
||||
|
||||
export const isValidRemoteCouchDBURI = (uri: string): boolean => {
|
||||
if (uri.startsWith("https://")) return true;
|
||||
if (uri.startsWith("http://")) return true;
|
||||
return false;
|
||||
};
|
||||
let last_post_successed = false;
|
||||
export const getLastPostFailedBySize = () => {
|
||||
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 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 (uri.toLowerCase() != uri) return "Remote URI and database name could not contain capital letters.";
|
||||
if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces.";
|
||||
let authHeader = "";
|
||||
if (auth.username && auth.password) {
|
||||
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${auth.username}:${auth.password}`));
|
||||
const encoded = window.btoa(utf8str);
|
||||
authHeader = "Basic " + encoded;
|
||||
} else {
|
||||
authHeader = "";
|
||||
}
|
||||
const conf: PouchDB.HttpAdapter.HttpAdapterConfiguration = {
|
||||
adapter: "http",
|
||||
auth,
|
||||
fetch: async function (url: string | Request, opts: RequestInit) {
|
||||
let size = "";
|
||||
const localURL = url.toString().substring(uri.length);
|
||||
const method = opts.method ?? "GET";
|
||||
if (opts.body) {
|
||||
const opts_length = opts.body.toString().length;
|
||||
if (opts_length > 1024 * 1024 * 10) {
|
||||
// over 10MB
|
||||
if (uri.contains(".cloudantnosqldb.")) {
|
||||
last_post_successed = false;
|
||||
Logger("This request should fail on IBM Cloudant.", LOG_LEVEL.VERBOSE);
|
||||
throw new Error("This request should fail on IBM Cloudant.");
|
||||
}
|
||||
}
|
||||
size = ` (${opts_length})`;
|
||||
}
|
||||
|
||||
if (!disableRequestURI && typeof url == "string" && typeof (opts.body ?? "") == "string") {
|
||||
const body = opts.body as string;
|
||||
|
||||
const transformedHeaders = { ...(opts.headers as Record<string, string>) };
|
||||
if (authHeader != "") transformedHeaders["authorization"] = authHeader;
|
||||
delete transformedHeaders["host"];
|
||||
delete transformedHeaders["Host"];
|
||||
delete transformedHeaders["content-length"];
|
||||
delete transformedHeaders["Content-Length"];
|
||||
const requestParam: RequestUrlParam = {
|
||||
url: url as string,
|
||||
method: opts.method,
|
||||
body: body,
|
||||
headers: transformedHeaders,
|
||||
contentType: "application/json",
|
||||
// contentType: opts.headers,
|
||||
};
|
||||
|
||||
try {
|
||||
const r = await fetchByAPI(requestParam);
|
||||
if (method == "POST" || method == "PUT") {
|
||||
last_post_successed = r.status - (r.status % 100) == 200;
|
||||
} else {
|
||||
last_post_successed = true;
|
||||
}
|
||||
Logger(`HTTP:${method}${size} to:${localURL} -> ${r.status}`, LOG_LEVEL.DEBUG);
|
||||
|
||||
return new Response(r.arrayBuffer, {
|
||||
headers: r.headers,
|
||||
status: r.status,
|
||||
statusText: `${r.status}`,
|
||||
});
|
||||
} catch (ex) {
|
||||
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
|
||||
// limit only in bulk_docs.
|
||||
if (url.toString().indexOf("_bulk_docs") !== -1) {
|
||||
last_post_successed = false;
|
||||
}
|
||||
Logger(ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
// -old implementation
|
||||
|
||||
try {
|
||||
const responce: Response = await fetch(url, opts);
|
||||
if (method == "POST" || method == "PUT") {
|
||||
last_post_successed = responce.ok;
|
||||
} else {
|
||||
last_post_successed = true;
|
||||
}
|
||||
Logger(`HTTP:${method}${size} to:${localURL} -> ${responce.status}`, LOG_LEVEL.DEBUG);
|
||||
return responce;
|
||||
} catch (ex) {
|
||||
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
|
||||
// limit only in bulk_docs.
|
||||
if (url.toString().indexOf("_bulk_docs") !== -1) {
|
||||
last_post_successed = false;
|
||||
}
|
||||
Logger(ex);
|
||||
throw ex;
|
||||
}
|
||||
// return await fetch(url, opts);
|
||||
},
|
||||
};
|
||||
|
||||
const db: PouchDB.Database<EntryDoc> = new PouchDB<EntryDoc>(uri, conf);
|
||||
if (passphrase && typeof passphrase === "string") {
|
||||
enableEncryption(db, passphrase);
|
||||
}
|
||||
try {
|
||||
const info = await db.info();
|
||||
return { db: db, info: info };
|
||||
} catch (ex) {
|
||||
let msg = `${ex.name}:${ex.message}`;
|
||||
if (ex.name == "TypeError" && ex.message == "Failed to fetch") {
|
||||
msg += "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector.";
|
||||
}
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
return msg;
|
||||
}
|
||||
};
|
||||
// check the version of remote.
|
||||
// if remote is higher than current(or specified) version, return false.
|
||||
export const checkRemoteVersion = async (db: PouchDB.Database, migrate: (from: number, to: number) => Promise<boolean>, barrier: number = VER): Promise<boolean> => {
|
||||
try {
|
||||
const versionInfo = (await db.get(VERSIONINFO_DOCID)) as EntryVersionInfo;
|
||||
if (versionInfo.type != "versioninfo") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const version = versionInfo.version;
|
||||
if (version < barrier) {
|
||||
const versionUpResult = await migrate(version, barrier);
|
||||
if (versionUpResult) {
|
||||
await bumpRemoteVersion(db);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (version == barrier) return true;
|
||||
return false;
|
||||
} catch (ex) {
|
||||
if (ex.status && ex.status == 404) {
|
||||
if (await bumpRemoteVersion(db)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
};
|
||||
export const bumpRemoteVersion = async (db: PouchDB.Database, barrier: number = VER): Promise<boolean> => {
|
||||
const vi: EntryVersionInfo = {
|
||||
_id: VERSIONINFO_DOCID,
|
||||
version: barrier,
|
||||
type: "versioninfo",
|
||||
};
|
||||
const versionInfo = (await resolveWithIgnoreKnownError(db.get(VERSIONINFO_DOCID), vi)) as EntryVersionInfo;
|
||||
if (versionInfo.type != "versioninfo") {
|
||||
return false;
|
||||
}
|
||||
vi._rev = versionInfo._rev;
|
||||
await db.put(vi);
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export async function putDesignDocuments(db: PouchDB.Database) {
|
||||
type DesignDoc = {
|
||||
_id: string;
|
||||
_rev: string;
|
||||
ver: number;
|
||||
filters: {
|
||||
default: string,
|
||||
push: string,
|
||||
pull: string,
|
||||
};
|
||||
}
|
||||
const design: DesignDoc = {
|
||||
"_id": "_design/replicate",
|
||||
"_rev": undefined as string | undefined,
|
||||
"ver": 2,
|
||||
"filters": {
|
||||
"default": function (doc: any, req: any) {
|
||||
return !("remote" in doc && doc.remote);
|
||||
}.toString(),
|
||||
"push": function (doc: any, req: any) {
|
||||
return true;
|
||||
}.toString(),
|
||||
"pull": function (doc: any, req: any) {
|
||||
return !(doc.type && doc.type == "leaf")
|
||||
}.toString(),
|
||||
}
|
||||
}
|
||||
|
||||
// We can use the filter on replication : filter: 'replicate/default',
|
||||
|
||||
try {
|
||||
const w = await db.get<DesignDoc>(design._id);
|
||||
if (w.ver < design.ver) {
|
||||
design._rev = w._rev;
|
||||
//@ts-ignore
|
||||
await db.put(design);
|
||||
return true;
|
||||
}
|
||||
} catch (ex) {
|
||||
if (ex.status && ex.status == 404) {
|
||||
delete design._rev;
|
||||
//@ts-ignore
|
||||
await db.put(design);
|
||||
return true;
|
||||
} else {
|
||||
Logger("Could not make design documents", LOG_LEVEL.INFO);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
10
styles.css
@@ -156,7 +156,7 @@ div.sls-setting-menu-btn {
|
||||
}
|
||||
|
||||
.sls-setting-tab:hover~div.sls-setting-menu-btn,
|
||||
.sls-setting-tab:checked~div.sls-setting-menu-btn {
|
||||
.sls-setting-label.selected .sls-setting-tab:checked~div.sls-setting-menu-btn {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
@@ -241,4 +241,12 @@ div.sls-setting-menu-btn {
|
||||
padding: 2px;
|
||||
margin: 1px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.isWizard .wizardHidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sls-setting:not(.isWizard) .wizardOnly {
|
||||
display: none;
|
||||
}
|
||||
@@ -2,15 +2,26 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"target": "ES2018",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
// "importsNotUsedAsValues": "error",
|
||||
"importHelpers": true,
|
||||
"importHelpers": false,
|
||||
"alwaysStrict": true,
|
||||
"lib": ["es2018", "DOM", "ES5", "ES6", "ES7"]
|
||||
"lib": [
|
||||
"es2018",
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7",
|
||||
"es2019.array"
|
||||
]
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["pouchdb-browser-webpack"]
|
||||
}
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"pouchdb-browser-webpack"
|
||||
]
|
||||
}
|
||||
42
updates.md
@@ -1,3 +1,18 @@
|
||||
### 0.15.0
|
||||
- Outdated configuration items have been removed.
|
||||
- Setup wizard has been implemented!
|
||||
|
||||
I appreciate for reviewing and giving me advice @Pouhon158!
|
||||
|
||||
#### Minors
|
||||
- 0.15.1 Missed the stylesheet.
|
||||
- 0.15.2 The wizard has been improved and documented!
|
||||
- 0.15.3 Fixed the issue about locking/unlocking remote database while rebuilding in the wizard.
|
||||
- 0.15.4 Fixed issues about asynchronous processing (e.g., Conflict check or hidden file detection)
|
||||
- 0.15.5 Add new features for setting Self-hosted LiveSync up more easier.
|
||||
- 0.15.6 File tracking logic has been refined.
|
||||
- 0.15.7 Fixed bug about renaming file.
|
||||
|
||||
### 0.14.1
|
||||
- The target selecting filter was implemented.
|
||||
Now we can set what files are synchronised by regular expression.
|
||||
@@ -11,23 +26,16 @@
|
||||
|
||||
#### Recommended configuration for Self-hosted CouchDB
|
||||
- Set chunk size to around 100 to 250 (10MB - 25MB per chunk)
|
||||
- *Set batch size to 100 and batch limit to 20 (0.14.2)*
|
||||
- Be sure to `Read chunks online` checked.
|
||||
|
||||
|
||||
### 0.13.0
|
||||
|
||||
- The metadata of the deleted files will be kept on the database by default. If you want to delete this as the previous version, please turn on `Delete metadata of deleted files.`. And, if you have upgraded from the older version, please ensure every device has been upgraded.
|
||||
- Please turn on `Delete metadata of deleted files.` if you are using livesync-classroom or filesystem-livesync.
|
||||
- We can see the history of deleted files.
|
||||
- `Pick file to show` was renamed to `Pick a file to show.
|
||||
- Files in the `Pick a file to show` are now ordered by their modified date descent.
|
||||
- Update information became to be shown on the major upgrade.
|
||||
|
||||
#### Minors
|
||||
- 0.13.1 Fixed on conflict resolution.
|
||||
- 0.13.2 Fixed file deletion failures.
|
||||
- 0.13.4
|
||||
- Now, we can synchronise hidden files that conflicted on each devices.
|
||||
- We can search for conflicting docs.
|
||||
- Pending processes can now be run at any time.
|
||||
- Performance improved on synchronising large numbers of files at once.
|
||||
- 0.14.2 Fixed issue about retrieving files if synchronisation has been interrupted or failed
|
||||
- 0.14.3 New test items have been added to `Check database configuration`.
|
||||
- 0.14.4 Fixed issue of importing configurations.
|
||||
- 0.14.5 Auto chunk size adjusting implemented.
|
||||
- 0.14.6 Change Target to ES2018
|
||||
- 0.14.7 Refactor and fix typos.
|
||||
- 0.14.8 Refactored again. There should be no change in behaviour, but please let me know if there is any.
|
||||
|
||||
... To continue on to `updates_old.md`.
|
||||
17
updates_old.md
Normal file
@@ -0,0 +1,17 @@
|
||||
### 0.13.0
|
||||
|
||||
- The metadata of the deleted files will be kept on the database by default. If you want to delete this as the previous version, please turn on `Delete metadata of deleted files.`. And, if you have upgraded from the older version, please ensure every device has been upgraded.
|
||||
- Please turn on `Delete metadata of deleted files.` if you are using livesync-classroom or filesystem-livesync.
|
||||
- We can see the history of deleted files.
|
||||
- `Pick file to show` was renamed to `Pick a file to show.
|
||||
- Files in the `Pick a file to show` are now ordered by their modified date descent.
|
||||
- Update information became to be shown on the major upgrade.
|
||||
|
||||
#### Minors
|
||||
- 0.13.1 Fixed on conflict resolution.
|
||||
- 0.13.2 Fixed file deletion failures.
|
||||
- 0.13.4
|
||||
- Now, we can synchronise hidden files that conflicted on each devices.
|
||||
- We can search for conflicting docs.
|
||||
- Pending processes can now be run at any time.
|
||||
- Performance improved on synchronising large numbers of files at once.
|
||||