Compare commits

...

57 Commits

Author SHA1 Message Date
vorotamoroz
f334974cc3 bump 2023-02-20 17:58:14 +09:00
vorotamoroz
8f2ae437c6 Fixed:
- Now reading error will be reported.
2023-02-20 17:54:57 +09:00
vorotamoroz
a0efda9e71 bump 2023-02-17 17:37:15 +09:00
vorotamoroz
be3d61c1c7 - New feature:
- If any conflicted files have been left, they will be reported.
- Fixed:
  - Now the name of the conflicting file is shown on the conflict-resolving dialogue.
  - Hidden files are now able to be merged again.
  - No longer error caused at plug-in being loaded.
- Improved:
  - Caching chunks are now limited in total size of cached chunks.
2023-02-17 17:35:06 +09:00
vorotamoroz
b24c4ef55b bump 2023-02-13 15:17:23 +09:00
vorotamoroz
ff850b48ca - Improved:
- Now we can preserve the logs into the file.
    - Note: This option will be enabled automatically also when we flagging a red flag.
  - File names can now be made platform-appropriate.
- Refactored:
  - Some redundant implementations have been sorted out.
2023-02-13 15:17:09 +09:00
vorotamoroz
ad3860ac40 bump 2023-02-09 17:24:57 +09:00
vorotamoroz
437b7ebae1 - Fixed:
- Now hidden files will not be synchronised while we are not configured.
  - Some processes could start without waiting for synchronisation to complete, but now they will wait for.
- Improved
  - Now, by placing `redflag3.md`, we can discard the local database and fetch again.
2023-02-09 17:22:12 +09:00
vorotamoroz
e3305c24e1 Merge pull request #170 from hilsonp/main
add tip : redflag.md can be a directory which is easier to create on iOS
2023-02-09 15:45:00 +09:00
Pierre Hilson
d008e2a1d0 add tip : redflag.md can be a directory which is easier to create on iOS 2023-02-03 18:58:54 +01:00
vorotamoroz
bb8c7eb043 bump 2023-02-03 18:36:33 +09:00
vorotamoroz
e61bebd3ee - Fixed: Skip patterns now handle capital letters.
- Improved
  - New configuration to avoid exceeding throttle capacity.
  - The conflicted `data.json` is no longer merged automatically.
2023-02-03 18:30:40 +09:00
vorotamoroz
99594fe517 Merge pull request #169 from karasevm/feat-timeout
Allow switching CouchDB long polling from heartbeat to timeout
2023-02-03 17:41:54 +09:00
Maksim Karasev
972d208af4 Expose useTimeouts option in the settings tab 2023-02-02 02:24:32 +03:00
vorotamoroz
2c36ec497c bump 2023-01-30 19:56:46 +09:00
vorotamoroz
677895547c Ensure that Obsidian will be notified when hidden files have been changed 2023-01-30 19:48:09 +09:00
vorotamoroz
aec0b2986b bump 2023-01-28 21:24:03 +09:00
vorotamoroz
e6025b92d8 Ensure logging. 2023-01-28 21:20:26 +09:00
vorotamoroz
fad9fed5ca bump 2023-01-27 21:47:04 +09:00
vorotamoroz
e46246cd63 Fixed:
- Fixed lack of error handling.
2023-01-27 17:49:53 +09:00
vorotamoroz
0f3be19dd7 bump 2023-01-25 22:39:50 +09:00
vorotamoroz
bc568ff479 Fixed:
- Now we can merge JSON files even if they have entries which cannot be compared.
2023-01-25 22:37:55 +09:00
vorotamoroz
2fdc7669f3 bump 2023-01-25 20:54:20 +09:00
vorotamoroz
ec8d9785ed - Improved:
- Plugins and their settings no longer need scanning if changes are monitored.
  - Now synchronising plugins and their settings are performed parallelly and faster.
  - We can place `redflag2.md` to rebuild the database automatically while the boot sequence.
- Experimental:
  - We can use a new adapter on PouchDB. This will make us smoother.
    - Note: Not compatible with the older version.
- Fixed:
  - The default batch size is smaller again.
  - Plugins and their setting can be synchronised again.
  - Hidden files and plugins are correctly scanned while rebuilding.
  - Files with the name started `_` are also being performed conflict-checking.
2023-01-25 20:53:20 +09:00
vorotamoroz
71a80cacc3 bump again 2023-01-19 19:05:16 +09:00
vorotamoroz
38daeca89f fixed leaked logging 2023-01-19 19:03:57 +09:00
vorotamoroz
7ec64a6a93 bump 2023-01-19 18:55:18 +09:00
vorotamoroz
c5c6deb742 Improved:
- Confidential information has no longer stored in data.json as is.
- Synchronising progress has been shown in the notification.
- We can commit passphrases with a keyboard.
- Configuration which had not been saved yet is marked now.

Fixed:
- Hidden files have been synchronised again.

And, minor changes have been included.
2023-01-19 18:50:06 +09:00
vorotamoroz
ef57fbfdda Fixed:
- Now the filename is shown on the Conflict resolving dialog
- Rename of files has been improved again.
2023-01-19 13:11:30 +09:00
vorotamoroz
bc158e9f2b bump 2023-01-17 17:46:06 +09:00
vorotamoroz
6513c53c7e Fixed:
- Document history is now displayed again.

Reorganised:
- Many files have been refactored.
2023-01-17 17:39:26 +09:00
vorotamoroz
5d1074065c bump 2023-01-16 17:33:31 +09:00
vorotamoroz
b444082b0c Fixed:
- Performance improvement
- Now `Chunk size` can be set to under one hundred.

New feature:
- The number of transfers required before replication stabilises is now displayed.
2023-01-16 17:31:37 +09:00
vorotamoroz
d5e6419504 bump 2023-01-15 11:17:08 +09:00
vorotamoroz
1bf1e1540d Fix diff check. 2023-01-15 11:09:23 +09:00
vorotamoroz
be1e6b11ac Fixed
- Large files addressed.
2023-01-13 19:43:39 +09:00
vorotamoroz
a486788572 bump 2023-01-06 16:33:54 +09:00
vorotamoroz
e5784a1da6 Fixed:
- Conflict merge of internal files is no longer broken.
Improved:
- Smoother status display inside the editor.
2023-01-06 16:27:39 +09:00
vorotamoroz
2100e22276 bump 2022-12-27 18:12:00 +09:00
vorotamoroz
ec08dc5fe8 Improved:
- Performance improved
  Prebuilt PouchDB is no longer used.

Fixed:
- Merging hidden files is also fixed.

New Feature:
- Now we can synchronise automatically after merging conflicts.
2022-12-27 18:09:51 +09:00
vorotamoroz
c92e94e552 fix eslint configuration 2022-12-27 17:58:42 +09:00
vorotamoroz
c7db8592c6 bump 2022-12-26 16:16:46 +09:00
vorotamoroz
fc3617d9f9 Fixed:
- Fixed merging issues.
- Fixed button styling.

Changed:
- Default behaviour of conflict checking on synchronising.
2022-12-26 16:12:57 +09:00
vorotamoroz
34c1b040db bump 2022-12-24 21:05:32 +09:00
vorotamoroz
6b85aecafe Fixed:
- Now our renamed/deleted files have been surely deleted again.
2022-12-24 21:05:09 +09:00
vorotamoroz
4dabadd5ea A little better. 2022-12-22 18:02:12 +09:00
vorotamoroz
0619c96c48 bump 2022-12-22 17:59:30 +09:00
vorotamoroz
b0f612b61c New feature:
- Use dynamic iteration count
Fixed:
- Read chunks online will fetch the remote chunks correctly.
- Read chunks online will save fetched chunks to the local database.
2022-12-22 17:58:52 +09:00
vorotamoroz
81caad8602 Canvases are now treated as a sort of plain text file. 2022-12-21 15:09:10 +09:00
vorotamoroz
f5e28b5e1c bump 2022-12-21 14:51:59 +09:00
vorotamoroz
0c206226b1 New feature
- JSON merging for data and canvas.
2022-12-21 14:51:39 +09:00
vorotamoroz
1ad5dcc1cc bump 2022-12-16 18:56:28 +09:00
vorotamoroz
a512566e5b New feature
- We can merge conflicted documents automatically if sensible.

Fixed:
- Writing to the storage will be pended while they have conflicts after replication.

Minor changes included.
2022-12-16 18:55:04 +09:00
vorotamoroz
02de82af46 bump 2022-12-06 18:03:31 +09:00
vorotamoroz
840e03a2d3 Fixed:
- Now we can verify and repair database again.
2022-12-06 17:59:48 +09:00
vorotamoroz
96b676caf3 bump 2022-12-05 19:53:24 +09:00
vorotamoroz
a8219de375 Improved:
- Splitting markdown
- Saving chunks

Changed:
- Chunk ID numbering rules

Fixed:
- Just weed.
2022-12-05 19:37:24 +09:00
25 changed files with 3355 additions and 1041 deletions

View File

@@ -1,3 +1,4 @@
npm node_modules
node_modules
build
.eslintrc.js.bak
.eslintrc.js.bak
src/lib/src/patches/pouchdb-utils

View File

@@ -2,7 +2,7 @@
[Japanese docs](./README_ja.md) [Chinese docs](./README_cn.md).
Self-hosted LiveSync is a community implemented synchronization plugin.
Self-hosted LiveSync is a community-implemented synchronization plugin.
A self-hosted or purchased CouchDB acts as the intermediate server. Available on every obsidian-compatible platform.
Note: It has no compatibility with the official "Obsidian Sync".
@@ -16,19 +16,19 @@ Before installing or upgrading LiveSync, please back your vault up.
- Visual conflict resolver included.
- Bidirectional synchronization between devices nearly in real-time
- You can use CouchDB or its compatibles like IBM Cloudant.
- End-to-End encryption supported.
- End-to-End encryption is supported.
- Plugin synchronization(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.)
Useful for researchers, engineers and developers with a need to keep their notes fully self-hosted for security reasons. Or just anyone who would like the peace of mind knowing that their notes are fully private.
Useful for researchers, engineers and developers with a need to keep their notes fully self-hosted for security reasons. Or just anyone who would like the peace of mind of knowing that their notes are fully private.
## IMPORTANT NOTICE
- Do not use in conjunction with another synchronization solution (including iCloud, Obsidian Sync). Before enabling this plugin, make sure to disable all the other synchronization methods to avoid content corruption or duplication. If you want to synchronize to two or more services, do them one by one and never enable two synchronization methods at the same time.
- Do not enable this plugin with another synchronization solution at the same time (including iCloud and Obsidian Sync). Before enabling this plugin, make sure to disable all the other synchronization methods to avoid content corruption or duplication. If you want to synchronize to two or more services, do them one by one and never enable two synchronization methods at the same time.
This includes not putting your vault inside a cloud-synchronized folder (eg. an iCloud folder or Dropbox folder)
- This is a synchronization plugin. Not a backup solutions. Do not rely on this for backup.
- This is a synchronization plugin. Not a backup solution. Do not rely on this for backup.
- If the device's storage runs out, database corruption may happen.
- Hidden files or any other invisible files wouldn't be kept in the database, thus won't be synchronized. (**and may also get deleted**)
- Hidden files or any other invisible files wouldn't be kept in the database, and thus won't be synchronized. (**and may also get deleted**)
## How to use
@@ -38,7 +38,7 @@ First, get your database ready. IBM Cloudant is preferred for testing. Or you ca
1. [Setup IBM Cloudant](docs/setup_cloudant.md)
2. [Setup your CouchDB](docs/setup_own_server.md)
Note: More information about alternative hosting methods needed! Currently, [using fly.io](https://github.com/vrtmrz/obsidian-livesync/discussions/85) is being discussed.
Note: More information about alternative hosting methods is needed! Currently, [using fly.io](https://github.com/vrtmrz/obsidian-livesync/discussions/85) is being discussed.
### Configure the plugin
@@ -46,13 +46,13 @@ See [Quick setup guide](doccs/../docs/quick_setup.md)
## Something looks corrupted...
Please open the configuration link again and Answer as below:
Please open the configuration link again and Answer 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 with a timestamp. So you can use a existed vault).
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 with a timestamp. So you can use an existing vault).
## Test Server
@@ -78,18 +78,20 @@ If you have deleted or renamed files, please wait until ⏳ icon disappeared.
## Hints
- If a folder becomes empty after a replication, it will be deleted by default. But you can toggle this behaviour. Check the [Settings](docs/settings.md).
- LiveSync mode drains more batteries in mobile devices. Periodic sync with some automatic sync is recommended.
- Mobile Obsidian can not connect to a non-secure (HTTP) or a locally-signed servers, even if the root certificate is installed on the device.
- Mobile Obsidian can not connect to non-secure (HTTP) or locally-signed servers, even if the root certificate is installed on the device.
- 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.
- 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)
- 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.
- To stop the boot-up sequence (eg. for fixing problems on databases), you can put a `redflag.md` file (or directory) at the root of your vault.
Tip for iOS: a redflag directory can be created at the root of the vault using the File application.
- Also, with `redflag2.md` placed, we can automatically rebuild both the local and the remote databases during the boot-up sequence. With `redflag3.md`, we can discard only the local database and fetch from the remote again.
- Q: The 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 is 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 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.)
Repo is here: [obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip). (Docs are a work in progress.)
## License

View File

@@ -1,5 +1,5 @@
# Quick setup
The Setup wizard has been implemented since v0.15.0. This simplifies the initial set-up.
The Setup wizard has been implemented since v0.15.0. This simplifies the initial setup.
Note: The subsequent devices should be set up using the `Copy setup URI` and `Open setup URI`.
@@ -34,18 +34,18 @@ Enter the information in the database we have set up.
![](../images/quick_setup_4.png)
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.
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 if they are leaked is reduced. So we strongly recommend enabling 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 connectionCheck database configuraion
### Test database connection and Check database configuration
Here we can check the status of the connection to the database and the database settings.
![](../images/quick_setup_5.png)
#### 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 whether we can connect to the database. If it fails, there are several reasons, but once you have done the `Check database configuration`, check if it fails there too.
#### Check database configuration
@@ -64,11 +64,11 @@ 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
## Local Database configuration
![](../images/quick_setup_8.png)
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.
Configure the local database. If we already have a Vaults with Self-hosted LiveSync installed and having the 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.

View File

@@ -17,14 +17,14 @@ If you feel something, please feel free to inform me.
| 🚑 | [Corrupted data](#corrupted-data) |
## Remote Database Configurations
Configure settings of synchronize server. If any synchronization is enabled, you can't edit this section. Please disable all synchronization to change.
Configure the settings of synchronize server. If any synchronization is enabled, you can't edit this section. Please disable all synchronization to change.
### URI
URI of CouchDB. In the case of Cloudant, It's "External Endpoint(preferred)".
**Do not end it up with a slash** when it doesn't contain the database name.
### Username
Your CouchDB's Username. With administrator's privilege is preferred.
Your CouchDB's Username. Administrator's privilege is preferred.
### Password
Your CouchDB's Password.
@@ -47,11 +47,11 @@ The passphrase to used as the key of encryption. Please use the long text.
### Apply
Set the End to End encryption enabled and its passphrase for use in replication.
If you change the passphrase of a existing database, overwriting the remote database is strongly recommended.
If you change the passphrase of an existing database, overwriting the remote database is strongly recommended.
### Overwrite remote database
Overwrite the remote database by the local database using the passphrase you applied.
Overwrite the remote database with the local database using the passphrase you applied.
### Rebuild
@@ -61,17 +61,17 @@ Rebuild remote and local databases with local files. It will delete all document
You can check the connection by clicking this button.
### Check database configuration
You can check and modify your CouchDB's configuration from here directly.
You can check and modify your CouchDB configuration from here directly.
### Lock remote database.
Other devices are banned from the database when you have locked the database.
If you have something troubled with other devices, you can protect the vault and remote database by your device.
If you have something troubled with other devices, you can protect the vault and remote database with your device.
## Local Database Configurations
"Local Database" is created inside your obsidian.
### Batch database update
Delay database update until raise replication, open another file, window visibility changed, or file events except for file modification.
Delay database update until raise replication, open another file, window visibility changes, or file events except for file modification.
This option can not be used with LiveSync at the same time.
@@ -81,23 +81,23 @@ If one device rebuilds or locks the remote database, every other device will be
### minimum chunk size and LongLine threshold
The configuration of chunk splitting.
Self-hosted LiveSync splits the note into chunks for efficient synchronization. This chunk should be longer than "Minimum chunk size".
Self-hosted LiveSync splits the note into chunks for efficient synchronization. This chunk should be longer than the "Minimum chunk size".
Specifically, the length of the chunk is determined by the following orders.
1. Find the nearest newline character, and if it is farther than LongLineThreshold, this piece becomes an independent chunk.
2. If not, find nearest to these items.
1. Newline character
2. Empty line (Windows style)
3. Empty line (non-Windows style)
3. Compare the farther in these 3 positions and next "\[newline\]#" position, pick a shorter piece to as chunk.
2. If not, find the nearest to these items.
1. A newline character
2. An empty line (Windows style)
3. An empty line (non-Windows style)
3. Compare the farther in these 3 positions and the next "newline\]#" position, and pick a shorter piece as a chunk.
This rule was made empirically from my dataset. If this rule acts as badly on your data. Please give me the information.
You can dump saved note structure to `Dump informations of this doc`. Replace every character to x except newline and "#" when sending information to me.
You can dump saved note structure to `Dump informations of this doc`. Replace every character with x except newline and "#" when sending information to me.
Default values are 20 letters and 250 letters.
The default values are 20 letters and 250 letters.
## General Settings

View File

@@ -1,7 +1,7 @@
# Setup CouchDB to your server
## Install CouchDB and access from PC or Mac
## Install CouchDB and access from a PC or Mac
The easiest way to set up the CouchDB is using the [docker image]((https://hub.docker.com/_/couchdb)).
@@ -39,7 +39,7 @@ $ docker run --rm -it -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /pat
*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.
If you could confirm that Self-hosted LiveSync can sync with the server, launch the docker image as a background as you like.
Example to run docker in detached mode:
```
@@ -47,10 +47,10 @@ $ docker run -d --restart always -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=passw
```
*Remember to replace the path with the path to your local.ini*
## Access from mobile device
## Access from a mobile device
If you want to access Self-hosted LiveSync from mobile devices, you need a valid SSL certificate.
### Testing from mobile
### Testing from a mobile
In the testing phase, [localhost.run](http://localhost.run/) or something like services is very useful.
example on using localhost.run)
@@ -94,6 +94,6 @@ Set the A record of your domain to point to your server, and host reverse proxy
Note: Mounting CouchDB on the top directory is not recommended.
Using Caddy is a handy way to serve the server with SSL automatically.
I have published [docker-compose.yml and ini files](https://github.com/vrtmrz/self-hosted-livesync-server) that launches Caddy and CouchDB at once. Please try it out.
I have published [docker-compose.yml and ini files](https://github.com/vrtmrz/self-hosted-livesync-server) that launch Caddy and CouchDB at once. Please try it out.
And, be sure to check the server log and be careful of malicious access.

View File

@@ -25,14 +25,16 @@ esbuild
"MANIFEST_VERSION": `"${manifestJson.version}"`,
"PACKAGE_VERSION": `"${packageJson.version}"`,
"UPDATE_INFO": `${updateInfo}`,
"global":"window",
},
external: ["obsidian", "electron", ...builtins],
external: ["obsidian", "electron", "crypto"],
format: "cjs",
watch: !prod,
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
platform: "browser",
plugins: [
sveltePlugin({
preprocess: sveltePreprocess(),

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.16.8",
"version": "0.17.25",
"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",

887
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "0.16.8",
"version": "0.17.25",
"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",
@@ -24,11 +24,21 @@
"eslint": "^8.28.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.26.0",
"events": "^3.3.0",
"obsidian": "^0.16.3",
"postcss": "^8.4.19",
"postcss-load-config": "^4.0.1",
"pouchdb-adapter-http": "^8.0.0",
"pouchdb-adapter-idb": "^8.0.0",
"pouchdb-adapter-indexeddb": "^8.0.0",
"pouchdb-core": "^8.0.0",
"pouchdb-find": "^8.0.0",
"pouchdb-mapreduce": "^8.0.0",
"pouchdb-replication": "^8.0.0",
"pouchdb-utils": "file:src/lib/src/patches/pouchdb-utils",
"svelte": "^3.53.1",
"svelte-preprocess": "^4.10.7",
"transform-pouch": "^2.0.0",
"tslib": "^2.4.1",
"typescript": "^4.9.3"
},

View File

@@ -1,17 +1,19 @@
import { App, Modal } from "obsidian";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
import { diff_result } from "./lib/src/types";
import { escapeStringToHTML } from "./lib/src/utils";
import { escapeStringToHTML } from "./lib/src/strbin";
export class ConflictResolveModal extends Modal {
// result: Array<[number, string]>;
result: diff_result;
filename: string;
callback: (remove_rev: string) => Promise<void>;
constructor(app: App, diff: diff_result, callback: (remove_rev: string) => Promise<void>) {
constructor(app: App, filename: string, diff: diff_result, callback: (remove_rev: string) => Promise<void>) {
super(app);
this.result = diff;
this.callback = callback;
this.filename = filename;
}
onOpen() {
@@ -20,6 +22,7 @@ export class ConflictResolveModal extends Modal {
contentEl.empty();
contentEl.createEl("h2", { text: "This document has conflicted changes." });
contentEl.createEl("span", { text: this.filename });
const div = contentEl.createDiv("");
div.addClass("op-scrollable");
let diff = "";

View File

@@ -1,10 +1,12 @@
import { TFile, Modal, App } from "obsidian";
import { path2id } from "./utils";
import { base64ToArrayBuffer, base64ToString, escapeStringToHTML, isValidPath } from "./lib/src/utils";
import { isValidPath, path2id } from "./utils";
import { base64ToArrayBuffer, base64ToString, escapeStringToHTML } from "./lib/src/strbin";
import ObsidianLiveSyncPlugin from "./main";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
import { LoadedEntry, LOG_LEVEL } from "./lib/src/types";
import { Logger } from "./lib/src/logger";
import { isErrorOfMissingDoc } from "./lib/src/utils_couchdb";
import { getDocData } from "./lib/src/utils";
export class DocumentHistoryModal extends Modal {
plugin: ObsidianLiveSyncPlugin;
@@ -33,13 +35,13 @@ export class DocumentHistoryModal extends Modal {
const db = this.plugin.localDatabase;
try {
const w = await db.localDatabase.get(path2id(this.file), { revs_info: true });
this.revs_info = w._revs_info.filter((e) => e.status == "available");
this.revs_info = w._revs_info.filter((e) => e?.status == "available");
this.range.max = `${this.revs_info.length - 1}`;
this.range.value = this.range.max;
this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`);
await this.loadRevs();
} catch (ex) {
if (ex.status && ex.status == 404) {
if (isErrorOfMissingDoc(ex)) {
this.range.max = "0";
this.range.value = "";
this.range.disabled = true;
@@ -64,7 +66,7 @@ export class DocumentHistoryModal extends Modal {
this.currentDoc = w;
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
let result = "";
const w1data = w.datatype == "plain" ? w.data : base64ToString(w.data);
const w1data = w.datatype == "plain" ? getDocData(w.data) : base64ToString(w.data);
this.currentDeleted = w.deleted;
this.currentText = w1data;
if (this.showDiff) {
@@ -74,7 +76,7 @@ export class DocumentHistoryModal extends Modal {
const w2 = await db.getDBEntry(path2id(this.file), { rev: oldRev }, false, false, true);
if (w2 != false) {
const dmp = new diff_match_patch();
const w2data = w2.datatype == "plain" ? w2.data : base64ToString(w2.data);
const w2data = w2.datatype == "plain" ? getDocData(w2.data) : base64ToString(w2.data);
const diff = dmp.diff_main(w2data, w1data);
dmp.diff_cleanupSemantic(diff);
for (const v of diff) {
@@ -176,7 +178,7 @@ export class DocumentHistoryModal extends Modal {
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);
await this.app.vault.adapter.write(pathToWrite, getDocData(this.currentDoc.data));
await focusFile(pathToWrite);
this.close();
} else if (this.currentDoc?.datatype == "newnote") {

54
src/JsonResolveModal.ts Normal file
View File

@@ -0,0 +1,54 @@
import { App, Modal } from "obsidian";
import { LoadedEntry } from "./lib/src/types";
import JsonResolvePane from "./JsonResolvePane.svelte";
export class JsonResolveModal extends Modal {
// result: Array<[number, string]>;
filename: string;
callback: (keepRev: string, mergedStr?: string) => Promise<void>;
docs: LoadedEntry[];
component: JsonResolvePane;
constructor(app: App, filename: string, docs: LoadedEntry[], callback: (keepRev: string, mergedStr?: string) => Promise<void>) {
super(app);
this.callback = callback;
this.filename = filename;
this.docs = docs;
}
async UICallback(keepRev: string, mergedStr?: string) {
this.close();
await this.callback(keepRev, mergedStr);
this.callback = null;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
if (this.component == null) {
this.component = new JsonResolvePane({
target: contentEl,
props: {
docs: this.docs,
callback: (keepRev, mergedStr) => this.UICallback(keepRev, mergedStr),
},
});
}
return;
}
onClose() {
const { contentEl } = this;
contentEl.empty();
// contentEl.empty();
if (this.callback != null) {
this.callback(null);
}
if (this.component != null) {
this.component.$destroy();
this.component = null;
}
}
}

162
src/JsonResolvePane.svelte Normal file
View File

@@ -0,0 +1,162 @@
<script lang="ts">
import { Diff, DIFF_DELETE, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
import type { LoadedEntry } from "./lib/src/types";
import { base64ToString } from "./lib/src/strbin";
import { getDocData } from "./lib/src/utils";
import { mergeObject } from "./utils";
export let docs: LoadedEntry[] = [];
export let callback: (keepRev: string, mergedStr?: string) => Promise<void> = async (_, __) => {
Promise.resolve();
};
let docA: LoadedEntry = undefined;
let docB: LoadedEntry = undefined;
let docAContent = "";
let docBContent = "";
let objA: any = {};
let objB: any = {};
let objAB: any = {};
let objBA: any = {};
let diffs: Diff[];
const modes = [
["", "Not now"],
["A", "A"],
["B", "B"],
["AB", "A + B"],
["BA", "B + A"],
] as ["" | "A" | "B" | "AB" | "BA", string][];
let mode: "" | "A" | "B" | "AB" | "BA" = "";
function docToString(doc: LoadedEntry) {
return doc.datatype == "plain" ? getDocData(doc.data) : base64ToString(doc.data);
}
function revStringToRevNumber(rev: string) {
return rev.split("-")[0];
}
function getDiff(left: string, right: string) {
const dmp = new diff_match_patch();
const mapLeft = dmp.diff_linesToChars_(left, right);
const diffLeftSrc = dmp.diff_main(mapLeft.chars1, mapLeft.chars2, false);
dmp.diff_charsToLines_(diffLeftSrc, mapLeft.lineArray);
return diffLeftSrc;
}
function getJsonDiff(a: object, b: object) {
return getDiff(JSON.stringify(a, null, 2), JSON.stringify(b, null, 2));
}
function apply() {
if (mode == "A") return callback(docA._rev, null);
if (mode == "B") return callback(docB._rev, null);
if (mode == "BA") return callback(null, JSON.stringify(objBA, null, 2));
if (mode == "AB") return callback(null, JSON.stringify(objAB, null, 2));
callback(null, null);
}
$: {
if (docs && docs.length >= 1) {
if (docs[0].mtime < docs[1].mtime) {
docA = docs[0];
docB = docs[1];
} else {
docA = docs[1];
docB = docs[0];
}
docAContent = docToString(docA);
docBContent = docToString(docB);
try {
objA = false;
objB = false;
objA = JSON.parse(docAContent);
objB = JSON.parse(docBContent);
objAB = mergeObject(objA, objB);
objBA = mergeObject(objB, objA);
if (JSON.stringify(objAB) == JSON.stringify(objBA)) {
objBA = false;
}
} catch (ex) {
objBA = false;
objAB = false;
}
}
}
$: mergedObjs = {
"": false,
A: objA,
B: objB,
AB: objAB,
BA: objBA,
};
$: selectedObj = mode in mergedObjs ? mergedObjs[mode] : {};
$: {
diffs = getJsonDiff(objA, selectedObj);
console.dir(selectedObj);
}
</script>
<h1>File Conflicted</h1>
{#if !docA || !docB}
<div class="message">Just for a minute, please!</div>
<div class="buttons">
<button on:click={apply}>Dismiss</button>
</div>
{:else}
<div class="options">
{#each modes as m}
{#if m[0] == "" || mergedObjs[m[0]] != false}
<label class={`sls-setting-label ${m[0] == mode ? "selected" : ""}`}
><input type="radio" name="disp" bind:group={mode} value={m[0]} class="sls-setting-tab" />
<div class="sls-setting-menu-btn">{m[1]}</div></label
>
{/if}
{/each}
</div>
{#if selectedObj != false}
<div class="op-scrollable json-source">
{#each diffs as diff}
<span class={diff[0] == DIFF_DELETE ? "deleted" : diff[0] == DIFF_INSERT ? "added" : "normal"}>{diff[1]}</span>
{/each}
</div>
{:else}
NO PREVIEW
{/if}
<div>
A Rev:{revStringToRevNumber(docA._rev)} ,{new Date(docA.mtime).toLocaleString()}
{docAContent.length} letters
</div>
<div>
B Rev:{revStringToRevNumber(docB._rev)} ,{new Date(docB.mtime).toLocaleString()}
{docBContent.length} letters
</div>
<div class="buttons">
<button on:click={apply}>Apply</button>
</div>
{/if}
<style>
.deleted {
text-decoration: line-through;
}
* {
box-sizing: border-box;
}
.scroller {
display: flex;
flex-direction: column;
overflow-y: scroll;
max-height: 60vh;
user-select: text;
}
.json-source {
white-space: pre;
height: auto;
overflow: auto;
min-height: var(--font-ui-medium);
flex-grow: 1;
}
</style>

View File

@@ -3,14 +3,15 @@ import { KeyValueDatabase, OpenKeyValueDatabase } from "./KeyValueDB.js";
import { LocalPouchDBBase } from "./lib/src/LocalPouchDBBase.js";
import { Logger } from "./lib/src/logger.js";
import { PouchDB } from "./lib/src/pouchdb-browser.js";
import { EntryDoc, LOG_LEVEL } from "./lib/src/types.js";
import { enableEncryption } from "./lib/src/utils.js";
import { EntryDoc, LOG_LEVEL, ObsidianLiveSyncSettings } from "./lib/src/types.js";
import { enableEncryption } from "./lib/src/utils_couchdb.js";
import { isCloudantURI, isValidRemoteCouchDBURI } from "./lib/src/utils_couchdb.js";
import { id2path, path2id } from "./utils.js";
export class LocalPouchDB extends LocalPouchDBBase {
kvDB: KeyValueDatabase;
settings: ObsidianLiveSyncSettings;
id2path(filename: string): string {
return id2path(filename);
}
@@ -18,6 +19,10 @@ export class LocalPouchDB extends LocalPouchDBBase {
return path2id(filename);
}
CreatePouchDBInstance<T>(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database<T> {
if (this.settings.useIndexedDBAdapter) {
options.adapter = "indexeddb";
return new PouchDB(name + "-indexeddb", options);
}
return new PouchDB(name, options);
}
beforeOnUnload(): void {
@@ -52,7 +57,7 @@ export class LocalPouchDB extends LocalPouchDBBase {
}
async connectRemoteCouchDB(uri: string, auth: { username: string; password: string }, disableRequestURI: boolean, passphrase: string | boolean): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> {
async connectRemoteCouchDB(uri: string, auth: { username: string; password: string }, disableRequestURI: boolean, passphrase: string | false, useDynamicIterationCount: 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.";
@@ -154,8 +159,8 @@ export class LocalPouchDB extends LocalPouchDBBase {
};
const db: PouchDB.Database<EntryDoc> = new PouchDB<EntryDoc>(uri, conf);
if (passphrase && typeof passphrase === "string") {
enableEncryption(db, passphrase);
if (passphrase !== "false" && typeof passphrase === "string") {
enableEncryption(db, passphrase, useDynamicIterationCount);
}
try {
const info = await db.info();

View File

@@ -1,21 +1,17 @@
import { App, Modal } from "obsidian";
import { escapeStringToHTML } from "./lib/src/utils";
import { logMessageStore } from "./lib/src/stores";
import { escapeStringToHTML } from "./lib/src/strbin";
import ObsidianLiveSyncPlugin from "./main";
export class LogDisplayModal extends Modal {
plugin: ObsidianLiveSyncPlugin;
logEl: HTMLDivElement;
unsubscribe: () => void;
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
super(app);
this.plugin = plugin;
}
updateLog() {
let msg = "";
for (const v of this.plugin.logMessage) {
msg += escapeStringToHTML(v) + "<br>";
}
this.logEl.innerHTML = msg;
}
onOpen() {
const { contentEl } = this;
@@ -25,13 +21,18 @@ export class LogDisplayModal extends Modal {
div.addClass("op-scrollable");
div.addClass("op-pre");
this.logEl = div;
this.updateLog = this.updateLog.bind(this);
this.plugin.addLogHook = this.updateLog;
this.updateLog();
this.unsubscribe = logMessageStore.observe((e) => {
let msg = "";
for (const v of e) {
msg += escapeStringToHTML(v) + "<br>";
}
this.logEl.innerHTML = msg;
})
logMessageStore.invalidate();
}
onClose() {
const { contentEl } = this;
contentEl.empty();
this.plugin.addLogHook = null;
if (this.unsubscribe) this.unsubscribe();
}
}

View File

@@ -1,7 +1,9 @@
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "obsidian";
import { DEFAULT_SETTINGS, LOG_LEVEL, ObsidianLiveSyncSettings, RemoteDBSettings } from "./lib/src/types";
import { DEFAULT_SETTINGS, LOG_LEVEL, ObsidianLiveSyncSettings, ConfigPassphraseStore, RemoteDBSettings } from "./lib/src/types";
import { path2id, id2path } from "./utils";
import { delay, versionNumberString2Number } from "./lib/src/utils";
import { delay } from "./lib/src/utils";
import { Semaphore } from "./lib/src/semaphore";
import { versionNumberString2Number } from "./lib/src/strbin";
import { Logger } from "./lib/src/logger";
import { checkSyncInfo, isCloudantURI } from "./lib/src/utils_couchdb.js";
import { testCrypt } from "./lib/src/e2ee_v2";
@@ -41,6 +43,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}
display(): void {
const { containerEl } = this;
let encrypt = this.plugin.settings.encrypt;
let passphrase = this.plugin.settings.passphrase;
let useDynamicIterationCount = this.plugin.settings.useDynamicIterationCount;
containerEl.empty();
@@ -133,6 +138,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
if (this.plugin.settings.syncOnFileOpen) return true;
if (this.plugin.settings.syncOnSave) return true;
if (this.plugin.settings.syncOnStart) return true;
if (this.plugin.settings.syncAfterMerge) return true;
if (this.plugin.localDatabase.syncStatus == "CONNECTED") return true;
if (this.plugin.localDatabase.syncStatus == "PAUSED") return true;
return false;
@@ -144,7 +150,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
new Setting(setupWizardEl)
.setName("Discard the existing configuration and set up")
.addButton((text) => {
text.setButtonText("Next").onClick(async () => {
text.setButtonText("Next").onClick(() => {
if (JSON.stringify(this.plugin.settings) != JSON.stringify(DEFAULT_SETTINGS)) {
this.plugin.localDatabase.closeReplication();
this.plugin.settings = { ...DEFAULT_SETTINGS };
@@ -170,6 +176,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.plugin.settings.syncOnSave = false;
this.plugin.settings.syncOnStart = false;
this.plugin.settings.syncOnFileOpen = false;
this.plugin.settings.syncAfterMerge = false;
this.plugin.localDatabase.closeReplication();
await this.plugin.saveSettings();
containerEl.addClass("isWizard");
@@ -226,7 +233,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
syncLive.forEach((e) => {
e.setDisabled(false).setTooltip("");
});
} else if (this.plugin.settings.syncOnFileOpen || this.plugin.settings.syncOnSave || this.plugin.settings.syncOnStart || this.plugin.settings.periodicReplication) {
} else if (this.plugin.settings.syncOnFileOpen || this.plugin.settings.syncOnSave || this.plugin.settings.syncOnStart || this.plugin.settings.periodicReplication || this.plugin.settings.syncAfterMerge) {
syncNonLive.forEach((e) => {
e.setDisabled(false).setTooltip("");
});
@@ -289,47 +296,78 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
)
);
new Setting(containerRemoteDatabaseEl)
const e2e = new Setting(containerRemoteDatabaseEl)
.setName("End to End Encryption")
.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) => {
toggle.setValue(encrypt).onChange(async (value) => {
if (inWizard) {
this.plugin.settings.encrypt = value;
passphrase.setDisabled(!value);
passphraseSetting.setDisabled(!value);
dynamicIteration.setDisabled(!value);
await this.plugin.saveSettings();
} else {
this.plugin.settings.workingEncrypt = value;
passphrase.setDisabled(!value);
encrypt = value;
passphraseSetting.setDisabled(!value);
dynamicIteration.setDisabled(!value);
await this.plugin.saveSettings();
markDirtyControl();
}
})
);
const passphrase = new Setting(containerRemoteDatabaseEl)
const markDirtyControl = () => {
passphraseSetting.controlEl.toggleClass("sls-item-dirty", passphrase != this.plugin.settings.passphrase);
e2e.controlEl.toggleClass("sls-item-dirty", encrypt != this.plugin.settings.encrypt);
dynamicIteration.controlEl.toggleClass("sls-item-dirty", useDynamicIterationCount != this.plugin.settings.useDynamicIterationCount)
}
const passphraseSetting = 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)
.setValue(passphrase)
.onChange(async (value) => {
if (inWizard) {
this.plugin.settings.passphrase = value;
await this.plugin.saveSettings();
} else {
this.plugin.settings.workingPassphrase = value;
passphrase = value;
await this.plugin.saveSettings();
markDirtyControl();
}
});
text.inputEl.setAttribute("type", "password");
});
passphrase.setDisabled(!this.plugin.settings.workingEncrypt);
passphraseSetting.setDisabled(!encrypt);
const dynamicIteration = new Setting(containerRemoteDatabaseEl)
.setName("Use dynamic iteration count (experimental)")
.setDesc("Balancing the encryption/decryption load against the length of the passphrase if toggled. (v0.17.5 or higher required)")
.addToggle((toggle) => {
toggle.setValue(useDynamicIterationCount)
.onChange(async (value) => {
if (inWizard) {
this.plugin.settings.useDynamicIterationCount = value;
await this.plugin.saveSettings();
} else {
useDynamicIterationCount = value;
await this.plugin.saveSettings();
markDirtyControl();
}
});
})
.setClass("wizardHidden");
dynamicIteration.setDisabled(!encrypt);
const checkWorkingPassphrase = async (): Promise<boolean> => {
const settingForCheck: RemoteDBSettings = {
...this.plugin.settings,
encrypt: this.plugin.settings.workingEncrypt,
passphrase: this.plugin.settings.workingPassphrase,
encrypt: encrypt,
passphrase: passphrase,
useDynamicIterationCount: useDynamicIterationCount,
};
console.dir(settingForCheck);
const db = await this.plugin.localDatabase.connectRemoteCouchDBWithSetting(settingForCheck, this.plugin.localDatabase.isMobile);
@@ -347,29 +385,32 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}
};
const applyEncryption = async (sendToServer: boolean) => {
if (this.plugin.settings.workingEncrypt && this.plugin.settings.workingPassphrase == "") {
if (encrypt && passphrase == "") {
Logger("If you enable encryption, you have to set the passphrase", LOG_LEVEL.NOTICE);
return;
}
if (this.plugin.settings.workingEncrypt && !(await testCrypt())) {
if (encrypt && !(await testCrypt())) {
Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.NOTICE);
return;
}
if (!(await checkWorkingPassphrase())) {
if (!(await checkWorkingPassphrase()) && !sendToServer) {
return;
}
if (!this.plugin.settings.workingEncrypt) {
this.plugin.settings.workingPassphrase = "";
if (!encrypt) {
passphrase = "";
}
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;
this.plugin.settings.syncAfterMerge = false;
this.plugin.settings.encrypt = encrypt;
this.plugin.settings.passphrase = passphrase;
this.plugin.settings.useDynamicIterationCount = useDynamicIterationCount;
await this.plugin.saveSettings();
markDirtyControl();
if (sendToServer) {
await this.plugin.initializeDatabase(true);
await this.plugin.markRemoteLocked();
@@ -390,7 +431,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setButtonText("Apply")
.setWarning()
.setDisabled(false)
.setClass("sls-btn-right")
.onClick(async () => {
await applyEncryption(true);
})
@@ -400,7 +440,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setButtonText("Apply w/o rebuilding")
.setWarning()
.setDisabled(false)
.setClass("sls-btn-right")
.onClick(async () => {
await applyEncryption(false);
})
@@ -413,7 +452,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.plugin.settings.syncOnSave = false;
this.plugin.settings.syncOnStart = false;
this.plugin.settings.syncOnFileOpen = false;
this.plugin.settings.syncAfterMerge = false;
await this.plugin.saveSettings();
applyDisplayEnabled();
@@ -448,7 +487,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setButtonText("Send")
.setWarning()
.setDisabled(false)
.setClass("sls-btn-left")
.onClick(async () => {
await rebuildDB("remoteOnly");
})
@@ -463,7 +501,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setButtonText("Rebuild")
.setWarning()
.setDisabled(false)
.setClass("sls-btn-left")
.onClick(async () => {
await rebuildDB("rebuildBothByThisDevice");
})
@@ -675,7 +712,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setButtonText("Next")
.setClass("mod-cta")
.setDisabled(false)
.onClick(async () => {
.onClick(() => {
if (!this.plugin.settings.encrypt) {
this.plugin.settings.passphrase = "";
}
@@ -696,7 +733,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setButtonText("Discard exist database and proceed")
.setDisabled(false)
.setWarning()
.onClick(async () => {
.onClick(() => {
if (!this.plugin.settings.encrypt) {
this.plugin.settings.passphrase = "";
}
@@ -738,7 +775,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setButtonText("Fetch")
.setWarning()
.setDisabled(false)
.setClass("sls-btn-left")
.onClick(async () => {
await rebuildDB("localOnly");
})
@@ -776,11 +812,28 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
button
.setButtonText("Next")
.setDisabled(false)
.onClick(async () => {
.onClick(() => {
changeDisplay("40");
})
);
containerLocalDatabaseEl.createEl("h3", {
text: sanitizeHTMLToDom(`Experimental`),
cls: "wizardHidden"
});
new Setting(containerLocalDatabaseEl)
.setName("Use new adapter")
.setDesc("This option is not compatible with a database made by older versions. Changing this configuration will fetch the remote database again.")
.setClass("wizardHidden")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.useIndexedDBAdapter).onChange(async (value) => {
this.plugin.settings.useIndexedDBAdapter = value;
await this.plugin.saveSettings();
await rebuildDB("localOnly");
})
);
addScreenElement("10", containerLocalDatabaseEl);
const containerGeneralSettingsEl = containerEl.createDiv();
containerGeneralSettingsEl.createEl("h3", { text: "General Settings" });
@@ -830,6 +883,14 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
});
text.inputEl.setAttribute("type", "number");
});
new Setting(containerGeneralSettingsEl)
.setName("Monitor changes to hidden files and plugin")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.watchInternalFileChanges).onChange(async (value) => {
this.plugin.settings.watchInternalFileChanges = value;
await this.plugin.saveSettings();
})
);
addScreenElement("20", containerGeneralSettingsEl);
@@ -932,7 +993,17 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
applyDisplayEnabled();
})
)
),
new Setting(containerSyncSettingEl)
.setName("Sync after merging file")
.setDesc("Sync automatically after merging files")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.syncAfterMerge).onChange(async (value) => {
this.plugin.settings.syncAfterMerge = value;
await this.plugin.saveSettings();
applyDisplayEnabled();
})
),
);
new Setting(containerSyncSettingEl)
@@ -974,7 +1045,24 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
})
);
new Setting(containerSyncSettingEl)
.setName("Disable sensible auto merging on markdown files")
.setDesc("If this switch is turned on, a merge dialog will be displayed, even if the sensible-merge is possible automatically. (Turn on to previous behavior)")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.disableMarkdownAutoMerge).onChange(async (value) => {
this.plugin.settings.disableMarkdownAutoMerge = value;
await this.plugin.saveSettings();
})
);
new Setting(containerSyncSettingEl)
.setName("Write documents after synchronization even if they have conflict")
.setDesc("Turn on to previous behavior")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.writeDocumentsIfConflicted).onChange(async (value) => {
this.plugin.settings.writeDocumentsIfConflicted = value;
await this.plugin.saveSettings();
})
);
new Setting(containerSyncSettingEl)
.setName("Sync hidden files")
@@ -984,14 +1072,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
})
);
new Setting(containerSyncSettingEl)
.setName("Monitor changes to internal files")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.watchInternalFileChanges).onChange(async (value) => {
this.plugin.settings.watchInternalFileChanges = value;
await this.plugin.saveSettings();
})
);
new Setting(containerSyncSettingEl)
.setName("Scan for hidden files before replication")
.setDesc("This configuration will be ignored if monitoring changes is enabled.")
@@ -1063,7 +1144,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setButtonText("Touch")
.setWarning()
.setDisabled(false)
.setClass("sls-btn-left")
.onClick(async () => {
const filesAll = await this.plugin.scanInternalFiles();
const targetFiles = await this.plugin.filterTargetFiles(filesAll);
@@ -1137,8 +1217,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.customChunkSize + "")
.onChange(async (value) => {
let v = Number(value);
if (isNaN(v) || v < 100) {
v = 100;
if (isNaN(v) || v < 1) {
v = 1;
}
this.plugin.settings.customChunkSize = v;
await this.plugin.saveSettings();
@@ -1199,6 +1279,52 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
text.inputEl.setAttribute("type", "number");
});
new Setting(containerSyncSettingEl)
.setName("Use timeouts instead of heartbeats")
.setDesc("If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.")
.addToggle((toggle) => {
toggle
.setValue(this.plugin.settings.useTimeouts)
.onChange(async (value) => {
this.plugin.settings.useTimeouts = value;
await this.plugin.saveSettings();
})
return toggle;
}
);
new Setting(containerSyncSettingEl)
.setName("A number of hashes to be cached")
.setDesc("")
.addText((text) => {
text.setPlaceholder("")
.setValue(this.plugin.settings.hashCacheMaxCount + "")
.onChange(async (value) => {
let v = Number(value);
if (isNaN(v) || v < 10) {
v = 10;
}
this.plugin.settings.hashCacheMaxCount = v;
await this.plugin.saveSettings();
});
text.inputEl.setAttribute("type", "number");
});
new Setting(containerSyncSettingEl)
.setName("The total length of hashes to be cached")
.setDesc("(Mega chars)")
.addText((text) => {
text.setPlaceholder("")
.setValue(this.plugin.settings.hashCacheMaxAmount + "")
.onChange(async (value) => {
let v = Number(value);
if (isNaN(v) || v < 1) {
v = 1;
}
this.plugin.settings.hashCacheMaxAmount = v;
await this.plugin.saveSettings();
});
text.inputEl.setAttribute("type", "number");
});
addScreenElement("30", containerSyncSettingEl);
const containerMiscellaneousEl = containerEl.createDiv();
containerMiscellaneousEl.createEl("h3", { text: "Miscellaneous" });
@@ -1238,6 +1364,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.plugin.settings.syncOnSave = false;
this.plugin.settings.syncOnStart = false;
this.plugin.settings.syncOnFileOpen = false;
this.plugin.settings.syncAfterMerge = false;
if (currentPreset == "LIVESYNC") {
this.plugin.settings.liveSync = true;
Logger("Synchronization setting configured as LiveSync.", LOG_LEVEL.NOTICE);
@@ -1247,6 +1374,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.plugin.settings.syncOnSave = false;
this.plugin.settings.syncOnStart = true;
this.plugin.settings.syncOnFileOpen = true;
this.plugin.settings.syncAfterMerge = true;
Logger("Synchronization setting configured as Periodic sync with batch database update.", LOG_LEVEL.NOTICE);
} else {
Logger("All synchronization disabled.", LOG_LEVEL.NOTICE);
@@ -1275,6 +1403,45 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
})
);
const passphrase_options: Record<ConfigPassphraseStore, string> = {
"": "Default",
LOCALSTORAGE: "Use a custom passphrase",
ASK_AT_LAUNCH: "Ask an passphrase at every launch",
}
new Setting(containerMiscellaneousEl)
.setName("Encrypting sensitive configuration items")
.addDropdown((dropdown) =>
dropdown
.addOptions(passphrase_options)
.setValue(this.plugin.settings.configPassphraseStore)
.onChange(async (value) => {
this.plugin.settings.configPassphraseStore = value as ConfigPassphraseStore;
this.plugin.usedPassphrase = "";
confPassphraseSetting.setDisabled(this.plugin.settings.configPassphraseStore != "LOCALSTORAGE");
await this.plugin.saveSettings();
})
)
.setClass("wizardHidden");
const confPassphrase = localStorage.getItem("ls-setting-passphrase") || "";
const confPassphraseSetting = new Setting(containerMiscellaneousEl)
.setName("Passphrase of sensitive configuration items")
.setDesc("This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.")
.addText((text) => {
text.setPlaceholder("")
.setValue(confPassphrase)
.onChange(async (value) => {
this.plugin.usedPassphrase = "";
localStorage.setItem("ls-setting-passphrase", value);
await this.plugin.saveSettings();
markDirtyControl();
});
text.inputEl.setAttribute("type", "password");
})
.setClass("wizardHidden");
confPassphraseSetting.setDisabled(this.plugin.settings.configPassphraseStore != "LOCALSTORAGE");
const infoApply = containerMiscellaneousEl.createEl("div", { text: `To finish setup, please select one of the presets` });
infoApply.addClass("op-warn-info");
infoApply.addClass("wizardOnly")
@@ -1288,7 +1455,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
new Setting(containerHatchEl)
.setName("Make report to inform the issue")
.setDesc("Verify and repair all files and update database without restoring")
.addButton((button) =>
button
.setButtonText("Make report")
@@ -1317,7 +1483,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI) ? "cloudant" : "self-hosted";
pluginConfig.couchDB_USER = REDACTED;
pluginConfig.passphrase = REDACTED;
pluginConfig.workingPassphrase = REDACTED;
pluginConfig.encryptedPassphrase = REDACTED;
pluginConfig.encryptedCouchDBConnection = REDACTED;
const msgConfig = `----remote config----
${stringifyYaml(responseConfig)}
@@ -1368,21 +1535,28 @@ ${stringifyYaml(pluginConfig)}`;
.setDisabled(false)
.setWarning()
.onClick(async () => {
const semaphore = Semaphore(10);
const files = this.app.vault.getFiles();
Logger("Verify and repair all files started", LOG_LEVEL.NOTICE, "verify");
// const notice = NewNotice("", 0);
let i = 0;
for (const file of files) {
i++;
Logger(`Update into ${file.path}`);
Logger(`${i}/${files.length}\n${file.path}`, LOG_LEVEL.NOTICE, "verify");
const processes = files.map(e => (async (file) => {
const releaser = await semaphore.acquire(1, "verifyAndRepair");
try {
await this.plugin.updateIntoDB(file);
Logger(`UPDATE DATABASE ${file.path}`);
await this.plugin.updateIntoDB(file, false, null, true);
i++;
Logger(`${i}/${files.length}\n${file.path}`, LOG_LEVEL.NOTICE, "verify");
} catch (ex) {
Logger("could not update:");
i++;
Logger(`Error while verifyAndRepair`, LOG_LEVEL.NOTICE);
Logger(ex);
} finally {
releaser();
}
}
)(e));
await Promise.all(processes);
Logger("done", LOG_LEVEL.NOTICE, "verify");
})
);
@@ -1396,6 +1570,15 @@ ${stringifyYaml(pluginConfig)}`;
await this.plugin.saveSettings();
})
);
new Setting(containerHatchEl)
.setName("Write logs into the file")
.setDesc("Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.writeLogToTheFile).onChange(async (value) => {
this.plugin.settings.writeLogToTheFile = value;
await this.plugin.saveSettings();
})
);
new Setting(containerHatchEl)
.setName("Discard local database to reset or uninstall Self-hosted LiveSync")
@@ -1440,7 +1623,7 @@ ${stringifyYaml(pluginConfig)}`;
new Setting(containerPluginSettings)
.setName("Scan plugins periodically")
.setDesc("Scan plugins every 1 minute.")
.setDesc("Scan plugins every 1 minute. This configuration will be ignored if monitoring changes of hidden files has been enabled.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => {
this.plugin.settings.autoSweepPluginsPeriodic = value;

View File

@@ -2,7 +2,7 @@
import ObsidianLiveSyncPlugin from "./main";
import { onMount } from "svelte";
import { DevicePluginList, PluginDataEntry } from "./types";
import { versionNumberString2Number } from "./lib/src/utils";
import { versionNumberString2Number } from "./lib/src/strbin";
type JudgeResult = "" | "NEWER" | "EVEN" | "EVEN_BUT_DIFFERENT" | "OLDER" | "REMOTE_ONLY";

View File

@@ -52,14 +52,14 @@ export class InputStringDialog extends Modal {
const { contentEl } = this;
contentEl.createEl("h1", { text: this.title });
new Setting(contentEl).setName(this.key).addText((text) =>
// For enter to submit
const formEl = contentEl.createEl("form");
new Setting(formEl).setName(this.key).addText((text) =>
text.onChange((value) => {
this.result = value;
})
);
new Setting(contentEl).addButton((btn) =>
new Setting(formEl).addButton((btn) =>
btn
.setButtonText("Ok")
.setCta()

Submodule src/lib updated: 85bb3556ba...fbb3fcd8b4

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import { PluginManifest } from "obsidian";
import { DatabaseEntry } from "./lib/src/types";
import { PluginManifest, TFile } from "obsidian";
import { DatabaseEntry, EntryBody } from "./lib/src/types";
export interface PluginDataEntry extends DatabaseEntry {
deviceVaultName: string;
@@ -30,3 +30,20 @@ export interface InternalFileInfo {
size: number;
deleted?: boolean;
}
export interface FileInfo {
path: string;
mtime: number;
ctime: number;
size: number;
deleted?: boolean;
file: TFile;
}
export type queueItem = {
entry: EntryBody;
missingChildren: string[];
timeout?: number;
done?: boolean;
warned?: boolean;
};

View File

@@ -1,6 +1,8 @@
import { normalizePath } from "obsidian";
import { DataWriteOptions, normalizePath, TFile, Platform } from "obsidian";
import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid } from "./lib/src/path";
import { path2id_base, id2path_base } from "./lib/src/utils";
import { Logger } from "./lib/src/logger";
import { LOG_LEVEL } from "./lib/src/types";
// For backward compatibility, using the path for determining id.
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
@@ -72,4 +74,220 @@ export function retrieveMemoObject<T>(key: string): T | false {
}
export function disposeMemoObject(key: string) {
delete memos[key];
}
export function isSensibleMargeApplicable(path: string) {
if (path.endsWith(".md")) return true;
return false;
}
export function isObjectMargeApplicable(path: string) {
if (path.endsWith(".canvas")) return true;
if (path.endsWith(".json")) return true;
return false;
}
export function tryParseJSON(str: string, fallbackValue?: any) {
try {
return JSON.parse(str);
} catch (ex) {
return fallbackValue;
}
}
const MARK_OPERATOR = `\u{0001}`;
const MARK_DELETED = `${MARK_OPERATOR}__DELETED`;
const MARK_ISARRAY = `${MARK_OPERATOR}__ARRAY`;
const MARK_SWAPPED = `${MARK_OPERATOR}__SWAP`;
function unorderedArrayToObject(obj: Array<any>) {
return obj.map(e => ({ [e.id as string]: e })).reduce((p, c) => ({ ...p, ...c }), {})
}
function objectToUnorderedArray(obj: object) {
const entries = Object.entries(obj);
if (entries.some(e => e[0] != e[1]?.id)) throw new Error("Item looks like not unordered array")
return entries.map(e => e[1]);
}
function generatePatchUnorderedArray(from: Array<any>, to: Array<any>) {
if (from.every(e => typeof (e) == "object" && ("id" in e)) && to.every(e => typeof (e) == "object" && ("id" in e))) {
const fObj = unorderedArrayToObject(from);
const tObj = unorderedArrayToObject(to);
const diff = generatePatchObj(fObj, tObj);
if (Object.keys(diff).length > 0) {
return { [MARK_ISARRAY]: diff };
} else {
return {};
}
}
return { [MARK_SWAPPED]: to };
}
export function generatePatchObj(from: Record<string | number | symbol, any>, to: Record<string | number | symbol, any>) {
const entries = Object.entries(from);
const tempMap = new Map<string | number | symbol, any>(entries);
const ret = {} as Record<string | number | symbol, any>;
const newEntries = Object.entries(to);
for (const [key, value] of newEntries) {
if (!tempMap.has(key)) {
//New
ret[key] = value;
tempMap.delete(key);
} else {
//Exists
const v = tempMap.get(key);
if (typeof (v) !== typeof (value) || (Array.isArray(v) !== Array.isArray(value))) {
//if type is not match, replace completely.
ret[key] = { [MARK_SWAPPED]: value };
} else {
if (typeof (v) == "object" && typeof (value) == "object" && !Array.isArray(v) && !Array.isArray(value)) {
const wk = generatePatchObj(v, value);
if (Object.keys(wk).length > 0) ret[key] = wk;
} else if (typeof (v) == "object" && typeof (value) == "object" && Array.isArray(v) && Array.isArray(value)) {
const wk = generatePatchUnorderedArray(v, value);
if (Object.keys(wk).length > 0) ret[key] = wk;
} else if (typeof (v) != "object" && typeof (value) != "object") {
if (JSON.stringify(tempMap.get(key)) !== JSON.stringify(value)) {
ret[key] = value;
}
} else {
if (JSON.stringify(tempMap.get(key)) !== JSON.stringify(value)) {
ret[key] = { [MARK_SWAPPED]: value };
}
}
}
tempMap.delete(key);
}
}
//Not used item, means deleted one
for (const [key,] of tempMap) {
ret[key] = MARK_DELETED
}
return ret;
}
export function applyPatch(from: Record<string | number | symbol, any>, patch: Record<string | number | symbol, any>) {
const ret = from;
const patches = Object.entries(patch);
for (const [key, value] of patches) {
if (value == MARK_DELETED) {
delete ret[key];
continue;
}
if (typeof (value) == "object") {
if (MARK_SWAPPED in value) {
ret[key] = value[MARK_SWAPPED];
continue;
}
if (MARK_ISARRAY in value) {
if (!(key in ret)) ret[key] = [];
if (!Array.isArray(ret[key])) {
throw new Error("Patch target type is mismatched (array to something)");
}
const orgArrayObject = unorderedArrayToObject(ret[key]);
const appliedObject = applyPatch(orgArrayObject, value[MARK_ISARRAY]);
const appliedArray = objectToUnorderedArray(appliedObject);
ret[key] = [...appliedArray];
} else {
if (!(key in ret)) {
ret[key] = value;
continue;
}
ret[key] = applyPatch(ret[key], value);
}
} else {
ret[key] = value;
}
}
return ret;
}
export function mergeObject(
objA: Record<string | number | symbol, any>,
objB: Record<string | number | symbol, any>
) {
const newEntries = Object.entries(objB);
const ret: any = { ...objA };
if (
typeof objA !== typeof objB ||
Array.isArray(objA) !== Array.isArray(objB)
) {
return objB;
}
for (const [key, v] of newEntries) {
if (key in ret) {
const value = ret[key];
if (
typeof v !== typeof value ||
Array.isArray(v) !== Array.isArray(value)
) {
//if type is not match, replace completely.
ret[key] = v;
} else {
if (
typeof v == "object" &&
typeof value == "object" &&
!Array.isArray(v) &&
!Array.isArray(value)
) {
ret[key] = mergeObject(v, value);
} else if (
typeof v == "object" &&
typeof value == "object" &&
Array.isArray(v) &&
Array.isArray(value)
) {
ret[key] = [...new Set([...v, ...value])];
} else {
ret[key] = v;
}
}
} else {
ret[key] = v;
}
}
return Object.entries(ret)
.sort()
.reduce((p, [key, value]) => ({ ...p, [key]: value }), {});
}
export function flattenObject(obj: Record<string | number | symbol, any>, path: string[] = []): [string, any][] {
if (typeof (obj) != "object") return [[path.join("."), obj]];
if (Array.isArray(obj)) return [[path.join("."), JSON.stringify(obj)]];
const e = Object.entries(obj);
const ret = []
for (const [key, value] of e) {
const p = flattenObject(value, [...path, key]);
ret.push(...p);
}
return ret;
}
export function modifyFile(file: TFile, data: string | ArrayBuffer, options?: DataWriteOptions) {
if (typeof (data) === "string") {
return app.vault.modify(file, data, options);
} else {
return app.vault.modifyBinary(file, data, options);
}
}
export function createFile(path: string, data: string | ArrayBuffer, options?: DataWriteOptions): Promise<TFile> {
if (typeof (data) === "string") {
return app.vault.create(path, data, options);
} else {
return app.vault.createBinary(path, data, options);
}
}
export function isValidPath(filename: string) {
if (Platform.isDesktop) {
// if(Platform.isMacOS) return isValidFilenameInDarwin(filename);
if (process.platform == "darwin") return isValidFilenameInDarwin(filename);
if (process.platform == "linux") return isValidFilenameInLinux(filename);
return isValidFilenameInWidows(filename);
}
if (Platform.isAndroidApp) return isValidFilenameInAndroid(filename);
if (Platform.isIosApp) return isValidFilenameInDarwin(filename);
//Fallback
Logger("Could not determine platform for checking filename", LOG_LEVEL.VERBOSE);
return isValidFilenameInWidows(filename);
}

View File

@@ -85,13 +85,6 @@
} */
.sls-btn-left {
padding-right: 4px;
}
.sls-btn-right {
padding-left: 4px;
}
.sls-header-button {
margin-left: 2em;
@@ -106,8 +99,9 @@
}
.CodeMirror-wrap::before,
.cm-s-obsidian>.cm-editor::before {
content: var(--slsmessage);
.cm-s-obsidian>.cm-editor::before,
.canvas-wrapper::before {
content: attr(data-log);
text-align: right;
white-space: pre-wrap;
position: absolute;
@@ -122,6 +116,10 @@
filter: grayscale(100%);
}
.canvas-wrapper::before {
right: 48px;
}
.CodeMirror-wrap::before {
right: 0px;
}
@@ -249,4 +247,8 @@ div.sls-setting-menu-btn {
.sls-setting:not(.isWizard) .wizardOnly {
display: none;
}
.sls-item-dirty::before {
content: "✏";
}

View File

@@ -1,55 +1,70 @@
### 0.16.0
- Now hidden files need not be scanned. Changes will be detected automatically.
- If you want it to back to its previous behaviour, please disable `Monitor changes to internal files`.
- Due to using an internal API, this feature may become unusable with a major update. If this happens, please disable this once.
### 0.17.0
- 0.17.0 has no surfaced changes but the design of saving chunks has been changed. They have compatibility but changing files after upgrading makes different chunks than before 0.16.x.
Please rebuild databases once if you have been worried about storage usage.
- Improved:
- Splitting markdown
- Saving chunks
- Changed:
- Chunk ID numbering rules
#### Minors
- __0.17.1 to 0.17.15 has been moved into `update_old.md`__
- 0.16.1 Added missing log updates.
- 0.16.2 Fixed many problems caused by combinations of `Sync On Save` and the tracking logic that changed at 0.15.6.
- 0.16.3
- Fixed detection of IBM Cloudant (And if there are some issues, be fixed automatically).
- A configuration information reporting tool has been implemented.
- 0.16.4 Fixed detection failure. Please set the `Chunk size` again when using a self-hosted database.
- 0.16.5
- Fixed
- Conflict detection and merging now be able to treat deleted files.
- Logs while the boot-up sequence has been tidied up.
- Fixed incorrect log entries.
- New Feature
- The feature of automatically deleting old expired metadata has been implemented.
We can configure it in `Delete old metadata of deleted files on start-up` in the `General Settings` pane.
- 0.16.6
- Fixed
- Automatic (temporary) batch size adjustment has been restored to work correctly.
- Chunk splitting has been backed to the previous behaviour for saving them correctly.
- 0.17.16:
- Improved:
- Plugins and their settings no longer need scanning if changes are monitored.
- Now synchronising plugins and their settings are performed parallelly and faster.
- We can place `redflag2.md` to rebuild the database automatically while the boot sequence.
- Experimental:
- We can use a new adapter on PouchDB. This will make us smoother.
- Note: Not compatible with the older version.
- Fixed:
- The default batch size is smaller again.
- Plugins and their setting can be synchronised again.
- Hidden files and plugins are correctly scanned while rebuilding.
- Files with the name started `_` are also being performed conflict-checking.
- 0.17.17
- Fixed: Now we can merge JSON files even if we failed to compare items like null.
- 0.17.18
- Fixed: Fixed lack of error handling.
- 0.17.19
- Fixed: Error reporting has been ensured.
- 0.17.20
- Improved: Changes of hidden files will be notified to Obsidian.
- 0.17.21
- Fixed: Skip patterns now handle capital letters.
- Improved
- Corrupted chunks will be detected automatically.
- Now on the case-insensitive system, `aaa.md` and `AAA.md` will be treated as the same file or path at applying changesets.
- 0.16.7 Nothing has been changed except toolsets, framework library, and as like them. Please inform me if something had been getting strange!
- 0.16.8 Now we can synchronise without `bad_request:invalid UTF-8 JSON` even while end-to-end encryption has been disabled.
Note:
Before 0.16.5, LiveSync had some issues making chunks. In this case, synchronisation had became been always failing after a corrupted one should be made. After 0.16.6, the corrupted chunk is automatically detected. Sorry for troubling you but please do `rebuild everything` when this plug-in notified so.
### 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.15.8 Fixed bug about deleting empty directory, weird behaviour on boot-sequence on mobile devices.
- 0.15.9 Improved chunk retrieving, now chunks are retrieved in batch on continuous requests.
- 0.15.10 Fixed:
- The boot sequence has been corrected and now boots smoothly.
- Auto applying of batch save will be processed earlier than before.
- New configuration to avoid exceeding throttle capacity.
- We have been grateful to @karasevm!
- The conflicted `data.json` is no longer merged automatically.
- This behaviour is not configurable, unlike the `Use newer file if conflicted` of normal files.
- 0.17.22
- Fixed:
- Now hidden files will not be synchronised while we are not configured.
- Some processes could start without waiting for synchronisation to complete, but now they will wait for.
- Improved
- Now, by placing `redflag3.md`, we can discard the local database and fetch again.
- The document has been updated! Thanks to @hilsonp!
- 0.17.23
- Improved:
- Now we can preserve the logs into the file.
- Note: This option will be enabled automatically also when we flagging a red flag.
- File names can now be made platform-appropriate.
- Refactored:
- Some redundant implementations have been sorted out.
- 0.17.24
- New feature:
- If any conflicted files have been left, they will be reported.
- Fixed:
- Now the name of the conflicting file is shown on the conflict-resolving dialogue.
- Hidden files are now able to be merged again.
- No longer error caused at plug-in being loaded.
- Improved:
- Caching chunks are now limited in total size of cached chunks.
- 0.17.25
- Fixed:
- Now reading error will be reported.
... To continue on to `updates_old.md`.

View File

@@ -1,3 +1,128 @@
### 0.17.0
- 0.17.0 has no surfaced changes but the design of saving chunks has been changed. They have compatibility but changing files after upgrading makes different chunks than before 0.16.x.
Please rebuild databases once if you have been worried about storage usage.
- Improved:
- Splitting markdown
- Saving chunks
- Changed:
- Chunk ID numbering rules
#### Minors
- 0.17.1
- Fixed: Now we can verify and repair the database.
- Refactored inside.
- 0.17.2
- New feature
- We can merge conflicted documents automatically if sensible.
- Fixed
- Writing to the storage will be pended while they have conflicts after replication.
- 0.17.3
- Now we supported canvas! And conflicted JSON files are also synchronised with merging its content if they are obvious.
- 0.17.4
- Canvases are now treated as a sort of plain text file. now we transfer only the metadata and chunks that have differences.
- 0.17.5 Now `read chunks online` had been fixed, and a new feature: `Use dynamic iteration count` to reduce the load on encryption/decryption.
Note: `Use dynamic iteration count` is not compatible with earlier versions.
- 0.17.6 Now our renamed/deleted files have been surely deleted again.
- 0.17.7
- Fixed:
- Fixed merging issues.
- Fixed button styling.
- Changed:
- Conflict checking on synchronising has been enabled for every note in default.
- 0.17.8
- Improved: Performance improved. Prebuilt PouchDB is no longer used.
- Fixed: Merging hidden files is also fixed.
- New Feature: Now we can synchronise automatically after merging conflicts.
- 0.17.9
- Fixed: Conflict merge of internal files is no longer broken.
- Improved: Smoother status display inside the editor.
- 0.17.10
- Fixed: Large file synchronising has been now addressed!
Note: When synchronising large files, we have to set `Chunk size` to lower than 50, disable `Read chunks online`, `Batch size` should be set 50-100, and `Batch limit` could be around 20.
- 0.17.11
- Fixed:
- Performance improvement
- Now `Chunk size` can be set to under one hundred.
- New feature:
- The number of transfers required before replication stabilises is now displayed.
- 0.17.12: Skipped.
- 0.17.13
- Fixed: Document history is now displayed again.
- Reorganised: Many files have been refactored.
- 0.17.14: Skipped.
- 0.17.15
- Improved:
- Confidential information has no longer stored in data.json as is.
- Synchronising progress has been shown in the notification.
- We can commit passphrases with a keyboard.
- Configuration which had not been saved yet is marked now.
- Now the filename is shown on the Conflict resolving dialog
- Fixed:
- Hidden files have been synchronised again.
- Rename of files has been fixed again.
And, minor changes have been included.
### 0.16.0
- Now hidden files need not be scanned. Changes will be detected automatically.
- If you want it to back to its previous behaviour, please disable `Monitor changes to internal files`.
- Due to using an internal API, this feature may become unusable with a major update. If this happens, please disable this once.
#### Minors
- 0.16.1 Added missing log updates.
- 0.16.2 Fixed many problems caused by combinations of `Sync On Save` and the tracking logic that changed at 0.15.6.
- 0.16.3
- Fixed detection of IBM Cloudant (And if there are some issues, be fixed automatically).
- A configuration information reporting tool has been implemented.
- 0.16.4 Fixed detection failure. Please set the `Chunk size` again when using a self-hosted database.
- 0.16.5
- Fixed
- Conflict detection and merging now be able to treat deleted files.
- Logs while the boot-up sequence has been tidied up.
- Fixed incorrect log entries.
- New Feature
- The feature of automatically deleting old expired metadata has been implemented.
We can configure it in `Delete old metadata of deleted files on start-up` in the `General Settings` pane.
- 0.16.6
- Fixed
- Automatic (temporary) batch size adjustment has been restored to work correctly.
- Chunk splitting has been backed to the previous behaviour for saving them correctly.
- Improved
- Corrupted chunks will be detected automatically.
- Now on the case-insensitive system, `aaa.md` and `AAA.md` will be treated as the same file or path at applying changesets.
- 0.16.7 Nothing has been changed except toolsets, framework library, and as like them. Please inform me if something had been getting strange!
- 0.16.8 Now we can synchronise without `bad_request:invalid UTF-8 JSON` even while end-to-end encryption has been disabled.
Note:
Before 0.16.5, LiveSync had some issues making chunks. In this case, synchronisation had became been always failing after a corrupted one should be made. After 0.16.6, the corrupted chunk is automatically detected. Sorry for troubling you but please do `rebuild everything` when this plug-in notified so.
### 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.15.8 Fixed bug about deleting empty directory, weird behaviour on boot-sequence on mobile devices.
- 0.15.9 Improved chunk retrieving, now chunks are retrieved in batch on continuous requests.
- 0.15.10 Fixed:
- The boot sequence has been corrected and now boots smoothly.
- Auto applying of batch save will be processed earlier than before.
### 0.14.1
- The target selecting filter was implemented.
Now we can set what files are synchronised by regular expression.