Compare commits

...

62 Commits

Author SHA1 Message Date
vorotamoroz
ee9364310d bump 2024-02-20 09:36:43 +00:00
vorotamoroz
86b9695bc2 Fixed:
- No longer deleted hidden files were ignored.
- The document history dialogue is now able to process the deleted revisions.
- Deletion of a hidden file is now surely performed even if the file is already conflicted.
2024-02-20 09:32:48 +00:00
vorotamoroz
e05f8771b9 bump 2024-02-20 05:15:48 +00:00
vorotamoroz
65619c2478 Fixed:
- Fixed a problem with synchronisation taking a long time to start in some cases.
- Now we can disable E2EE encryption.
Improved:
- `Setup Wizard` is now more clear.
- `Minimal Setup` is now more simple.
- Self-hosted LiveSync now be able to use even if there are vaults with the same name.
- Now Self-hosted LiveSync waits until set-up is complete.
- Show reload prompts when possibly recommended while settings.
New feature:
- A guidance dialogue prompting for settings will be shown after the installation.
Changed
- Some setting names has been changed
2024-02-20 05:13:53 +00:00
vorotamoroz
1552fa9d9e Merge branch 'main' of https://github.com/vrtmrz/obsidian-livesync 2024-02-15 08:36:49 +00:00
vorotamoroz
767f12b52f Rewrite 2024-02-15 08:35:12 +00:00
vorotamoroz
4071ba120e Update troubleshooting.md 2024-02-15 13:56:15 +09:00
vorotamoroz
2c0e3ba01c update faq 2024-02-15 04:38:31 +00:00
vorotamoroz
90adf06830 Merge branch 'main' of https://github.com/vrtmrz/obsidian-livesync 2024-02-15 04:04:06 +00:00
vorotamoroz
cf8e7ff6ca bump 2024-02-15 04:04:01 +00:00
vorotamoroz
95c3ff5043 Update common lib. 2024-02-15 04:02:28 +00:00
vorotamoroz
7ea3515801 Fixed:
- Some description of settings have been refined
New feature:
- TroubleShooting is now shown in the setting dialogue.
2024-02-15 04:02:20 +00:00
vorotamoroz
f866981a8a Update troubleshooting.md 2024-02-14 17:38:52 +09:00
vorotamoroz
8f36d6f893 fix minor layout 2024-02-09 03:20:31 +00:00
vorotamoroz
6dd86e9392 modify minor layouts. 2024-02-09 03:16:33 +00:00
vorotamoroz
d22716bef0 make more clear documents. 2024-02-09 03:13:03 +00:00
vorotamoroz
5d9baec5e4 update colab note 2024-02-07 10:17:45 +00:00
vorotamoroz
27d71ca2fb New utilities. 2024-02-06 11:03:51 +00:00
vorotamoroz
c024ed13d3 Refining the content. 2024-02-06 11:02:48 +00:00
vorotamoroz
b9527ccab0 bump 2024-01-30 17:31:52 +00:00
vorotamoroz
fa3aa2702c Fixed:
- Now the result of conflict resolution could be surely written into the storage.
- Deleted files can be handled correctly again in the history dialogue and conflict dialogue.
- Some wrong log messages were fixed.
- Change handling now has become more stable.
- Some event handling became to be safer.
Improved:
- Dumping document information shows conflicts and revisions.
- The timestamp-only differences can be surely cached.
- Timestamp difference detection can be rounded by two seconds.
Refactored:
- A bit of organisation to write the test.
2024-01-30 17:31:02 +00:00
vorotamoroz
93e7cbb133 bump. 2024-01-29 08:41:03 +00:00
vorotamoroz
716ae32e02 Fixed:
- Deletion of files is now reliably synchronised.
2024-01-29 08:40:41 +00:00
vorotamoroz
d6d8cbcf5a bump 2024-01-29 07:57:02 +00:00
vorotamoroz
efd348b266 Fixed:
- No longer detects storage changes which have been caused by Self-hosted LiveSync itself.
- Setting sync file will be detected only if it has been configured now.
  - And its log will be shown only while the verbose log is enabled.
- Customisation file enumeration has got less blingy.
Fixed and improved:
- In-editor-status is now shown in the following areas:
  - Note editing pane (Source mode and live-preview mode).
  - New tab pane.
  - Canvas pane.
2024-01-29 07:56:02 +00:00
vorotamoroz
8969b1800a bump 2024-01-24 08:53:00 +00:00
vorotamoroz
2c8e026e29 Fixed:
- Now the results of resolving conflicts are surely synchronised.
Modified:
- Some setting items got new clear names.
New feature:
- We can limit the synchronising files by their size.
- Now the settings could be stored in a specific markdown file to synchronise or switch it
- Customisation of the obsoleted device is now able to be deleted at once.
2024-01-24 08:52:47 +00:00
vorotamoroz
a6c27eab3d Merge pull request #367 from calvinbui/patch-1
Skip workspace-mobile.json for cross-platform sync
2024-01-24 15:55:50 +09:00
vorotamoroz
9b5c57d540 Merge pull request #336 from toon159/patch-1
Lower payload size limit and batch limit from 10 to 2
2024-01-22 13:03:29 +09:00
Calvin Bui
c251c596e8 Skip workspace-mobile.json for cross-platform sync 2024-01-18 13:23:48 +11:00
vorotamoroz
61188cfaef bump 2024-01-16 08:36:37 +00:00
vorotamoroz
97d944fd75 New feature:
- We can perform automatic conflict resolution for inactive files, and postpone only manual ones by `Postpone manual resolution of inactive files`.
- Now we can see the image in the document history dialogue.
  - We can see the difference of the image, in the document history dialogue.
	- And also we can highlight differences.

Improved:
- Hidden file sync has been stabilised.
- Now automatically reloads the conflict-resolution dialogue when new conflicted revisions have arrived.

Fixed:
- No longer periodic process runs after unloading the plug-in.
- Now the modification of binary files is surely stored in the storage.
2024-01-16 08:32:43 +00:00
vorotamoroz
d3dc1e7328 Minor fix and refine the readme 2024-01-12 10:29:18 +00:00
vorotamoroz
45304af369 bump 2024-01-12 09:38:57 +00:00
vorotamoroz
7f422d58f2 - Refined:
- Task scheduling logics has been rewritten.
  - Possibly many bugs and fragile behaviour has been fixed
- Fixed:
  - Remote-chunk-fetching now works with keeping request intervals
- New feature:
  - We can show only the icons in the editor.
2024-01-12 09:36:49 +00:00
vorotamoroz
c2491fdfad bump 2023-12-11 12:55:01 +09:00
vorotamoroz
06a6e391e8 Fixed for change detection bug. 2023-12-11 12:53:50 +09:00
vorotamoroz
f99475f6b7 bump 2023-12-11 12:46:23 +09:00
vorotamoroz
109fc00b9d Fixed
- Now ID of the documents is shown in the log with the first 8 letters.
2023-12-11 12:45:40 +09:00
vorotamoroz
c071d822e1 - Improved:
- Now all revisions will be shown only its first a few letters.
- Fixed:
  - Check before modifying files has been implemented.
  - Content change detection has been improved.
2023-12-11 12:22:17 +09:00
vorotamoroz
d2de5b4710 bump 2023-12-04 19:39:47 +09:00
vorotamoroz
cf5ecd8922 Implemented:
- Now we can use SHA1 for hash function as fallback.
2023-12-04 19:39:04 +09:00
vorotamoroz
b337a05b5a bump 2023-11-27 07:13:15 +00:00
vorotamoroz
9ea6bee9d1 - Fixed:
- No longer files are broken while rebuilding.
    - Now, Large binary files can be written correctly on a mobile platform.
    - Any decoding errors now make zero-byte files.
  - Modified:
    - All files are processed sequentially for each.
2023-11-27 06:55:55 +00:00
vorotamoroz
9747c26d50 bump 2023-11-25 02:22:26 +09:00
vorotamoroz
bb4b764586 - Fixed:
- No more infinity loops on larger files.
    - Show message on decode error.
  - Refactored:
    - Fixed to avoid obsolete global variables.
2023-11-25 02:21:44 +09:00
vorotamoroz
279b4b41e5 bump 2023-11-24 10:32:46 +00:00
vorotamoroz
b644fb791d - Changes and performance improvements:
- Now the saving files are processed by Blob.
    - The V2-Format has been reverted.
    - New encoding format has been enabled in default.
    - WARNING: Since this version, the compatibilities with older Filesystem LiveSync have been lost.
2023-11-24 10:31:58 +00:00
Vichaya Raksakunpanich
5802ed31be Update ObsidianLiveSyncSettingTab.ts
Lower payload size limit and batch limit to 2 due to IBM Cloudant read/write limitation. Hope it will fix the "Replication error".
2023-11-24 16:25:16 +07:00
vorotamoroz
ac9428e96b Fixed
- To better replication, path obfuscation is now deterministic even if with E2EE.
2023-11-15 08:44:03 +00:00
vorotamoroz
280d9e1dd9 Fixed: Fixed the issue of TOML editing. 2023-11-07 01:07:58 +00:00
vorotamoroz
f7209e566c bump 2023-10-24 10:07:29 +01:00
vorotamoroz
4a9ab2d1de Fixed:
- No longer enumerating file names is broken.
2023-10-24 10:07:17 +01:00
vorotamoroz
cb74b5ee93 - Fixed
- Now empty file could be decoded.
    - Local files are no longer pre-saved before fetching from a remote database.
    - No longer deadlock while applying customisation sync.
    - Configuration with multiple files is now able to be applied correctly.
    - Deleting folder propagation now works without enabling the use of a trash bin.
2023-10-24 09:54:56 +01:00
vorotamoroz
60eecd7001 bump 2023-10-17 12:00:59 +09:00
vorotamoroz
4bd7b54bcd Fixed:
- Now the files which having digit or character prefixes in the path will not be ignored.
2023-10-17 12:00:19 +09:00
vorotamoroz
8923c73d1b bump 2023-10-14 23:08:34 +09:00
vorotamoroz
11e64b13e2 The text-input-dialogue is no longer broken. 2023-10-14 23:07:51 +09:00
vorotamoroz
983d9248ed bump 2023-10-13 04:23:59 +01:00
vorotamoroz
7240e84328 - New feature:
- We can launch Customization sync from the Ribbon if we enable it.
- Fixed:
  - Setup URI is now back to the previous spec; be encrypted by V1.
  - The Settings dialogue is now registered at the beginning of the start-up process.
- Improved:
  - Enumerating documents has been faster.
2023-10-13 04:22:24 +01:00
vorotamoroz
0d55ae2532 Merge pull request #298 from LiamSwayne/patch-1
Grammar fix
2023-10-04 13:07:42 +09:00
Liam Swayne
dbd284f5dd grammar fix 2023-10-03 22:26:30 -04:00
46 changed files with 3412 additions and 2178 deletions

109
README.md
View File

@@ -1,57 +1,52 @@
<!-- For translation: 20240209r0 -->
# Self-hosted LiveSync # Self-hosted LiveSync
[Japanese docs](./README_ja.md) - [Chinese docs](./README_cn.md). [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, available on every obsidian-compatible platform and using CouchDB as the server.
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".
![obsidian_live_sync_demo](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif) ![obsidian_live_sync_demo](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif)
Before installing or upgrading LiveSync, please back your vault up. Note: This plugin cannot synchronise with the official "Obsidian Sync".
## Features ## Features
- Visual conflict resolver included. - Synchronize vaults very efficiently with less traffic.
- Bidirectional synchronization between devices nearly in real-time - Good at conflicted modification.
- You can use CouchDB or its compatibles like IBM Cloudant. - Automatic merging for simple conflicts.
- End-to-End encryption is supported. - Using OSS solution for the server.
- Plugin synchronization(Beta) - Compatible solutions can be used.
- Receive WebClip from [obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf) (End-to-End encryption will not be applicable.) - Supporting End-to-end encryption.
- Synchronisation of settings, snippets, themes, and plug-ins, via [Customization sync(Beta)](#customization-sync) or [Hidden File Sync](#hiddenfilesync)
- WebClip from [obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
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. This plug-in might be 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 >[!IMPORTANT]
> - Before installing or upgrading this plug-in, please back your vault up.
- 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. > - Do not enable this plugin with another synchronization solution at the same time (including iCloud and Obsidian Sync).
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 solution. 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, and thus won't be synchronized. (**and may also get deleted**)
## How to use ## How to use
### Get your database ready. ### 3-minute setup - CouchDB on fly.io
First, get your database ready. fly.io is preferred for testing. Or you can use your own server with CouchDB. For more information, refer below: **Recommended for beginners**
1. [Setup fly.io](docs/setup_flyio.md)
2. [Setup IBM Cloudant](docs/setup_cloudant.md)
3. [Setup your CouchDB](docs/setup_own_server.md)
### Configure the plugin [![LiveSync Setup onto Fly.io SpeedRun 2024 using Google Colab](https://img.youtube.com/vi/7sa_I1832Xc/0.jpg)](https://www.youtube.com/watch?v=7sa_I1832Xc)
See [Quick setup guide](doccs/../docs/quick_setup.md) 1. [Setup CouchDB on fly.io](docs/setup_flyio.md)
2. Configure plug-in in [Quick Setup](docs/quick_setup.md)
## Something looks corrupted... ### Manually Setup
Please open the configuration link again and Answer below: 1. Setup the server
- If your local database looks corrupted (in other words, when your Obsidian getting weird even standalone.) 1. [Setup CouchDB on fly.io](docs/setup_flyio.md)
- Answer `No` to `Keep local DB?` 2. [Setup your CouchDB](docs/setup_own_server.md)
- If your remote database looks corrupted (in other words, when something happens while replicating) 2. Configure plug-in in [Quick Setup](docs/quick_setup.md)
- Answer `No` to `Keep remote DB?`
> [!TIP]
> We are still able to use IBM Cloudant. However, it is not recommended for several reasons nowadays. Here is [Setup IBM Cloudant](docs/setup_cloudant.md)
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).
## Information in StatusBar ## Information in StatusBar
@@ -59,36 +54,28 @@ Synchronization status is shown in statusbar.
- Status - Status
- ⏹️ Stopped - ⏹️ Stopped
- 💤 LiveSync enabled. Waiting for changes. - 💤 LiveSync enabled. Waiting for changes
- ⚡️ Synchronization in progress. - ⚡️ Synchronization in progress
- ⚠ An error occurred. - ⚠ An error occurred
- ↑ Uploaded chunks and metadata - Statistical indicator
- ↓ Downloaded chunks and metadata - ↑ Uploaded chunks and metadata
- ⏳ Number of pending processes - ↓ Downloaded chunks and metadata
- 🧩 Number of files waiting for their chunks. - Progress indicator
If you have deleted or renamed files, please wait until ⏳ icon disappeared. - 📥 Unprocessed transferred items
- 📄 Working database operation
- 💾 Working write storage processes
- ⏳ Working read storage processes
- 🛫 Pending read storage processes
- ⚙️ Working or pending storage processes of hidden files
- 🧩 Waiting chunks
- 🔌 Working Customisation items (Configuration, snippets, and plug-ins)
To prevent file and database corruption, please wait until all progress indicators have disappeared. Especially in case of if you have deleted or renamed files.
## 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 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 (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 a work in progress.) ## Tips and Troubleshooting
If you are having problems getting the plugin working see: [Tips and Troubleshooting](docs/troubleshooting.md)
## Troubleshooting
If you are having problems getting the plugin working see: [Troubleshooting](docs/troubleshooting.md)
## License ## License

View File

@@ -1,322 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/gist/vrtmrz/37c3efd7842e49947aaaa7f665e5020a/deploy_couchdb_to_flyio_v2_with_swap.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "markdown",
"source": [
"History:\n",
"- 18, May, 2023: Initial.\n",
"- 19, Jun., 2023: Patched for enabling swap.\n",
"- 22, Aug., 2023: Generating Setup-URI implemented."
],
"metadata": {
"id": "HiRV7G8Gk1Rs"
}
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "2Vh0mEQEZuAK"
},
"outputs": [],
"source": [
"# Configurations\n",
"import os\n",
"os.environ['region']=\"nrt\"\n",
"os.environ['couchUser']=\"alkcsa93\"\n",
"os.environ['couchPwd']=\"c349usdfnv48fsasd\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "SPmbB0jZauQ1"
},
"outputs": [],
"source": [
"# Delete once\n",
"!rm ./fly.toml"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "Nze7QoxLZ7Yx"
},
"outputs": [],
"source": [
"# Installation\n",
"# You have to set up your account in here.\n",
"!curl -L https://fly.io/install.sh | sh\n",
"!/root/.fly/bin/flyctl auth signup"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "MVJwsIYrbgtx"
},
"outputs": [],
"source": [
"# Generate server\n",
"!/root/.fly/bin/flyctl launch --auto-confirm --generate-name --detach --no-deploy --region ${region}\n",
"!/root/.fly/bin/fly volumes create --region ${region} couchdata --size 2 --yes"
]
},
{
"cell_type": "code",
"source": [
"# Check the toml once.\n",
"!cat fly.toml"
],
"metadata": {
"id": "2RSoO9o-i2TT"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "zUtPZLVnbvdQ"
},
"outputs": [],
"source": [
"# Modify fly.toml\n",
"## Port modification\n",
"!sed -i 's/8080/5984/g' fly.toml\n",
"## Add user into.\n",
"!echo -e \"\\n[env]\\n COUCHDB_USER = \\\"${couchUser}\\\"\" >> ./fly.toml\n",
"## Set the location of an ini file which to save configurations persistently via erlang flags.\n",
"!echo -e \"\\nERL_FLAGS=\\\"-couch_ini /opt/couchdb/etc/default.ini /opt/couchdb/etc/default.d/ /opt/couchdb/etc/local.d /opt/couchdb/etc/local.ini /opt/couchdb/data/persistence.ini\\\"\" >> ./fly.toml\n",
"## Mounting volumes to store data and ini file.\n",
"!echo -e \"\\n[mounts]\\n source=\\\"couchdata\\\"\\n destination=\\\"/opt/couchdb/data\\\"\" >> ./fly.toml\n",
"!cat fly.toml"
]
},
{
"cell_type": "code",
"source": [
"# Make the Dockerfile to modify the permission of the ini file. If you want to use a specific version, you should change `latest` here.\n",
"!echo -e \"\\n[build]\\n dockerfile = \\\"./Dockerfile\\\"\" >> ./fly.toml"
],
"metadata": {
"id": "LQPsZ_dYxkTu"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"!echo -e \"FROM couchdb:latest\\nRUN sed -i '2itouch /opt/couchdb/data/persistence.ini && chmod +w /opt/couchdb/data/persistence.ini && fallocate -l 512M /swapfile && chmod 0600 /swapfile && mkswap /swapfile && echo 10 > /proc/sys/vm/swappiness && swapon /swapfile && echo 1 > /proc/sys/vm/overcommit_memory' /docker-entrypoint.sh\" > ./Dockerfile"
],
"metadata": {
"id": "44cBeGJ9on5i"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Check dockerfile\n",
"!cat ./Dockerfile"
],
"metadata": {
"id": "ai2R3BbpxRSe"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "xWdsTCI6bzk2"
},
"outputs": [],
"source": [
"# Configure password\n",
"!/root/.fly/bin/flyctl secrets set COUCHDB_PASSWORD=${couchPwd}"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "k0WIQlShcXGa"
},
"outputs": [],
"source": [
"# Deploy server\n",
"# Be sure to shutdown after the test.\n",
"!/root/.fly/bin/flyctl deploy --detach --remote-only\n",
"!/root/.fly/bin/flyctl status"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "0ySggkdlfq7M"
},
"outputs": [],
"source": [
"import subprocess, json\n",
"result = subprocess.run([\"/root/.fly/bin/flyctl\",\"status\",\"-j\"], capture_output=True, text=True)\n",
"if result.returncode==0:\n",
" hostname = json.loads(result.stdout)[\"Hostname\"]\n",
" os.environ['couchHost']=\"https://%s\" % (hostname)\n",
" print(\"Your couchDB server is https://%s/\" % (hostname))\n",
"else:\n",
" print(\"Something occured.\")\n"
]
},
{
"cell_type": "code",
"source": [
"# Finish setting up the CouchDB\n",
"# Please repeat until the request is completed without error messages\n",
"# i.e., You have to redo this block while \"curl: (35) OpenSSL SSL_connect: Connection reset by peer in connection to xxxx\" is showing.\n",
"!curl -X POST \"${couchHost}/_cluster_setup\" -H \"Content-Type: application/json\" -d \"{\\\"action\\\":\\\"enable_single_node\\\",\\\"username\\\":\\\"${couchUser}\\\",\\\"password\\\":\\\"${couchPwd}\\\",\\\"bind_address\\\":\\\"0.0.0.0\\\",\\\"port\\\":5984,\\\"singlenode\\\":true}\" --user \"${couchUser}:${couchPwd}\""
],
"metadata": {
"id": "cGlSzVqlQG_z"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Please repeat until all lines are completed without error messages\n",
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/chttpd/require_valid_user\" -H \"Content-Type: application/json\" -d '\"true\"' --user \"${couchUser}:${couchPwd}\"\n",
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/chttpd_auth/require_valid_user\" -H \"Content-Type: application/json\" -d '\"true\"' --user \"${couchUser}:${couchPwd}\"\n",
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/httpd/WWW-Authenticate\" -H \"Content-Type: application/json\" -d '\"Basic realm=\\\"couchdb\\\"\"' --user \"${couchUser}:${couchPwd}\"\n",
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/httpd/enable_cors\" -H \"Content-Type: application/json\" -d '\"true\"' --user \"${couchUser}:${couchPwd}\"\n",
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/chttpd/enable_cors\" -H \"Content-Type: application/json\" -d '\"true\"' --user \"${couchUser}:${couchPwd}\"\n",
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/chttpd/max_http_request_size\" -H \"Content-Type: application/json\" -d '\"4294967296\"' --user \"${couchUser}:${couchPwd}\"\n",
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/couchdb/max_document_size\" -H \"Content-Type: application/json\" -d '\"50000000\"' --user \"${couchUser}:${couchPwd}\"\n",
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/cors/credentials\" -H \"Content-Type: application/json\" -d '\"true\"' --user \"${couchUser}:${couchPwd}\"\n",
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/cors/origins\" -H \"Content-Type: application/json\" -d '\"app://obsidian.md,capacitor://localhost,http://localhost\"' --user \"${couchUser}:${couchPwd}\""
],
"metadata": {
"id": "JePzrsHypY18"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Now, our CouchDB has been surely installed and configured. Cheers!\n",
"\n",
"In the steps that follow, create a setup-URI.\n",
"\n",
"This URI could be imported directly into Self-hosted LiveSync, to configure the use of the CouchDB which we configured now."
],
"metadata": {
"id": "YfSOomsoXbGS"
}
},
{
"cell_type": "code",
"source": [
"# Database config\n",
"import random, string\n",
"\n",
"def randomname(n):\n",
" return ''.join(random.choices(string.ascii_letters + string.digits, k=n))\n",
"\n",
"# The database name\n",
"os.environ['database']=\"obsidiannote\"\n",
"# The passphrase to E2EE\n",
"os.environ['passphrase']=randomname(20)\n",
"\n",
"print(\"Your database:\"+os.environ['database'])\n",
"print(\"Your passphrase:\"+os.environ['passphrase'])"
],
"metadata": {
"id": "416YncOqXdNn"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Install deno for make setup uri\n",
"!curl -fsSL https://deno.land/x/install/install.sh | sh"
],
"metadata": {
"id": "C4d7C0HAXgsr"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Fetch module for encrypting a Setup URI\n",
"!curl -o encrypt.ts https://gist.githubusercontent.com/vrtmrz/f9d1d95ee2ca3afa1a924a2c6759b854/raw/d7a070d864a6f61403d8dc74208238d5741aeb5a/encrypt.ts"
],
"metadata": {
"id": "hQL_Dx-PXise"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Make buttons!\n",
"from IPython.display import HTML\n",
"result = subprocess.run([\"/root/.deno/bin/deno\",\"run\",\"-A\",\"encrypt.ts\"], capture_output=True, text=True)\n",
"text=\"\"\n",
"if result.returncode==0:\n",
" text = result.stdout.strip()\n",
" result = HTML(f\"<button onclick=navigator.clipboard.writeText('{text}')>Copy setup uri</button><br>Importing passphrase is `welcome`. <br>If you want to synchronise in live mode, please apply a preset after setup.)\")\n",
"else:\n",
" result = \"Failed to encrypt the setup URI\"\n",
"result"
],
"metadata": {
"id": "o0gX_thFXlIZ"
},
"execution_count": null,
"outputs": []
}
],
"metadata": {
"colab": {
"provenance": [],
"private_outputs": true,
"include_colab_link": true
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
},
"gpuClass": "standard"
},
"nbformat": 4,
"nbformat_minor": 0
}

View File

@@ -2,34 +2,51 @@
[Japanese docs](./quick_setup_ja.md) - [Chinese docs](./quick_setup_cn.md). [Japanese docs](./quick_setup_ja.md) - [Chinese docs](./quick_setup_cn.md).
The plugin has so many configuration options to deal with different circumstances. However, there are not so many settings that are actually used. Therefore, `The Setup wizard` has been implemented to simplify the initial setup. The plugin has so many configuration options to deal with different circumstances. However, only a few settings are required in the normal cases. Therefore, `The Setup wizard` has been implemented to simplify the setup.
Note: Subsequent devices are recommended to be set up using the `Copy setup URI` and `Open setup URI`.
## The Setup wizard
Open the `🧙‍♂️ Setup wizard` in the settings dialogue. If the plugin has not been configured before, it should already be open.
![](../images/quick_setup_1.png) ![](../images/quick_setup_1.png)
- Discard the existing configuration and set up There are three methods to set up Self-hosted LiveSync.
If you have changed any settings, this button allows you to discard all changes before setting up.
- Do not discard the existing configuration and set up 1. [Using setup URIs](#1-using-setup-uris) *(Recommended)*
Simply reconfigure. Be careful. In wizard mode, you cannot see all configuration items, even if they have been configured. 2. [Minimal setup](#2-minimal-setup)
3. [Full manually setup the and Enable on this dialogue](#3-manually-setup)
Pressing `Next` on one of the above options will put the configuration dialog into wizard mode. ## At the first device
### Wizard mode ### 1. Using setup URIs
> [!TIP] What is the setup URI? Why is it required?
> The setup URI is the encrypted representation of Self-hosted LiveSync configuration as a URI. This starts `obsidian://setuplivesync?settings=`. This is encrypted with a passphrase, so that it can be shared relatively securely between devices. It is a bit long, but it is one line. This allows a series of settings to be set at once without any inconsistencies.
>
> If you have configured the remote database by [Automated setup on Fly.io](./setup_flyio.md#a-very-automated-setup) or [set up your server with the tool](./setup_own_server.md#1-generate-the-setup-uri-on-a-desktop-device-or-server), **you should have one of them**
In this procedure, [this video](https://youtu.be/7sa_I1832Xc?t=146) may help us.
1. Click `Use` button (Or launch `Use the copied setup URI` from Command palette).
2. Paste the Setup URI into the dialogue
3. Type the passphrase of the Setup URI
4. Answer `yes` for `Importing LiveSync's conf, OK?`.
5. Answer `Set it up as secondary or subsequent device` for `How would you like to set it up?`.
6. Initialisation will begin, please hold a while.
7. You will asked about the hidden file synchronisation, answer as you like.
1. If you are new to Self-hosted LiveSync, we can configure it later so leave it once.
8. Synchronisation has been started! `Reload app without saving` is recommended after the indicators of Self-hosted LiveSync disappear.
OK, we can proceed the [next step](#).
### 2. Minimal setup
If you do not have any setup URI, Press the `start` button. The setting dialogue turns into the wizard mode and will display only minimal items.
>[!TIP]
> We can generate the setup URI with the tool in any time. Please use [this tool](./setup_own_server.md#1-generate-the-setup-uri-on-a-desktop-device-or-server).
![](../images/quick_setup_2.png) ![](../images/quick_setup_2.png)
Let's see how to use it step-by-step. #### Remote database configuration
## Remote Database configuration 1. Enter the information for the database we have set up.
### Remote database configuration
Enter the information for the database we have set up.
![](../images/quick_setup_3.png) ![](../images/quick_setup_3.png)
@@ -37,13 +54,9 @@ Enter the information for the database we have set up.
#### Test database connection and Check database configuration #### Test database connection and Check database configuration
We can check the connectivity to the database, and the database settings. We can check the connectivity to the database, and the database settings.
![](../images/quick_setup_5.png) ![](../images/quick_setup_5.png)
#### Test Database Connection #### Check and Fix database configuration
Check whether we can connect to the database. If it fails, there are several possible reasons, but first attempt the `Check database configuration` check to see if it fails there too.
#### Check database configuration
Check the database settings and fix any problems on the spot. Check the database settings and fix any problems on the spot.
@@ -52,40 +65,43 @@ Check the database settings and fix any problems on the spot.
This item may vary depending on the connection. In the above case, press all three Fix buttons. 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. If the Fix buttons disappear and all become check marks, we are done.
#### Confidentiality configuration (Optional but very preferred)
### Confidentiality configuration
![](../images/quick_setup_4.png) ![](../images/quick_setup_4.png)
Encrypt your database in case of unintended database exposure; enable End to End encryption and the contents of your notes will be encrypted at the moment it leaves the device. We strongly recommend enabling it. And `Path Obfuscation` also obfuscates filenames. Now stable and recommended. Enable End-to-end encryption and the contents of your notes will be encrypted at the moment it leaves the device. We strongly recommend enabling it. And `Path Obfuscation` also obfuscates filenames. Now stable and recommended.
Encryption is based on 256-bit AES-GCM.
These setting can be disabled if you are inside a closed network and it is clear that you will not be accessed by third parties. These setting can be disabled if you are inside a closed network and it is clear that you will not be accessed by third parties.
![](../images/quick_setup_7.png) > [!TIP]
> Encryption is based on 256-bit AES-GCM.
#### Next We should proceed to the Next step.
Go to the Sync Settings.
#### Discard existing database and proceed #### Sync Settings
Discard the contents of the Remote database and go to the Sync Settings.
### Sync Settings
Finally, finish the wizard by selecting a preset for synchronisation. Finally, finish the wizard by selecting a preset for synchronisation.
![](../images/quick_setup_9_1.png) ![](../images/quick_setup_9_1.png)
Select any synchronisation methods 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`. Select any synchronisation methods we want to use and `Apply`. If database initialisation is required, it will be performed at this time. When `All done!` is displayed, we are ready to synchronise.
The dialogue of `Copy settings as a new setup URI` will be open automatically. Please input a passphrase to encrypt the new `Setup URI`. (This passphrase is to encrypt the setup URI, not the vault).
![](../images/quick_setup_10.png) ![](../images/quick_setup_10.png)
Set the passphrase as you like. The Setup URI will be copied to the clipboard, please make a note(Not in Obsidian) of this.
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 >[!TIP]
After installing Self-hosted LiveSync on the first 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. We can copy this in any time by `Copy current settings as a new setup URI`.
Answer the following.
- `Yes` to `Importing LiveSync's conf, OK?` ### 3. Manually setup
- `Set it up as secondary or subsequent device` to `How would you like to set it up?`.
Then, The configuration will take effect and replication will start. Your files will be synchronised soon! You may need to close the settings dialog and reopen it to see the settings fields populated properly, but they will be set. It is strongly recommended to perform a "minimal set-up" first and set up the other contents after making sure has been synchronised.
However, if you have some specific reasons to configure it manually, please click the `Enable` button of `Enable LiveSync on this device as the set-up was completed manually`.
And, please copy the setup URI by `Copy current settings as a new setup URI` and make a note(Not in Obsidian) of this.
## At the subsequent device
After installing Self-hosted LiveSync on the first device, we should have a setup URI. **The first choice is to use it**. Please share it with the device you want to setup.
It is completely same as [Using setup URIs on the first device](#1-using-setup-uris). Please refer it.

View File

@@ -1,24 +1,53 @@
<!-- For translation: 20240209r0 -->
# Setup CouchDB on fly.io # Setup CouchDB on fly.io
In some cases, the use of IBM Cloudant was found to be hard. We looked for alternatives, but there were no services available. Therefore, we have to build our own servers, which is quite a challenge. In this situation, with fly.io, most of the processes are simplified and only CouchDB needs to be configured.
This is how to configure fly.io and CouchDB on it for Self-hosted LiveSync. This is how to configure fly.io and CouchDB on it for Self-hosted LiveSync.
It generally falls within the `Free Allowances` range in most cases. > [!WARNING]
> It is **your** instance. In Obsidian, we have files locally. Hence, do not hesitate to destroy the remote database if you feel something have got weird. We can launch and switch to the new CouchDB instance anytime[^1].
>
[^1]: Actually, I am always building the database for reproduction of the issue like so.
**[Automatic setup using Colaboratory](#automatic-setup-using-colaboratory) is recommended, after reading this document through. It is reproducible and hard to fail.** > [!NOTE]
> **What and why is the Fly.io?**
> At some point, we started to experience problems related to our IBM Cloudant account. At the same time, Self-hosted LiveSync started to improve its functionality, requiring CouchDB in a more natural state to use all its features.
>
> Then we found Fly.io. Fly.io is the PaaS Platform, which can be useable for a very reasonable price. It generally falls within the `Free Allowances` range in most cases.
## Required materials ## Required materials
- A valid credit or debit card. - A valid credit or debit card.
## Warning ## Setup CouchDB instance
- It will be `your` instance. Check the log regularly. ### A. Very automated setup
## Prerequisites [![LiveSync Setup onto Fly.io SpeedRun 2024 using Google Colab](https://img.youtube.com/vi/7sa_I1832Xc/0.jpg)](https://www.youtube.com/watch?v=7sa_I1832Xc)
For simplicity, the following description assumes that the settings are as shown in the table below. Please read it in accordance with the actual settings you wish to make. 1. Open [setup-flyio-on-the-fly-v2.ipynb](../setup-flyio-on-the-fly-v2.ipynb).
2. Press the `Open in Colab` button.
3. Choose a region and run all blocks (Refer to video).
1. If you do not have the account yet, the sign-up page will be shown, please follow the instructions. The [Official document is here](https://fly.io/docs/hands-on/sign-up/).
4. Copy the Setup-URI and Use it in the Obsidian.
5. You have been synchronised. Use the Setup-URI in subsequent devices.
Steps 4 and 5 are detailed in the [Quick Setup](./quick_setup.md#1-using-setup-uris).
> [!NOTE]
> Your automatically configured configurations will be shown on the result in the Colab note like below, and **it will not be saved**. Please make a note of it somewhere.
> ```
> -- YOUR CONFIGURATION --
> URL : https://billowing-dawn-6619.fly.dev
> username: billowing-cherry-22580
> password: misty-dew-13571
> region : nrt
> ```
### B. Scripted Setup
Please refer to the document of [deploy-server.sh](../utils/readme.md#deploy-serversh).
### C. Manual Setup
| Used in the text | Meaning and where to use | Memo | | Used in the text | Meaning and where to use | Memo |
| ---------------- | --------------------------- | ------------------------------------------------------------------------ | | ---------------- | --------------------------- | ------------------------------------------------------------------------ |
@@ -26,11 +55,7 @@ For simplicity, the following description assumes that the settings are as shown
| dfusiuada9suy | Password | | | dfusiuada9suy | Password | |
| nrt | Region to make the instance | We can use any [region](https://fly.io/docs/reference/regions/) near us. | | nrt | Region to make the instance | We can use any [region](https://fly.io/docs/reference/regions/) near us. |
## Steps with your computer #### 1. Install flyctl
If you want to avoid installing anything, please skip to [Automatic setup using Colaboratory](#automatic-setup-using-colaboratory).
### 1. Install flyctl
- Mac or Linux - Mac or Linux
@@ -44,7 +69,7 @@ $ curl -L https://fly.io/install.sh | sh
$ iwr https://fly.io/install.ps1 -useb | iex $ iwr https://fly.io/install.ps1 -useb | iex
``` ```
### 2. Sign up or Sign in to fly.io #### 2. Sign up or Sign in to fly.io
- Sign up - Sign up
@@ -58,148 +83,90 @@ $ fly auth signup
$ fly auth login $ fly auth login
``` ```
For more information, please refer [Sign up](https://fly.io/docs/hands-on/sign-up/) and [Sign in](https://fly.io/docs/hands-on/sign-in/). For more information, please refer to [Sign up](https://fly.io/docs/hands-on/sign-up/) and [Sign in](https://fly.io/docs/hands-on/sign-in/).
### 3. Make configuration files #### 3. Make a configuration file
Please be careful, `nrt` is the region where near to Japan. Please use your preferred region. 1. Make `fly.toml` from template `fly.template.toml`.
We can simply copy and rename the file. The template is on [utils/flyio/fly.template.toml](../utils/flyio/fly.template.toml)
2. Decide the instance name, initialize the App, and set credentials.
1. Make fly.toml >[!TIP]
> - The name `billowing-dawn-6619` is randomly decided name, and it will be a part of the CouchDB URL. It should be globally unique. Therefore, it is recommended to use something random for this name.
> - Explicit naming is very good for humans. However, we do not often get the chance to actually enter this manually (have designed so). This database may contain important information for you. The needle should be hidden in the haystack.
```bash
$ fly launch --name=billowing-dawn-6619 --env="COUCHDB_USER=campanella" --copy-config=true --detach --no-deploy --region nrt --yes
$ fly secrets set COUCHDB_PASSWORD=dfusiuada9suy
```
#### 4. Deploy
``` ```
$ flyctl launch --generate-name --detach --no-deploy --region nrt $ flyctl deploy
Creating app in /home/vrtmrz/dev/fly/demo An existing fly.toml file was found
Scanning source code Using build strategies '[the "couchdb:latest" docker image]'. Remove [build] from fly.toml to force a rescan
Could not find a Dockerfile, nor detect a runtime or framework from source code. Continuing with a blank app. Creating app in /home/vorotamoroz/dev/obsidian-livesync/utils/flyio
automatically selected personal organization: vorotamoroz We're about to launch your app on Fly.io. Here's what you're getting:
App will use 'nrt' region as primary
Organization: vorotamoroz (fly launch defaults to the personal org)
Name: billowing-dawn-6619 (specified on the command line)
Region: Tokyo, Japan (specified on the command line)
App Machines: shared-cpu-1x, 256MB RAM (specified on the command line)
Postgres: <none> (not requested)
Redis: <none> (not requested)
Created app 'billowing-dawn-6619' in organization 'personal' Created app 'billowing-dawn-6619' in organization 'personal'
Admin URL: https://fly.io/apps/billowing-dawn-6619 Admin URL: https://fly.io/apps/billowing-dawn-6619
Hostname: billowing-dawn-6619.fly.dev Hostname: billowing-dawn-6619.fly.dev
Wrote config file fly.toml Wrote config file fly.toml
``` Validating /home/vorotamoroz/dev/obsidian-livesync/utils/flyio/fly.toml
Platform: machines
`billowing-dawn-6619` is an automatically generated name. It is used as the hostname. Please note it in something. ✓ Configuration is valid
Note: we can specify this without `--generate-name`, but does not recommend in the trial phases. Your app is ready! Deploy with `flyctl deploy`
Secrets are staged for the first deployment
1. Make volume
```
$ flyctl volumes create --region nrt couchdata --size 2 --yes
ID: vol_g67340kxgmmvydxw
Name: couchdata
App: billowing-dawn-6619
Region: nrt
Zone: 35b7
Size GB: 2
Encrypted: true
Created at: 02 Jun 23 01:19 UTC
```
3. Edit fly.toml
Changes:
- Change exposing port from `8080` to `5984`
- Mounting the volume `couchdata` created in step 2 under `/opt/couchdb/data`
- Set `campanella` for the administrator of CouchDB
- Customise CouchDB to use persistent ini-file; which is located under the data directory.
- To use Dockerfile
```diff
# fly.toml app configuration file generated for billowing-dawn-6619 on 2023-06-02T10:18:59+09:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = "billowing-dawn-6619"
primary_region = "nrt"
[http_service]
- internal_port = 8080
+ internal_port = 5984
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
+[mounts]
+ source="couchdata"
+ destination="/opt/couchdb/data"
+
+[env]
+ COUCHDB_USER = "campanella"
+ ERL_FLAGS="-couch_ini /opt/couchdb/etc/default.ini /opt/couchdb/etc/default.d/ /opt/couchdb/etc/local.d /opt/couchdb/etc/local.ini /opt/couchdb/data/persistence.ini"
+
+[build]
+ dockerfile = "./Dockerfile"
```
4. Make `Dockerfile`
Create a Dockerfile that patches the start-up script to fix ini file permissions.
```dockerfile
FROM couchdb:latest
RUN sed -i '2itouch /opt/couchdb/data/persistence.ini && chmod +w /opt/couchdb/data/persistence.ini' /docker-entrypoint.sh
```
5. Set credential
```
flyctl secrets set COUCHDB_PASSWORD=dfusiuada9suy
```
### 4. Deploy
```
$ flyctl deploy --detach --remote-only
==> Verifying app config ==> Verifying app config
Validating /home/vrtmrz/dev/fly/demo/fly.toml Validating /home/vorotamoroz/dev/obsidian-livesync/utils/flyio/fly.toml
Platform: machines Platform: machines
✓ Configuration is valid ✓ Configuration is valid
--> Verified app config --> Verified app config
==> Building image ==> Building image
Remote builder fly-builder-bold-sky-4515 ready Searching for image 'couchdb:latest' remotely...
==> Creating build context image found: img_ox20prk63084j1zq
--> Creating build context done
==> Building image with Docker
--> docker host: 20.10.12 linux x86_64
-------------:SNIPPED:------------- Watch your deployment at https://fly.io/apps/billowing-dawn-6619/monitoring
Watch your app at https://fly.io/apps/billowing-dawn-6619/monitoring
Provisioning ips for billowing-dawn-6619 Provisioning ips for billowing-dawn-6619
Dedicated ipv6: 2a09:8280:1::2d:240f Dedicated ipv6: 2a09:8280:1::37:fde9
Shared ipv4: 66.241.125.213 Shared ipv4: 66.241.124.163
Add a dedicated ipv4 with: fly ips allocate-v4 Add a dedicated ipv4 with: fly ips allocate-v4
Creating a 1 GB volume named 'couchdata' for process group 'app'. Use 'fly vol extend' to increase its size
This deployment will: This deployment will:
* create 1 "app" machine * create 1 "app" machine
No machines in group app, launching a new machine No machines in group app, launching a new machine
Machine e7845d1f297183 [app] has state: started
WARNING The app is not listening on the expected address and will not be reachable by fly-proxy.
You can fix this by configuring your app to listen on the following addresses:
- 0.0.0.0:5984
Found these processes inside the machine with open listening sockets:
PROCESS | ADDRESSES
-----------------*---------------------------------------
/.fly/hallpass | [fdaa:0:73b9:a7b:22e:3851:7f28:2]:22
Finished launching new machines Finished launching new machines
NOTE: The machines for [app] have services with 'auto_stop_machines = true' that will be stopped when idling NOTE: The machines for [app] have services with 'auto_stop_machines = true' that will be stopped when idling
-------
Checking DNS configuration for billowing-dawn-6619.fly.dev
Visit your newly deployed app at https://billowing-dawn-6619.fly.dev/
``` ```
Now your CouchDB has been launched. (Do not forget to delete it if no longer need). #### 5. Apply CouchDB configuration
If failed, please check by `flyctl doctor`. Failure of remote build may be resolved by `flyctl` wireguard reset` or something.
```
$ flyctl status
App
Name = billowing-dawn-6619
Owner = personal
Hostname = billowing-dawn-6619.fly.dev
Image = billowing-dawn-6619:deployment-01H1WWB3CK5Z9ZX71KHBSDGHF1
Platform = machines
Machines
PROCESS ID VERSION REGION STATE CHECKS LAST UPDATED
app e7845d1f297183 1 nrt started 2023-06-02T01:43:34Z
```
### 5. Apply CouchDB configuration
After the initial setup, CouchDB needs some more customisations to be used from Self-hosted LiveSync. It can be configured in browsers or by HTTP-REST APIs. After the initial setup, CouchDB needs some more customisations to be used from Self-hosted LiveSync. It can be configured in browsers or by HTTP-REST APIs.
@@ -273,15 +240,13 @@ iwr -UseBasicParsing -Method 'PUT' -ContentType 'application/json; charset=utf-8
Note: Each of these should also be repeated until finished in 200. Note: Each of these should also be repeated until finished in 200.
### 6. Use it from Self-hosted LiveSync #### 6. Use it from Self-hosted LiveSync
Now the CouchDB is ready to use from Self-hosted LiveSync. We can use `https://billowing-dawn-6619.fly.dev` in URI, `campanella` in `Username` and `dfusiuada9suy` in `Password` on Self-hosted LiveSync. `Database name` could be anything you want. Now the CouchDB is ready to use from Self-hosted LiveSync. We can use `https://billowing-dawn-6619.fly.dev` in URI, `campanella` in `Username` and `dfusiuada9suy` in `Password` on Self-hosted LiveSync. The `Database name` could be anything you want.
`Enhance chunk size` could be up to around `100`. Please refer to the [Minimal Setup of the Quick Setup](./quick_setup.md#2-minimal-setup).
## Automatic setup using Colaboratory ## Delete the Instance
We can perform all these steps by using [this Colaboratory notebook](/deploy_couchdb_to_flyio_v2_with_swap.ipynb) without installing anything. If you want to delete the CouchDB instance, you can do that in [fly.io Dashboard](https://fly.io/dashboard/personal)
## After testing / before creating a new instance If you have done with [B. Scripted Setup](#b-scripted-setup), we can use [delete-server.sh](../utils/readme.md#delete-serversh).
**Be sure to delete the instance. We can check instances on the [Dashboard](https://fly.io/dashboard/personal)**

View File

@@ -1,138 +1,126 @@
# Setup a CouchDB server # Setup a CouchDB server
## Table of Contents ## Table of Contents
- [Configure](#configure)
- [Run](#run) - [Setup a CouchDB server](#setup-a-couchdb-server)
- [Docker CLI](#docker-cli) - [Table of Contents](#table-of-contents)
- [Docker Compose](#docker-compose) - [1. Prepare CouchDB](#1-prepare-couchdb)
- [Access from a mobile device](#access-from-a-mobile-device) - [A. Using Docker container](#a-using-docker-container)
- [Testing from a mobile](#testing-from-a-mobile) - [1. Prepare](#1-prepare)
- [Setting up your domain](#setting-up-your-domain) - [2. Run docker container](#2-run-docker-container)
- [Reverse Proxies](#reverse-proxies) - [B. Install CouchDB directly](#b-install-couchdb-directly)
- [Traefik](#traefik) - [2. Run couchdb-init.sh for initialise](#2-run-couchdb-initsh-for-initialise)
- [3. Expose CouchDB to the Internet](#3-expose-couchdb-to-the-internet)
- [4. Client Setup](#4-client-setup)
- [1. Generate the setup URI on a desktop device or server](#1-generate-the-setup-uri-on-a-desktop-device-or-server)
- [2. Setup Self-hosted LiveSync to Obsidian](#2-setup-self-hosted-livesync-to-obsidian)
- [Manual setup information](#manual-setup-information)
- [Setting up your domain](#setting-up-your-domain)
- [Reverse Proxies](#reverse-proxies)
- [Traefik](#traefik)
--- ---
## Configure ## 1. Prepare CouchDB
### A. Using Docker container
The easiest way to set up a CouchDB instance is using the official [docker image](https://hub.docker.com/_/couchdb). #### 1. Prepare
```bash
Some initial configuration is required. Create a `local.ini` to use Self-hosted LiveSync as follows ([CouchDB has to be version 3.2 or higher](https://docs.couchdb.org/en/latest/config/http.html#chttpd/enable_cors), if lower `enable_cors = true` has to be under section `[httpd]` ): # Prepare environment variables.
export hostname=localhost:5984
export username=goojdasjdas #Please change as you like.
export password=kpkdasdosakpdsa #Please change as you like
```ini # Prepare directories which saving data and configurations.
[couchdb] mkdir couchdb-data
single_node=true mkdir couchdb-etc
max_document_size = 50000000
[chttpd]
require_valid_user = true
max_http_request_size = 4294967296
enable_cors = true
[chttpd_auth]
require_valid_user = true
authentication_redirect = /_utils/session.html
[httpd]
WWW-Authenticate = Basic realm="couchdb"
bind_address = 0.0.0.0
[cors]
origins = app://obsidian.md, capacitor://localhost, http://localhost
credentials = true
headers = accept, authorization, content-type, origin, referer
methods = GET,PUT,POST,HEAD,DELETE
max_age = 3600
``` ```
## Run #### 2. Run docker container
### Docker CLI 1. Boot Check.
```
$ docker run --name couchdb-for-ols --rm -it -e COUCHDB_USER=${username} -e COUCHDB_PASSWORD=${password} -v ${PWD}/couchdb-data:/opt/couchdb/data -v ${PWD}/couchdb-etc:/opt/couchdb/etc/local.d -p 5984:5984 couchdb
```
If your container has been exited, please check the permission of couchdb-data, and couchdb-etc.
Once CouchDB run, these directories will be owned by uid:`5984`. Please chown it for you again.
You can launch CouchDB using your `local.ini` like this: 2. Enable it in background
``` ```
$ 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 $ docker run --name couchdb-for-ols -d --restart always -e COUCHDB_USER=${username} -e COUCHDB_PASSWORD=${password} -v ${PWD}/couchdb-data:/opt/couchdb/data -v ${PWD}/couchdb-etc:/opt/couchdb/etc/local.d -p 5984:5984 couchdb
``` ```
*Remember to replace the path with the path to your local.ini* ### B. Install CouchDB directly
Please refer the [official document](https://docs.couchdb.org/en/stable/install/index.html). However, we do not have to configure it fully. Just administrator needs to be configured.
Run in detached mode: ## 2. Run couchdb-init.sh for initialise
``` ```
$ 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 $ curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | bash
```
*Remember to replace the path with the path to your local.ini*
### Docker Compose
Create a directory, place your `local.ini` within it, and create a `docker-compose.yml` alongside it. Make sure to have write permissions for `local.ini` and the about to be created `data` folder after the container start. The directory structure should look similar to this:
```
obsidian-livesync
├── docker-compose.yml
└── local.ini
``` ```
A good place to start for `docker-compose.yml`: If it results like following:
```yaml ```
version: "2.1" -- Configuring CouchDB by REST APIs... -->
services: {"ok":true}
couchdb: ""
image: couchdb ""
container_name: obsidian-livesync ""
user: 1000:1000 ""
environment: ""
- COUCHDB_USER=admin ""
- COUCHDB_PASSWORD=password ""
volumes: ""
- ./data:/opt/couchdb/data ""
- ./local.ini:/opt/couchdb/etc/local.ini <-- Configuring CouchDB by REST APIs Done!
ports:
- 5984:5984
restart: unless-stopped
``` ```
And finally launch the container Your CouchDB has been initialised successfully. If you want this manually, please read the script.
## 3. Expose CouchDB to the Internet
- You can skip this instruction if you using only in intranet and only with desktop devices.
- For mobile devices, Obsidian requires a valid SSL certificate. Usually, it needs exposing the internet.
Whatever solutions we can use. For the simplicity, following sample uses Cloudflare Zero Trust for testing.
``` ```
# -d will launch detached so the container runs in background $ cloudflared tunnel --url http://localhost:5984
docker compose up -d 2024-02-14T10:35:25Z INF Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee. If you intend to use Tunnels in production you should use a pre-created named tunnel by following: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
2024-02-14T10:35:25Z INF Requesting new quick Tunnel on trycloudflare.com...
2024-02-14T10:35:26Z INF +--------------------------------------------------------------------------------------------+
2024-02-14T10:35:26Z INF | Your quick Tunnel has been created! Visit it at (it may take some time to be reachable): |
2024-02-14T10:35:26Z INF | https://tiles-photograph-routine-groundwater.trycloudflare.com |
2024-02-14T10:35:26Z INF +--------------------------------------------------------------------------------------------+
:
:
:
```
Now `https://tiles-photograph-routine-groundwater.trycloudflare.com` is our server. Make it into background once please.
## 4. Client Setup
> [!TIP]
> Now manually configuration is not recommended for some reasons. However, if you want to do so, please use `Setup wizard`. The recommended extra configurations will be also set.
### 1. Generate the setup URI on a desktop device or server
```bash
$ export hostname=https://tiles-photograph-routine-groundwater.trycloudflare.com #Point to your vault
$ export database=obsidiannotes #Please change as you like
$ export passphrase=dfsapkdjaskdjasdas #Please change as you like
$ deno run -A https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/flyio/generate_setupuri.ts
obsidian://setuplivesync?settings=%5B%22tm2DpsOE74nJAryprZO2M93wF%2Fvg.......4b26ed33230729%22%5D
``` ```
## Access from a mobile device ### 2. Setup Self-hosted LiveSync to Obsidian
If you want to access Self-hosted LiveSync from mobile devices, you need a valid SSL certificate. [This video](https://youtu.be/7sa_I1832Xc?t=146) may help us.
1. Install Self-hosted LiveSync
2. Choose `Use the copied setup URI` from the command palette and paste the setup URI. (obsidian://setuplivesync?settings=.....).
3. Type `welcome` for setup-uri passphrase.
4. Answer `yes` and `Set it up...`, and finish the first dialogue with `Keep them disabled`.
5. `Reload app without save` once.
### Testing from a mobile ---
In the testing phase, [localhost.run](https://localhost.run/) or something like services is very useful.
example using localhost.run: ## Manual setup information
```
$ ssh -R 80:localhost:5984 nokey@localhost.run
Warning: Permanently added the RSA host key for IP address '35.171.254.69' to the list of known hosts.
===============================================================================
Welcome to localhost.run!
Follow your favourite reverse tunnel at [https://twitter.com/localhost_run].
**You need a SSH key to access this service.**
If you get a permission denied follow Gitlab's most excellent howto:
https://docs.gitlab.com/ee/ssh/
*Only rsa and ed25519 keys are supported*
To set up and manage custom domains go to https://admin.localhost.run/
More details on custom domains (and how to enable subdomains of your custom
domain) at https://localhost.run/docs/custom-domains
To explore using localhost.run visit the documentation site:
https://localhost.run/docs/
===============================================================================
** your connection id is xxxxxxxxxxxxxxxxxxxxxxxxxxxx, please mention it if you send me a message about an issue. **
xxxxxxxx.localhost.run tunneled with tls termination, https://xxxxxxxx.localhost.run
Connection to localhost.run closed by remote host.
Connection to localhost.run closed.
```
https://xxxxxxxx.localhost.run is the temporary server address.
### Setting up your domain ### Setting up your domain

View File

@@ -1,12 +1,66 @@
# Info <!-- 2024-02-15 -->
In this document some tips for solving issues will be given. # Tips and Troubleshooting
- [Notable bugs and fixes](#notable-bugs-and-fixes)
- [FAQ](#faq)
- [Troubleshooting](#troubleshooting)
- [Tips](#tips)
<!-- - -->
# Issue with syncing with mobile device ## Notable bugs and fixes
- If you are getting problem like could not sync files when doing initial sync. Try lower batch size ### Binary files get bigger on iOS
- Open setting - Reported at: v0.20.x
- Open Live Sync settings - Fixed at: v0.21.2 (Fixed but not reviewed)
- Go to sync settings - Required action: larger files will not be fixed automatically, please perform `Verify and repair all files`. If our local database and storage are not matched, we will be asked to apply which one.
- Go down to Advanced settings.
- Lower "Batch size" and "Batch limit" and try to sync again. ### Some setting name has been changed
- If you keep getting error keep lowering until you find the sweet spot. - Fixed at: v0.22.6
| Previous name | New name |
| ---------------------------- | ---------------------------------------- |
| Open setup URI | Use the copied setup URI |
| Copy setup URI | Copy current settings as a new setup URI |
| Setup Wizard | Minimal Setup |
| Check database configuration | Check and Fix database configuration |
## FAQ
### Why `Use an old adapter for compatibility` is somehow enabled in my vault?
Because you are a compassionate and experienced user. Before v0.17.16, we used an old adapter for the local database. At that time, current default adapter has not been stable.
The new adapter has better performance and has a new feature like purging. Therefore, we should use new adapters and current default is so.
However, when switching from an old adapter to a new adapter, some converting or local database rebuilding is required, and it takes a few time. It was a long time ago now, but we once inconvenienced everyone in a hurry when we changed the format of our database.
For these reasons, this toggle is automatically on if we have upgraded from vault which using an old adapter.
When you rebuild everything or fetch from the remote again, you will be asked to switch this.
Therefore, experienced users (especially those stable enough not to have to rebuild the database) may have this toggle enabled in their Vault.
Please disable it when you have enough time.
<!-- Add here -->
## Troubleshooting
<!-- Add here -->
## Tips
<!-- Add here -->
### Old tips
- If a folder becomes empty after a replication, it will be deleted by default. But you can toggle this behaviour. Check the [Settings](settings.md).
- LiveSync mode drains more batteries in mobile devices. Periodic sync with some automatic sync is recommended.
- 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 (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](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 a work in progress.)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,10 +1,10 @@
{ {
"id": "obsidian-livesync", "id": "obsidian-livesync",
"name": "Self-hosted LiveSync", "name": "Self-hosted LiveSync",
"version": "0.20.2", "version": "0.22.7",
"minAppVersion": "0.9.12", "minAppVersion": "0.9.12",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz", "author": "vorotamoroz",
"authorUrl": "https://github.com/vrtmrz", "authorUrl": "https://github.com/vrtmrz",
"isDesktopOnly": false "isDesktopOnly": false
} }

46
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.20.2", "version": "0.22.7",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.20.2", "version": "0.22.7",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"diff-match-patch": "^1.0.5", "diff-match-patch": "^1.0.5",
@@ -36,7 +36,7 @@
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.28.0", "eslint-plugin-import": "^2.28.0",
"events": "^3.3.0", "events": "^3.3.0",
"obsidian": "^1.3.5", "obsidian": "^1.4.11",
"postcss": "^8.4.27", "postcss": "^8.4.27",
"postcss-load-config": "^4.0.1", "postcss-load-config": "^4.0.1",
"pouchdb-adapter-http": "^8.0.1", "pouchdb-adapter-http": "^8.0.1",
@@ -3077,9 +3077,9 @@
"dev": true "dev": true
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.6", "version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -3223,9 +3223,9 @@
} }
}, },
"node_modules/obsidian": { "node_modules/obsidian": {
"version": "1.4.0", "version": "1.4.11",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.4.0.tgz", "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.4.11.tgz",
"integrity": "sha512-fsZMPlxgflGSBSP6P4BjQi5+0MqZl3h6FEDEZ3CNnweNdDw0doyqN3FMO/PGWfuxPT77WicVwUxekuI3e6eCGg==", "integrity": "sha512-BCVYTvaXxElJMl6MMbDdY/CGK+aq18SdtDY/7vH8v6BxCBQ6KF4kKxL0vG9UZ0o5qh139KpUoJHNm+6O5dllKA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/codemirror": "5.60.8", "@types/codemirror": "5.60.8",
@@ -3376,9 +3376,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.27", "version": "8.4.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -3395,7 +3395,7 @@
} }
], ],
"dependencies": { "dependencies": {
"nanoid": "^3.3.6", "nanoid": "^3.3.7",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
}, },
@@ -6817,9 +6817,9 @@
"dev": true "dev": true
}, },
"nanoid": { "nanoid": {
"version": "3.3.6", "version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true "dev": true
}, },
"natural-compare": { "natural-compare": {
@@ -6913,9 +6913,9 @@
} }
}, },
"obsidian": { "obsidian": {
"version": "1.4.0", "version": "1.4.11",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.4.0.tgz", "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.4.11.tgz",
"integrity": "sha512-fsZMPlxgflGSBSP6P4BjQi5+0MqZl3h6FEDEZ3CNnweNdDw0doyqN3FMO/PGWfuxPT77WicVwUxekuI3e6eCGg==", "integrity": "sha512-BCVYTvaXxElJMl6MMbDdY/CGK+aq18SdtDY/7vH8v6BxCBQ6KF4kKxL0vG9UZ0o5qh139KpUoJHNm+6O5dllKA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/codemirror": "5.60.8", "@types/codemirror": "5.60.8",
@@ -7026,12 +7026,12 @@
"dev": true "dev": true
}, },
"postcss": { "postcss": {
"version": "8.4.27", "version": "8.4.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
"dev": true, "dev": true,
"requires": { "requires": {
"nanoid": "^3.3.6", "nanoid": "^3.3.7",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.20.2", "version": "0.22.7",
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"main": "main.js", "main": "main.js",
"type": "module", "type": "module",
@@ -17,9 +17,9 @@
"@types/diff-match-patch": "^1.0.32", "@types/diff-match-patch": "^1.0.32",
"@types/node": "^20.2.5", "@types/node": "^20.2.5",
"@types/pouchdb": "^6.4.0", "@types/pouchdb": "^6.4.0",
"@types/pouchdb-browser": "^6.1.3",
"@types/pouchdb-adapter-http": "^6.1.3", "@types/pouchdb-adapter-http": "^6.1.3",
"@types/pouchdb-adapter-idb": "^6.1.4", "@types/pouchdb-adapter-idb": "^6.1.4",
"@types/pouchdb-browser": "^6.1.3",
"@types/pouchdb-core": "^7.0.11", "@types/pouchdb-core": "^7.0.11",
"@types/pouchdb-mapreduce": "^6.1.7", "@types/pouchdb-mapreduce": "^6.1.7",
"@types/pouchdb-replication": "^6.4.4", "@types/pouchdb-replication": "^6.4.4",
@@ -33,7 +33,7 @@
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.28.0", "eslint-plugin-import": "^2.28.0",
"events": "^3.3.0", "events": "^3.3.0",
"obsidian": "^1.3.5", "obsidian": "^1.4.11",
"postcss": "^8.4.27", "postcss": "^8.4.27",
"postcss-load-config": "^4.0.1", "postcss-load-config": "^4.0.1",
"pouchdb-adapter-http": "^8.0.1", "pouchdb-adapter-http": "^8.0.1",
@@ -60,4 +60,4 @@
"xxhash-wasm": "0.4.2", "xxhash-wasm": "0.4.2",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
} }
} }

View File

@@ -0,0 +1,151 @@
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"provenance": [],
"private_outputs": true,
"authorship_tag": "ABX9TyMexQ5pErH5LBG2tENtEVWf",
"include_colab_link": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
}
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/gist/vrtmrz/9402b101746e08e969b1a4f5f0deb465/setup-flyio-on-the-fly-v2.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "markdown",
"source": [
"- Initial version 7th Feb. 2024"
],
"metadata": {
"id": "AzLlAcLFRO5A"
}
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "z1x8DQpa9opC"
},
"outputs": [],
"source": [
"# Install prerequesties\n",
"!curl -L https://fly.io/install.sh | sh\n",
"!curl -fsSL https://deno.land/x/install/install.sh | sh\n",
"!apt update && apt -y install jq\n",
"import os\n",
"%env PATH=/root/.fly/bin:/root/.deno/bin/:{os.environ[\"PATH\"]}\n",
"!git clone --recursive https://github.com/vrtmrz/obsidian-livesync"
]
},
{
"cell_type": "code",
"source": [
"# Login up sign up\n",
"!flyctl auth signup"
],
"metadata": {
"id": "mGN08BaFDviy"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Select a region and execute the block."
],
"metadata": {
"id": "BBFTFOP6vA8m"
}
},
{
"cell_type": "code",
"source": [
"# see https://fly.io/docs/reference/regions/\n",
"region = \"nrt/Tokyo, Japan\" #@param [\"ams/Amsterdam, Netherlands\",\"arn/Stockholm, Sweden\",\"atl/Atlanta, Georgia (US)\",\"bog/Bogotá, Colombia\",\"bos/Boston, Massachusetts (US)\",\"cdg/Paris, France\",\"den/Denver, Colorado (US)\",\"dfw/Dallas, Texas (US)\",\"ewr/Secaucus, NJ (US)\",\"eze/Ezeiza, Argentina\",\"gdl/Guadalajara, Mexico\",\"gig/Rio de Janeiro, Brazil\",\"gru/Sao Paulo, Brazil\",\"hkg/Hong Kong, Hong Kong\",\"iad/Ashburn, Virginia (US)\",\"jnb/Johannesburg, South Africa\",\"lax/Los Angeles, California (US)\",\"lhr/London, United Kingdom\",\"mad/Madrid, Spain\",\"mia/Miami, Florida (US)\",\"nrt/Tokyo, Japan\",\"ord/Chicago, Illinois (US)\",\"otp/Bucharest, Romania\",\"phx/Phoenix, Arizona (US)\",\"qro/Querétaro, Mexico\",\"scl/Santiago, Chile\",\"sea/Seattle, Washington (US)\",\"sin/Singapore, Singapore\",\"sjc/San Jose, California (US)\",\"syd/Sydney, Australia\",\"waw/Warsaw, Poland\",\"yul/Montreal, Canada\",\"yyz/Toronto, Canada\" ] {allow-input: true}\n",
"%env region={region.split(\"/\")[0]}\n",
"#%env appame=\n",
"#%env username=\n",
"#%env password=\n",
"#%env database=\n",
"#%env passphrase=\n",
"\n",
"# automatic setup leave it -->\n",
"%cd obsidian-livesync/utils/flyio\n",
"!./deploy-server.sh | tee deploy-result.txt\n",
"\n",
"## Show result button\n",
"from IPython.display import HTML\n",
"last_line=\"\"\n",
"with open('deploy-result.txt', 'r') as f:\n",
" last_line = f.readlines()[-1]\n",
" last_line = str.strip(last_line)\n",
"\n",
"if last_line.startswith(\"obsidian://\"):\n",
" result = HTML(f\"Copy your setup-URI with this button! -&gt; <button onclick=\\\"navigator.clipboard.writeText('{last_line}')\\\">Copy setup uri</button><br>Importing passphrase is `welcome`. <br>If you want to synchronise in live mode, please apply a preset after ensuring the imported configuration works.\")\n",
"else:\n",
" result = \"Failed to encrypt the setup URI\"\n",
"result"
],
"metadata": {
"id": "TNl0A603EF9E"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"If you see the `Copy setup URI` button, Congratulations! Your CouchDB is ready to use! Please click the button. And open this on Obsidian.\n",
"\n",
"And, you should keep the output to your secret memo.\n",
"\n"
],
"metadata": {
"id": "oeIzExnEKhFp"
}
},
{
"cell_type": "markdown",
"source": [
"\n",
"\n",
"---\n",
"\n",
"\n",
"If you want to delete this CouchDB instance, you can do it by executing next cell. \n",
"If your fly.toml has been gone, access https://fly.io/dashboard and check the existing app."
],
"metadata": {
"id": "sdQrqOjERN3K"
}
},
{
"cell_type": "code",
"source": [
"!./delete-server.sh"
],
"metadata": {
"id": "7JMSkNvVIIfg"
},
"execution_count": null,
"outputs": []
}
]
}

View File

@@ -1,20 +1,22 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { Notice, type PluginManifest, parseYaml, normalizePath } from "./deps"; import { Notice, type PluginManifest, parseYaml, normalizePath } from "./deps";
import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID, AnyEntry } from "./lib/src/types"; import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID, AnyEntry, SavingEntry } from "./lib/src/types";
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE } from "./lib/src/types"; import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE } from "./lib/src/types";
import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "./types"; import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "./types";
import { delay, getDocData } from "./lib/src/utils"; import { createTextBlob, delay, getDocData, sendSignal, waitForSignal } from "./lib/src/utils";
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
import { WrappedNotice } from "./lib/src/wrapper"; import { WrappedNotice } from "./lib/src/wrapper";
import { readString, crc32CKHash, decodeBinary, encodeBinary } from "./lib/src/strbin"; import { readString, decodeBinary, arrayBufferToBase64, sha1 } from "./lib/src/strbin";
import { serialized } from "./lib/src/lock"; import { serialized } from "./lib/src/lock";
import { LiveSyncCommands } from "./LiveSyncCommands"; import { LiveSyncCommands } from "./LiveSyncCommands";
import { stripAllPrefixes } from "./lib/src/path"; import { stripAllPrefixes } from "./lib/src/path";
import { PeriodicProcessor, askYesNo, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "./utils"; import { PeriodicProcessor, askYesNo, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "./utils";
import { PluginDialogModal } from "./dialogs"; import { PluginDialogModal } from "./dialogs";
import { JsonResolveModal } from "./JsonResolveModal"; import { JsonResolveModal } from "./JsonResolveModal";
import { pipeGeneratorToGenerator, processAllGeneratorTasksWithConcurrencyLimit } from './lib/src/task'; import { QueueProcessor } from './lib/src/processor';
import { pluginScanningCount } from './lib/src/stores';
import type ObsidianLiveSyncPlugin from './main';
const d = "\u200b"; const d = "\u200b";
const d2 = "\n"; const d2 = "\n";
@@ -111,6 +113,7 @@ function deserialize2(str: string): PluginDataEx {
data data
} }
) )
tokens.nextLine();
} while (filename); } while (filename);
return result; return result;
} }
@@ -161,6 +164,16 @@ export type PluginDataEx = {
mtime: number, mtime: number,
}; };
export class ConfigSync extends LiveSyncCommands { export class ConfigSync extends LiveSyncCommands {
constructor(plugin: ObsidianLiveSyncPlugin) {
super(plugin);
pluginScanningCount.onChanged((e) => {
const total = e.value;
pluginIsEnumerating.set(total != 0);
if (total == 0) {
Logger(`Processing configurations done`, LOG_LEVEL_INFO, "get-plugins");
}
})
}
confirmPopup: WrappedNotice = null; confirmPopup: WrappedNotice = null;
get kvDB() { get kvDB() {
return this.plugin.kvDB; return this.plugin.kvDB;
@@ -269,7 +282,7 @@ export class ConfigSync extends LiveSyncCommands {
for (const file of data.files) { for (const file of data.files) {
const work = { ...file }; const work = { ...file };
const tempStr = getDocData(work.data); const tempStr = getDocData(work.data);
work.data = [crc32CKHash(tempStr)]; work.data = [await sha1(tempStr)];
xFiles.push(work); xFiles.push(work);
} }
return ({ return ({
@@ -301,63 +314,71 @@ export class ConfigSync extends LiveSyncCommands {
this.plugin.saveSettingData(); this.plugin.saveSettingData();
} }
} }
pluginScanProcessor = new QueueProcessor(async (v: AnyEntry[]) => {
const plugin = v[0];
const path = plugin.path || this.getPath(plugin);
const oldEntry = (this.pluginList.find(e => e.documentPath == path));
if (oldEntry && oldEntry.mtime == plugin.mtime) return;
try {
const pluginData = await this.loadPluginData(path);
if (pluginData) {
return [pluginData];
}
// Failed to load
return;
} catch (ex) {
Logger(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE);
Logger(ex, LOG_LEVEL_VERBOSE);
}
return;
}, { suspended: true, batchSize: 1, concurrentLimit: 5, delay: 300, yieldThreshold: 10 }).pipeTo(
new QueueProcessor(
async (pluginDataList) => {
// Concurrency is two, therefore, we can unlock the previous awaiting.
sendSignal("plugin-next-load");
let newList = [...this.pluginList];
for (const item of pluginDataList) {
newList = newList.filter(x => x.documentPath != item.documentPath);
newList.push(item)
}
this.pluginList = newList;
pluginList.set(newList);
if (pluginDataList.length != 10) {
// If the queue is going to be empty, await subsequent for a while.
await waitForSignal("plugin-next-load", 1000);
}
return;
}
, { suspended: true, batchSize: 10, concurrentLimit: 2, delay: 250, yieldThreshold: 25, totalRemainingReactiveSource: pluginScanningCount })).startPipeline().root.onIdle(() => {
Logger(`All files enumerated`, LOG_LEVEL_INFO, "get-plugins");
this.createMissingConfigurationEntry();
});
async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise<void> { async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise<void> {
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
// pluginList.set([]); // pluginList.set([]);
if (!this.settings.usePluginSync) { if (!this.settings.usePluginSync) {
this.pluginScanProcessor.clearQueue();
this.pluginList = []; this.pluginList = [];
pluginList.set(this.pluginList) pluginList.set(this.pluginList)
return; return;
} }
await Promise.resolve(); // Just to prevent warning. try {
scheduleTask("update-plugin-list-task", 200, async () => { const updatedDocumentId = updatedDocumentPath ? await this.path2id(updatedDocumentPath) : "";
await serialized("update-plugin-list", async () => { const plugins = updatedDocumentPath ?
try { this.localDatabase.findEntries(updatedDocumentId, updatedDocumentId + "\u{10ffff}", { include_docs: true, key: updatedDocumentId, limit: 1 }) :
const updatedDocumentId = updatedDocumentPath ? await this.path2id(updatedDocumentPath) : ""; this.localDatabase.findEntries(ICXHeader + "", `${ICXHeader}\u{10ffff}`, { include_docs: true });
const plugins = updatedDocumentPath ? for await (const v of plugins) {
this.localDatabase.findEntries(updatedDocumentId, updatedDocumentId + "\u{10ffff}", { include_docs: true, key: updatedDocumentId, limit: 1 }) : const path = v.path || this.getPath(v);
this.localDatabase.findEntries(ICXHeader + "", `${ICXHeader}\u{10ffff}`, { include_docs: true }); if (updatedDocumentPath && updatedDocumentPath != path) continue;
let count = 0; this.pluginScanProcessor.enqueue(v);
pluginIsEnumerating.set(true); }
for await (const v of processAllGeneratorTasksWithConcurrencyLimit(20, pipeGeneratorToGenerator(plugins, async plugin => { } finally {
const path = plugin.path || this.getPath(plugin); pluginIsEnumerating.set(false);
if (updatedDocumentPath && updatedDocumentPath != path) { }
return false; pluginIsEnumerating.set(false);
}
const oldEntry = (this.pluginList.find(e => e.documentPath == path));
if (oldEntry && oldEntry.mtime == plugin.mtime) return false;
try {
count++;
if (count % 10 == 0) Logger(`Enumerating files... ${count}`, logLevel, "get-plugins");
Logger(`plugin-${path}`, LOG_LEVEL_VERBOSE);
return this.loadPluginData(path);
// return entries;
} catch (ex) {
//TODO
Logger(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE);
console.warn(ex);
}
return false;
}))) {
if ("ok" in v) {
if (v.ok !== false) {
let newList = [...this.pluginList];
const item = v.ok;
newList = newList.filter(x => x.documentPath != item.documentPath);
newList.push(item)
if (updatedDocumentPath != "") newList = newList.filter(e => e.documentPath != updatedDocumentPath);
this.pluginList = newList;
pluginList.set(newList);
}
}
}
Logger(`All files enumerated`, logLevel, "get-plugins");
this.createMissingConfigurationEntry();
} finally {
pluginIsEnumerating.set(false);
}
});
});
// return entries; // return entries;
} }
async compareUsingDisplayData(dataA: PluginDataExDisplay, dataB: PluginDataExDisplay) { async compareUsingDisplayData(dataA: PluginDataExDisplay, dataB: PluginDataExDisplay) {
@@ -415,9 +436,9 @@ export class ConfigSync extends LiveSyncCommands {
await this.ensureDirectoryEx(path); await this.ensureDirectoryEx(path);
if (!content) { if (!content) {
const dt = decodeBinary(f.data); const dt = decodeBinary(f.data);
await this.app.vault.adapter.writeBinary(path, dt); await this.vaultAccess.adapterWrite(path, dt);
} else { } else {
await this.app.vault.adapter.write(path, content); await this.vaultAccess.adapterWrite(path, content);
} }
Logger(`Applying ${f.filename} of ${data.displayName || data.name}.. Done`); Logger(`Applying ${f.filename} of ${data.displayName || data.name}.. Done`);
@@ -537,16 +558,16 @@ export class ConfigSync extends LiveSyncCommands {
recentProcessedInternalFiles = [] as string[]; recentProcessedInternalFiles = [] as string[];
async makeEntryFromFile(path: FilePath): Promise<false | PluginDataExFile> { async makeEntryFromFile(path: FilePath): Promise<false | PluginDataExFile> {
const stat = await this.app.vault.adapter.stat(path); const stat = await this.vaultAccess.adapterStat(path);
let version: string | undefined; let version: string | undefined;
let displayName: string | undefined; let displayName: string | undefined;
if (!stat) { if (!stat) {
return false; return false;
} }
const contentBin = await this.app.vault.adapter.readBinary(path); const contentBin = await this.vaultAccess.adapterReadBinary(path);
let content: string[]; let content: string[];
try { try {
content = await encodeBinary(contentBin, this.settings.useV1); content = await arrayBufferToBase64(contentBin);
if (path.toLowerCase().endsWith("/manifest.json")) { if (path.toLowerCase().endsWith("/manifest.json")) {
const v = readString(new Uint8Array(contentBin)); const v = readString(new Uint8Array(contentBin));
try { try {
@@ -649,10 +670,10 @@ export class ConfigSync extends LiveSyncCommands {
return return
} }
const content = serialize(dt); const content = createTextBlob(serialize(dt));
try { try {
const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, false); const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, false);
let saveData: LoadedEntry; let saveData: SavingEntry;
if (old === false) { if (old === false) {
saveData = { saveData = {
_id: id, _id: id,
@@ -661,7 +682,7 @@ export class ConfigSync extends LiveSyncCommands {
mtime, mtime,
ctime: mtime, ctime: mtime,
datatype: "newnote", datatype: "newnote",
size: content.length, size: content.size,
children: [], children: [],
deleted: false, deleted: false,
type: "newnote", type: "newnote",
@@ -676,7 +697,7 @@ export class ConfigSync extends LiveSyncCommands {
...old, ...old,
data: content, data: content,
mtime, mtime,
size: content.length, size: content.size,
datatype: "newnote", datatype: "newnote",
children: [], children: [],
deleted: false, deleted: false,
@@ -698,7 +719,7 @@ export class ConfigSync extends LiveSyncCommands {
async watchVaultRawEventsAsync(path: FilePath) { async watchVaultRawEventsAsync(path: FilePath) {
if (!this.settings.usePluginSync) return false; if (!this.settings.usePluginSync) return false;
if (!this.isTargetPath(path)) return false; if (!this.isTargetPath(path)) return false;
const stat = await this.app.vault.adapter.stat(path); const stat = await this.vaultAccess.adapterStat(path);
// Make sure that target is a file. // Make sure that target is a file.
if (stat && stat.type != "file") if (stat && stat.type != "file")
return false; return false;

View File

@@ -1,16 +1,18 @@
import { normalizePath, type PluginManifest } from "./deps"; import { normalizePath, type PluginManifest } from "./deps";
import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED } from "./lib/src/types"; import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED, type SavingEntry, type DocumentID } from "./lib/src/types";
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "./types"; import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "./types";
import { Parallels, delay, isDocContentSame } from "./lib/src/utils"; import { createBinaryBlob, isDocContentSame, sendSignal } from "./lib/src/utils";
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
import { PouchDB } from "./lib/src/pouchdb-browser.js"; import { PouchDB } from "./lib/src/pouchdb-browser.js";
import { scheduleTask, isInternalMetadata, PeriodicProcessor } from "./utils"; import { isInternalMetadata, PeriodicProcessor } from "./utils";
import { WrappedNotice } from "./lib/src/wrapper"; import { WrappedNotice } from "./lib/src/wrapper";
import { decodeBinary, encodeBinary } from "./lib/src/strbin"; import { decodeBinary, encodeBinary } from "./lib/src/strbin";
import { serialized } from "./lib/src/lock"; import { serialized } from "./lib/src/lock";
import { JsonResolveModal } from "./JsonResolveModal"; import { JsonResolveModal } from "./JsonResolveModal";
import { LiveSyncCommands } from "./LiveSyncCommands"; import { LiveSyncCommands } from "./LiveSyncCommands";
import { addPrefix, stripAllPrefixes } from "./lib/src/path"; import { addPrefix, stripAllPrefixes } from "./lib/src/path";
import { KeyedQueueProcessor, QueueProcessor } from "./lib/src/processor";
import { hiddenFilesEventCount, hiddenFilesProcessingCount } from "./lib/src/stores";
export class HiddenFileSync extends LiveSyncCommands { export class HiddenFileSync extends LiveSyncCommands {
periodicInternalFileScanProcessor: PeriodicProcessor = new PeriodicProcessor(this.plugin, async () => this.settings.syncInternalFiles && this.localDatabase.isReady && await this.syncInternalFilesAndDatabase("push", false)); periodicInternalFileScanProcessor: PeriodicProcessor = new PeriodicProcessor(this.plugin, async () => this.settings.syncInternalFiles && this.localDatabase.isReady && await this.syncInternalFilesAndDatabase("push", false));
@@ -75,22 +77,17 @@ export class HiddenFileSync extends LiveSyncCommands {
return; return;
} }
procInternalFiles: string[] = [];
async execInternalFile() {
await serialized("execInternal", async () => {
const w = [...this.procInternalFiles];
this.procInternalFiles = [];
Logger(`Applying hidden ${w.length} files change...`);
await this.syncInternalFilesAndDatabase("pull", false, false, w);
Logger(`Applying hidden ${w.length} files changed`);
});
}
procInternalFile(filename: string) { procInternalFile(filename: string) {
this.procInternalFiles.push(filename); this.internalFileProcessor.enqueueWithKey(filename, filename);
scheduleTask("procInternal", 500, async () => {
await this.execInternalFile();
});
} }
internalFileProcessor = new KeyedQueueProcessor<string, any>(
async (filenames) => {
Logger(`START :Applying hidden ${filenames.length} files change`, LOG_LEVEL_VERBOSE);
await this.syncInternalFilesAndDatabase("pull", false, false, filenames);
Logger(`DONE :Applying hidden ${filenames.length} files change`, LOG_LEVEL_VERBOSE);
return;
}, { batchSize: 100, concurrentLimit: 1, delay: 10, yieldThreshold: 10, suspended: false, totalRemainingReactiveSource: hiddenFilesEventCount }
);
recentProcessedInternalFiles = [] as string[]; recentProcessedInternalFiles = [] as string[];
async watchVaultRawEventsAsync(path: FilePath) { async watchVaultRawEventsAsync(path: FilePath) {
@@ -103,13 +100,15 @@ export class HiddenFileSync extends LiveSyncCommands {
Logger(`Hidden file skipped: ${path} is synchronized in customization sync.`, LOG_LEVEL_VERBOSE); Logger(`Hidden file skipped: ${path} is synchronized in customization sync.`, LOG_LEVEL_VERBOSE);
return; return;
} }
const stat = await this.app.vault.adapter.stat(path); const stat = await this.vaultAccess.adapterStat(path);
// sometimes folder is coming. // sometimes folder is coming.
if (stat && stat.type != "file") if (stat != null && stat.type != "file") {
return; return;
const storageMTime = ~~((stat && stat.mtime || 0) / 1000); }
const mtime = stat == null ? 0 : stat?.mtime ?? 0;
const storageMTime = ~~((mtime) / 1000);
const key = `${path}-${storageMTime}`; const key = `${path}-${storageMTime}`;
if (this.recentProcessedInternalFiles.contains(key)) { if (mtime != 0 && this.recentProcessedInternalFiles.contains(key)) {
//If recently processed, it may caused by self. //If recently processed, it may caused by self.
return; return;
} }
@@ -137,25 +136,55 @@ export class HiddenFileSync extends LiveSyncCommands {
async resolveConflictOnInternalFiles() { async resolveConflictOnInternalFiles() {
// Scan all conflicted internal files // Scan all conflicted internal files
const conflicted = this.localDatabase.findEntries(ICHeader, ICHeaderEnd, { conflicts: true }); const conflicted = this.localDatabase.findEntries(ICHeader, ICHeaderEnd, { conflicts: true });
for await (const doc of conflicted) { this.conflictResolutionProcessor.suspend();
if (!("_conflicts" in doc)) try {
continue; for await (const doc of conflicted) {
if (isInternalMetadata(doc._id)) { if (!("_conflicts" in doc))
await this.resolveConflictOnInternalFile(doc.path); continue;
if (isInternalMetadata(doc._id)) {
this.conflictResolutionProcessor.enqueue(doc.path);
}
} }
} catch (ex) {
Logger("something went wrong on resolving all conflicted internal files");
Logger(ex, LOG_LEVEL_VERBOSE);
} }
await this.conflictResolutionProcessor.startPipeline().waitForPipeline();
} }
async resolveConflictOnInternalFile(path: FilePathWithPrefix): Promise<boolean> { async resolveByNewerEntry(id: DocumentID, path: FilePathWithPrefix, currentDoc: EntryDoc, currentRev: string, conflictedRev: string) {
const conflictedDoc = await this.localDatabase.getRaw(id, { rev: conflictedRev });
// determine which revision should been deleted.
// simply check modified time
const mtimeCurrent = ("mtime" in currentDoc && currentDoc.mtime) || 0;
const mtimeConflicted = ("mtime" in conflictedDoc && conflictedDoc.mtime) || 0;
// Logger(`Revisions:${new Date(mtimeA).toLocaleString} and ${new Date(mtimeB).toLocaleString}`);
// console.log(`mtime:${mtimeA} - ${mtimeB}`);
const delRev = mtimeCurrent < mtimeConflicted ? currentRev : conflictedRev;
// delete older one.
await this.localDatabase.removeRevision(id, delRev);
Logger(`Older one has been deleted:${path}`);
const cc = await this.localDatabase.getRaw(id, { conflicts: true });
if (cc._conflicts.length == 0) {
await this.extractInternalFileFromDatabase(stripAllPrefixes(path))
} else {
this.conflictResolutionProcessor.enqueue(path);
}
// check the file again
}
conflictResolutionProcessor = new QueueProcessor(async (paths: FilePathWithPrefix[]) => {
const path = paths[0];
sendSignal(`cancel-internal-conflict:${path}`);
try { try {
// Retrieve data // Retrieve data
const id = await this.path2id(path, ICHeader); const id = await this.path2id(path, ICHeader);
const doc = await this.localDatabase.getRaw(id, { conflicts: true }); const doc = await this.localDatabase.getRaw(id, { conflicts: true });
// If there is no conflict, return with false. // If there is no conflict, return with false.
if (!("_conflicts" in doc)) if (!("_conflicts" in doc))
return false; return;
if (doc._conflicts.length == 0) if (doc._conflicts.length == 0)
return false; return;
Logger(`Hidden file conflicted:${path}`); Logger(`Hidden file conflicted:${path}`);
const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0])); const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
const revA = doc._rev; const revA = doc._rev;
@@ -171,49 +200,51 @@ export class HiddenFileSync extends LiveSyncCommands {
if (result) { if (result) {
Logger(`Object merge:${path}`, LOG_LEVEL_INFO); Logger(`Object merge:${path}`, LOG_LEVEL_INFO);
const filename = stripAllPrefixes(path); const filename = stripAllPrefixes(path);
const isExists = await this.app.vault.adapter.exists(filename); const isExists = await this.plugin.vaultAccess.adapterExists(filename);
if (!isExists) { if (!isExists) {
await this.ensureDirectoryEx(filename); await this.ensureDirectoryEx(filename);
} }
await this.app.vault.adapter.write(filename, result); await this.plugin.vaultAccess.adapterWrite(filename, result);
const stat = await this.app.vault.adapter.stat(filename); const stat = await this.vaultAccess.adapterStat(filename);
await this.storeInternalFileToDatabase({ path: filename, ...stat }); await this.storeInternalFileToDatabase({ path: filename, ...stat });
await this.extractInternalFileFromDatabase(filename); await this.extractInternalFileFromDatabase(filename);
await this.localDatabase.removeRaw(id, revB); await this.localDatabase.removeRevision(id, revB);
return this.resolveConflictOnInternalFile(path); this.conflictResolutionProcessor.enqueue(path);
return;
} else { } else {
Logger(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE); Logger(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE);
} }
return [{ path, revA, revB, id, doc }];
const docAMerge = await this.localDatabase.getDBEntry(path, { rev: revA });
const docBMerge = await this.localDatabase.getDBEntry(path, { rev: revB });
if (docAMerge != false && docBMerge != false) {
if (await this.showJSONMergeDialogAndMerge(docAMerge, docBMerge)) {
await delay(200);
// Again for other conflicted revisions.
return this.resolveConflictOnInternalFile(path);
}
return false;
}
} }
const revBDoc = await this.localDatabase.getRaw(id, { rev: revB }); // When not JSON file, resolve conflicts by choosing a newer one.
// determine which revision should been deleted. await this.resolveByNewerEntry(id, path, doc, revA, revB);
// simply check modified time return;
const mtimeA = ("mtime" in doc && doc.mtime) || 0;
const mtimeB = ("mtime" in revBDoc && revBDoc.mtime) || 0;
// Logger(`Revisions:${new Date(mtimeA).toLocaleString} and ${new Date(mtimeB).toLocaleString}`);
// console.log(`mtime:${mtimeA} - ${mtimeB}`);
const delRev = mtimeA < mtimeB ? revA : revB;
// delete older one.
await this.localDatabase.removeRaw(id, delRev);
Logger(`Older one has been deleted:${path}`);
// check the file again
return this.resolveConflictOnInternalFile(path);
} catch (ex) { } catch (ex) {
Logger(`Failed to resolve conflict (Hidden): ${path}`); Logger(`Failed to resolve conflict (Hidden): ${path}`);
Logger(ex, LOG_LEVEL_VERBOSE); Logger(ex, LOG_LEVEL_VERBOSE);
return false; return;
} }
}, {
suspended: false, batchSize: 1, concurrentLimit: 5, delay: 10, keepResultUntilDownstreamConnected: true, yieldThreshold: 10,
pipeTo: new QueueProcessor(async (results) => {
const { id, doc, path, revA, revB } = results[0];
const docAMerge = await this.localDatabase.getDBEntry(path, { rev: revA });
const docBMerge = await this.localDatabase.getDBEntry(path, { rev: revB });
if (docAMerge != false && docBMerge != false) {
if (await this.showJSONMergeDialogAndMerge(docAMerge, docBMerge)) {
// Again for other conflicted revisions.
this.conflictResolutionProcessor.enqueue(path);
}
return;
} else {
// If either revision could not read, force resolving by the newer one.
await this.resolveByNewerEntry(id, path, doc, revA, revB);
}
}, { suspended: false, batchSize: 1, concurrentLimit: 1, delay: 10, keepResultUntilDownstreamConnected: false, yieldThreshold: 10 })
})
queueConflictCheck(path: FilePathWithPrefix) {
this.conflictResolutionProcessor.enqueue(path);
} }
//TODO: Tidy up. Even though it is experimental feature, So dirty... //TODO: Tidy up. Even though it is experimental feature, So dirty...
@@ -278,28 +309,38 @@ export class HiddenFileSync extends LiveSyncCommands {
acc[stripAllPrefixes(this.getPath(cur))] = cur; acc[stripAllPrefixes(this.getPath(cur))] = cur;
return acc; return acc;
}, {} as { [key: string]: InternalFileEntry; }); }, {} as { [key: string]: InternalFileEntry; });
const para = Parallels(); await new QueueProcessor(async (filenames: FilePath[]) => {
for (const filename of allFileNames) { const filename = filenames[0];
processed++; processed++;
if (processed % 100 == 0) { if (processed % 100 == 0) {
Logger(`Hidden file: ${processed}/${fileCount}`, logLevel, "sync_internal"); Logger(`Hidden file: ${processed}/${fileCount}`, logLevel, "sync_internal");
} }
if (!filename) continue; if (!filename) return;
if (ignorePatterns.some(e => filename.match(e))) if (ignorePatterns.some(e => filename.match(e)))
continue; return;
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) { if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
continue; return;
} }
const fileOnStorage = filename in filesMap ? filesMap[filename] : undefined; const fileOnStorage = filename in filesMap ? filesMap[filename] : undefined;
const fileOnDatabase = filename in filesOnDBMap ? filesOnDBMap[filename] : undefined; const fileOnDatabase = filename in filesOnDBMap ? filesOnDBMap[filename] : undefined;
const cache = filename in caches ? caches[filename] : { storageMtime: 0, docMtime: 0 }; return [{
filename,
await para.wait(5); fileOnStorage,
const proc = (async (xFileOnStorage: InternalFileInfo, xFileOnDatabase: InternalFileEntry) => { fileOnDatabase,
}]
}, { suspended: true, batchSize: 1, concurrentLimit: 10, delay: 0, totalRemainingReactiveSource: hiddenFilesProcessingCount })
.pipeTo(new QueueProcessor(async (params) => {
const
{
filename,
fileOnStorage: xFileOnStorage,
fileOnDatabase: xFileOnDatabase
} = params[0];
if (xFileOnStorage && xFileOnDatabase) { if (xFileOnStorage && xFileOnDatabase) {
const cache = filename in caches ? caches[filename] : { storageMtime: 0, docMtime: 0 };
// Both => Synchronize // Both => Synchronize
if ((direction != "pullForce" && direction != "pushForce") && xFileOnDatabase.mtime == cache.docMtime && xFileOnStorage.mtime == cache.storageMtime) { if ((direction != "pullForce" && direction != "pushForce") && xFileOnDatabase.mtime == cache.docMtime && xFileOnStorage.mtime == cache.storageMtime) {
return; return;
@@ -335,16 +376,21 @@ export class HiddenFileSync extends LiveSyncCommands {
} }
} }
} else if (xFileOnStorage && !xFileOnDatabase) { } else if (xFileOnStorage && !xFileOnDatabase) {
await this.storeInternalFileToDatabase(xFileOnStorage); if (direction == "push" || direction == "pushForce" || direction == "safe") {
await this.storeInternalFileToDatabase(xFileOnStorage);
} else {
await this.extractInternalFileFromDatabase(xFileOnStorage.path);
}
} else { } else {
throw new Error("Invalid state on hidden file sync"); throw new Error("Invalid state on hidden file sync");
// Something corrupted? // Something corrupted?
} }
return;
}, { suspended: true, batchSize: 1, concurrentLimit: 5, delay: 0 }))
.root
.enqueueAll(allFileNames)
.startPipeline().waitForPipeline();
});
para.add(proc(fileOnStorage, fileOnDatabase))
}
await para.all();
await this.kvDB.set("diff-caches-internal", caches); await this.kvDB.set("diff-caches-internal", caches);
// When files has been retrieved from the database. they must be reloaded. // When files has been retrieved from the database. they must be reloaded.
@@ -408,10 +454,10 @@ export class HiddenFileSync extends LiveSyncCommands {
const id = await this.path2id(file.path, ICHeader); const id = await this.path2id(file.path, ICHeader);
const prefixedFileName = addPrefix(file.path, ICHeader); const prefixedFileName = addPrefix(file.path, ICHeader);
const contentBin = await this.app.vault.adapter.readBinary(file.path); const contentBin = await this.plugin.vaultAccess.adapterReadBinary(file.path);
let content: string[]; let content: Blob;
try { try {
content = await encodeBinary(contentBin, this.settings.useV1); content = createBinaryBlob(contentBin);
} catch (ex) { } catch (ex) {
Logger(`The file ${file.path} could not be encoded`); Logger(`The file ${file.path} could not be encoded`);
Logger(ex, LOG_LEVEL_VERBOSE); Logger(ex, LOG_LEVEL_VERBOSE);
@@ -421,7 +467,7 @@ export class HiddenFileSync extends LiveSyncCommands {
return await serialized("file-" + prefixedFileName, async () => { return await serialized("file-" + prefixedFileName, async () => {
try { try {
const old = await this.localDatabase.getDBEntry(prefixedFileName, null, false, false); const old = await this.localDatabase.getDBEntry(prefixedFileName, null, false, false);
let saveData: LoadedEntry; let saveData: SavingEntry;
if (old === false) { if (old === false) {
saveData = { saveData = {
_id: id, _id: id,
@@ -436,7 +482,7 @@ export class HiddenFileSync extends LiveSyncCommands {
type: "newnote", type: "newnote",
}; };
} else { } else {
if (isDocContentSame(old.data, content) && !forceWrite) { if (await isDocContentSame(old.data, content) && !forceWrite) {
// Logger(`STORAGE --> DB:${file.path}: (hidden) Not changed`, LOG_LEVEL_VERBOSE); // Logger(`STORAGE --> DB:${file.path}: (hidden) Not changed`, LOG_LEVEL_VERBOSE);
return; return;
} }
@@ -486,6 +532,14 @@ export class HiddenFileSync extends LiveSyncCommands {
type: "newnote", type: "newnote",
}; };
} else { } else {
// Remove all conflicted before deleting.
const conflicts = await this.localDatabase.getRaw(old._id, { conflicts: true });
if ("_conflicts" in conflicts) {
for (const conflictRev of conflicts._conflicts) {
await this.localDatabase.removeRevision(old._id, conflictRev);
Logger(`STORAGE -x> DB:${filename}: (hidden) conflict removed ${old._rev} => ${conflictRev}`, LOG_LEVEL_VERBOSE);
}
}
if (old.deleted) { if (old.deleted) {
Logger(`STORAGE -x> DB:${filename}: (hidden) already deleted`); Logger(`STORAGE -x> DB:${filename}: (hidden) already deleted`);
return; return;
@@ -511,7 +565,7 @@ export class HiddenFileSync extends LiveSyncCommands {
} }
async extractInternalFileFromDatabase(filename: FilePath, force = false) { async extractInternalFileFromDatabase(filename: FilePath, force = false) {
const isExists = await this.app.vault.adapter.exists(filename); const isExists = await this.plugin.vaultAccess.adapterExists(filename);
const prefixedFileName = addPrefix(filename, ICHeader); const prefixedFileName = addPrefix(filename, ICHeader);
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) { if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
return; return;
@@ -519,8 +573,7 @@ export class HiddenFileSync extends LiveSyncCommands {
return await serialized("file-" + prefixedFileName, async () => { return await serialized("file-" + prefixedFileName, async () => {
try { try {
// Check conflicted status // Check conflicted status
//TODO option const fileOnDB = await this.localDatabase.getDBEntry(prefixedFileName, { conflicts: true }, false, true, true);
const fileOnDB = await this.localDatabase.getDBEntry(prefixedFileName, { conflicts: true }, false, true);
if (fileOnDB === false) if (fileOnDB === false)
throw new Error(`File not found on database.:${filename}`); throw new Error(`File not found on database.:${filename}`);
// Prevent overwrite for Prevent overwriting while some conflicted revision exists. // Prevent overwrite for Prevent overwriting while some conflicted revision exists.
@@ -528,13 +581,13 @@ export class HiddenFileSync extends LiveSyncCommands {
Logger(`Hidden file ${filename} has conflicted revisions, to keep in safe, writing to storage has been prevented`, LOG_LEVEL_INFO); Logger(`Hidden file ${filename} has conflicted revisions, to keep in safe, writing to storage has been prevented`, LOG_LEVEL_INFO);
return; return;
} }
const deleted = "deleted" in fileOnDB ? fileOnDB.deleted : false; const deleted = fileOnDB.deleted || fileOnDB._deleted || false;
if (deleted) { if (deleted) {
if (!isExists) { if (!isExists) {
Logger(`STORAGE <x- DB:${filename}: deleted (hidden) Deleted on DB, but the file is already not found on storage.`); Logger(`STORAGE <x- DB:${filename}: deleted (hidden) Deleted on DB, but the file is already not found on storage.`);
} else { } else {
Logger(`STORAGE <x- DB:${filename}: deleted (hidden).`); Logger(`STORAGE <x- DB:${filename}: deleted (hidden).`);
await this.app.vault.adapter.remove(filename); await this.plugin.vaultAccess.adapterRemove(filename);
try { try {
//@ts-ignore internalAPI //@ts-ignore internalAPI
await this.app.vault.adapter.reconcileInternalFile(filename); await this.app.vault.adapter.reconcileInternalFile(filename);
@@ -547,7 +600,7 @@ export class HiddenFileSync extends LiveSyncCommands {
} }
if (!isExists) { if (!isExists) {
await this.ensureDirectoryEx(filename); await this.ensureDirectoryEx(filename);
await this.app.vault.adapter.writeBinary(filename, decodeBinary(fileOnDB.data), { mtime: fileOnDB.mtime, ctime: fileOnDB.ctime }); await this.plugin.vaultAccess.adapterWrite(filename, decodeBinary(fileOnDB.data), { mtime: fileOnDB.mtime, ctime: fileOnDB.ctime });
try { try {
//@ts-ignore internalAPI //@ts-ignore internalAPI
await this.app.vault.adapter.reconcileInternalFile(filename); await this.app.vault.adapter.reconcileInternalFile(filename);
@@ -558,13 +611,13 @@ export class HiddenFileSync extends LiveSyncCommands {
Logger(`STORAGE <-- DB:${filename}: written (hidden,new${force ? ", force" : ""})`); Logger(`STORAGE <-- DB:${filename}: written (hidden,new${force ? ", force" : ""})`);
return true; return true;
} else { } else {
const contentBin = await this.app.vault.adapter.readBinary(filename); const contentBin = await this.plugin.vaultAccess.adapterReadBinary(filename);
const content = await encodeBinary(contentBin, this.settings.useV1); const content = await encodeBinary(contentBin);
if (isDocContentSame(content, fileOnDB.data) && !force) { if (await isDocContentSame(content, fileOnDB.data) && !force) {
// Logger(`STORAGE <-- DB:${filename}: skipped (hidden) Not changed`, LOG_LEVEL_VERBOSE); // Logger(`STORAGE <-- DB:${filename}: skipped (hidden) Not changed`, LOG_LEVEL_VERBOSE);
return true; return true;
} }
await this.app.vault.adapter.writeBinary(filename, decodeBinary(fileOnDB.data), { mtime: fileOnDB.mtime, ctime: fileOnDB.ctime }); await this.plugin.vaultAccess.adapterWrite(filename, decodeBinary(fileOnDB.data), { mtime: fileOnDB.mtime, ctime: fileOnDB.ctime });
try { try {
//@ts-ignore internalAPI //@ts-ignore internalAPI
await this.app.vault.adapter.reconcileInternalFile(filename); await this.app.vault.adapter.reconcileInternalFile(filename);
@@ -587,7 +640,7 @@ export class HiddenFileSync extends LiveSyncCommands {
showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise<boolean> { showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise<boolean> {
return serialized("conflict:merge-data", () => new Promise((res) => { return new Promise((res) => {
Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE); Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
const docs = [docA, docB]; const docs = [docA, docB];
const path = stripAllPrefixes(docA.path); const path = stripAllPrefixes(docA.path);
@@ -613,12 +666,12 @@ export class HiddenFileSync extends LiveSyncCommands {
} }
} }
if (!keep && result) { if (!keep && result) {
const isExists = await this.app.vault.adapter.exists(filename); const isExists = await this.plugin.vaultAccess.adapterExists(filename);
if (!isExists) { if (!isExists) {
await this.ensureDirectoryEx(filename); await this.ensureDirectoryEx(filename);
} }
await this.app.vault.adapter.write(filename, result); await this.plugin.vaultAccess.adapterWrite(filename, result);
const stat = await this.app.vault.adapter.stat(filename); const stat = await this.plugin.vaultAccess.adapterStat(filename);
await this.storeInternalFileToDatabase({ path: filename, ...stat }, true); await this.storeInternalFileToDatabase({ path: filename, ...stat }, true);
try { try {
//@ts-ignore internalAPI //@ts-ignore internalAPI
@@ -641,7 +694,7 @@ export class HiddenFileSync extends LiveSyncCommands {
} }
}); });
modal.open(); modal.open();
})); });
} }
async scanInternalFiles(): Promise<InternalFileInfo[]> { async scanInternalFiles(): Promise<InternalFileInfo[]> {
@@ -657,7 +710,7 @@ export class HiddenFileSync extends LiveSyncCommands {
const files = filenames.filter(path => synchronisedInConfigSync.every(filterFile => !path.toLowerCase().startsWith(filterFile))).map(async (e) => { const files = filenames.filter(path => synchronisedInConfigSync.every(filterFile => !path.toLowerCase().startsWith(filterFile))).map(async (e) => {
return { return {
path: e as FilePath, path: e as FilePath,
stat: await this.app.vault.adapter.stat(e) stat: await this.plugin.vaultAccess.adapterStat(e)
}; };
}); });
const result: InternalFileInfo[] = []; const result: InternalFileInfo[] = [];

View File

@@ -1,8 +1,8 @@
import { normalizePath, type PluginManifest } from "./deps"; import { normalizePath, type PluginManifest } from "./deps";
import type { DocumentID, EntryDoc, FilePathWithPrefix, LoadedEntry } from "./lib/src/types"; import type { DocumentID, EntryDoc, FilePathWithPrefix, LoadedEntry, SavingEntry } from "./lib/src/types";
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types"; import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types";
import { type PluginDataEntry, PERIODIC_PLUGIN_SWEEP, type PluginList, type DevicePluginList, PSCHeader, PSCHeaderEnd } from "./types"; import { type PluginDataEntry, PERIODIC_PLUGIN_SWEEP, type PluginList, type DevicePluginList, PSCHeader, PSCHeaderEnd } from "./types";
import { getDocData, isDocContentSame } from "./lib/src/utils"; import { createTextBlob, getDocData, isDocContentSame } from "./lib/src/utils";
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
import { PouchDB } from "./lib/src/pouchdb-browser.js"; import { PouchDB } from "./lib/src/pouchdb-browser.js";
import { isPluginMetadata, PeriodicProcessor } from "./utils"; import { isPluginMetadata, PeriodicProcessor } from "./utils";
@@ -186,18 +186,17 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
} }
Logger(`Reading plugin:${m.name}(${m.id})`, LOG_LEVEL_VERBOSE); Logger(`Reading plugin:${m.name}(${m.id})`, LOG_LEVEL_VERBOSE);
const path = normalizePath(m.dir) + "/"; const path = normalizePath(m.dir) + "/";
const adapter = this.app.vault.adapter;
const files = ["manifest.json", "main.js", "styles.css", "data.json"]; const files = ["manifest.json", "main.js", "styles.css", "data.json"];
const pluginData: { [key: string]: string; } = {}; const pluginData: { [key: string]: string; } = {};
for (const file of files) { for (const file of files) {
const thePath = path + file; const thePath = path + file;
if (await adapter.exists(thePath)) { if (await this.plugin.vaultAccess.adapterExists(thePath)) {
pluginData[file] = await adapter.read(thePath); pluginData[file] = await this.plugin.vaultAccess.adapterRead(thePath);
} }
} }
let mtime = 0; let mtime = 0;
if (await adapter.exists(path + "/data.json")) { if (await this.plugin.vaultAccess.adapterExists(path + "/data.json")) {
mtime = (await adapter.stat(path + "/data.json")).mtime; mtime = (await this.plugin.vaultAccess.adapterStat(path + "/data.json")).mtime;
} }
const p: PluginDataEntry = { const p: PluginDataEntry = {
@@ -211,13 +210,14 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
mtime: mtime, mtime: mtime,
type: "plugin", type: "plugin",
}; };
const d: LoadedEntry = { const blob = createTextBlob(JSON.stringify(p));
const d: SavingEntry = {
_id: p._id, _id: p._id,
path: p._id as string as FilePathWithPrefix, path: p._id as string as FilePathWithPrefix,
data: JSON.stringify(p), data: blob,
ctime: mtime, ctime: mtime,
mtime: mtime, mtime: mtime,
size: 0, size: blob.size,
children: [], children: [],
datatype: "plain", datatype: "plain",
type: "plain" type: "plain"
@@ -228,7 +228,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
if (old !== false) { if (old !== false) {
const oldData = { data: old.data, deleted: old._deleted }; const oldData = { data: old.data, deleted: old._deleted };
const newData = { data: d.data, deleted: d._deleted }; const newData = { data: d.data, deleted: d._deleted };
if (isDocContentSame(oldData.data, newData.data) && oldData.deleted == newData.deleted) { if (await isDocContentSame(oldData.data, newData.data) && oldData.deleted == newData.deleted) {
Logger(`Nothing changed:${m.name}`); Logger(`Nothing changed:${m.name}`);
return; return;
} }
@@ -268,7 +268,6 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
async applyPluginData(plugin: PluginDataEntry) { async applyPluginData(plugin: PluginDataEntry) {
await serialized("plugin-" + plugin.manifest.id, async () => { await serialized("plugin-" + plugin.manifest.id, async () => {
const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/"; const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/";
const adapter = this.app.vault.adapter;
// @ts-ignore // @ts-ignore
const stat = this.app.plugins.enabledPlugins.has(plugin.manifest.id) == true; const stat = this.app.plugins.enabledPlugins.has(plugin.manifest.id) == true;
if (stat) { if (stat) {
@@ -277,7 +276,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL_NOTICE); Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL_NOTICE);
} }
if (plugin.dataJson) if (plugin.dataJson)
await adapter.write(pluginTargetFolderPath + "data.json", plugin.dataJson); await this.plugin.vaultAccess.adapterWrite(pluginTargetFolderPath + "data.json", plugin.dataJson);
Logger("wrote:" + pluginTargetFolderPath + "data.json", LOG_LEVEL_NOTICE); Logger("wrote:" + pluginTargetFolderPath + "data.json", LOG_LEVEL_NOTICE);
if (stat) { if (stat) {
// @ts-ignore // @ts-ignore
@@ -298,14 +297,13 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
} }
const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/"; const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/";
const adapter = this.app.vault.adapter; if ((await this.plugin.vaultAccess.adapterExists(pluginTargetFolderPath)) === false) {
if ((await adapter.exists(pluginTargetFolderPath)) === false) { await this.app.vault.adapter.mkdir(pluginTargetFolderPath);
await adapter.mkdir(pluginTargetFolderPath);
} }
await adapter.write(pluginTargetFolderPath + "main.js", plugin.mainJs); await this.plugin.vaultAccess.adapterWrite(pluginTargetFolderPath + "main.js", plugin.mainJs);
await adapter.write(pluginTargetFolderPath + "manifest.json", plugin.manifestJson); await this.plugin.vaultAccess.adapterWrite(pluginTargetFolderPath + "manifest.json", plugin.manifestJson);
if (plugin.styleCss) if (plugin.styleCss)
await adapter.write(pluginTargetFolderPath + "styles.css", plugin.styleCss); await this.plugin.vaultAccess.adapterWrite(pluginTargetFolderPath + "styles.css", plugin.styleCss);
if (stat) { if (stat) {
// @ts-ignore // @ts-ignore
await this.app.plugins.loadPlugin(plugin.manifest.id); await this.app.plugins.loadPlugin(plugin.manifest.id);

View File

@@ -5,7 +5,7 @@ import { PouchDB } from "./lib/src/pouchdb-browser.js";
import { askSelectString, askYesNo, askString } from "./utils"; import { askSelectString, askYesNo, askString } from "./utils";
import { decrypt, encrypt } from "./lib/src/e2ee_v2"; import { decrypt, encrypt } from "./lib/src/e2ee_v2";
import { LiveSyncCommands } from "./LiveSyncCommands"; import { LiveSyncCommands } from "./LiveSyncCommands";
import { delay } from "./lib/src/utils"; import { delay, fireAndForget } from "./lib/src/utils";
import { confirmWithMessage } from "./dialogs"; import { confirmWithMessage } from "./dialogs";
import { Platform } from "./deps"; import { Platform } from "./deps";
import { fetchAllUsedChunks } from "./lib/src/utils_couchdb"; import { fetchAllUsedChunks } from "./lib/src/utils_couchdb";
@@ -17,25 +17,25 @@ export class SetupLiveSync extends LiveSyncCommands {
this.plugin.addCommand({ this.plugin.addCommand({
id: "livesync-copysetupuri", id: "livesync-copysetupuri",
name: "Copy the setup URI", name: "Copy settings as a new setup URI",
callback: this.command_copySetupURI.bind(this), callback: () => fireAndForget(this.command_copySetupURI()),
}); });
this.plugin.addCommand({ this.plugin.addCommand({
id: "livesync-copysetupuri-short", id: "livesync-copysetupuri-short",
name: "Copy the setup URI (With customization sync)", name: "Copy settings as a new setup URI (With customization sync)",
callback: this.command_copySetupURIWithSync.bind(this), callback: () => fireAndForget(this.command_copySetupURIWithSync()),
}); });
this.plugin.addCommand({ this.plugin.addCommand({
id: "livesync-copysetupurifull", id: "livesync-copysetupurifull",
name: "Copy the setup URI (Full)", name: "Copy settings as a new setup URI (Full)",
callback: this.command_copySetupURIFull.bind(this), callback: () => fireAndForget(this.command_copySetupURIFull()),
}); });
this.plugin.addCommand({ this.plugin.addCommand({
id: "livesync-opensetupuri", id: "livesync-opensetupuri",
name: "Open the setup URI", name: "Use the copied setup URI (Formerly Open setup URI)",
callback: this.command_openSetupURI.bind(this), callback: () => fireAndForget(this.command_openSetupURI()),
}); });
} }
onInitializeDatabase(showNotice: boolean) { } onInitializeDatabase(showNotice: boolean) { }
@@ -60,7 +60,7 @@ export class SetupLiveSync extends LiveSyncCommands {
delete setting[k]; delete setting[k];
} }
} }
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false, false)); const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false));
const uri = `${configURIBase}${encryptedSetting}`; const uri = `${configURIBase}${encryptedSetting}`;
await navigator.clipboard.writeText(uri); await navigator.clipboard.writeText(uri);
Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
@@ -70,13 +70,13 @@ export class SetupLiveSync extends LiveSyncCommands {
if (encryptingPassphrase === false) if (encryptingPassphrase === false)
return; return;
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" }; const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false, false)); const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false));
const uri = `${configURIBase}${encryptedSetting}`; const uri = `${configURIBase}${encryptedSetting}`;
await navigator.clipboard.writeText(uri); await navigator.clipboard.writeText(uri);
Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
} }
async command_copySetupURIWithSync() { async command_copySetupURIWithSync() {
this.command_copySetupURI(false); await this.command_copySetupURI(false);
} }
async command_openSetupURI() { async command_openSetupURI() {
const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`); const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`);
@@ -108,6 +108,7 @@ export class SetupLiveSync extends LiveSyncCommands {
newSettingW.configPassphraseStore = ""; newSettingW.configPassphraseStore = "";
newSettingW.encryptedPassphrase = ""; newSettingW.encryptedPassphrase = "";
newSettingW.encryptedCouchDBConnection = ""; newSettingW.encryptedCouchDBConnection = "";
newSettingW.additionalSuffixOfDatabaseName = `${("appId" in this.app ? this.app.appId : "")}`
const setupJustImport = "Just import setting"; const setupJustImport = "Just import setting";
const setupAsNew = "Set it up as secondary or subsequent device"; const setupAsNew = "Set it up as secondary or subsequent device";
const setupAsMerge = "Secondary device but try keeping local changes"; const setupAsMerge = "Secondary device but try keeping local changes";
@@ -115,6 +116,7 @@ export class SetupLiveSync extends LiveSyncCommands {
const setupManually = "Leave everything to me"; const setupManually = "Leave everything to me";
newSettingW.syncInternalFiles = false; newSettingW.syncInternalFiles = false;
newSettingW.usePluginSync = false; newSettingW.usePluginSync = false;
newSettingW.isConfigured = true;
// Migrate completely obsoleted configuration. // Migrate completely obsoleted configuration.
if (!newSettingW.useIndexedDBAdapter) { if (!newSettingW.useIndexedDBAdapter) {
newSettingW.useIndexedDBAdapter = true; newSettingW.useIndexedDBAdapter = true;
@@ -321,7 +323,6 @@ Of course, we are able to disable these features.`
this.plugin.settings.suspendFileWatching = false; this.plugin.settings.suspendFileWatching = false;
await this.plugin.syncAllFiles(true); await this.plugin.syncAllFiles(true);
await this.plugin.loadQueuedFiles(); await this.plugin.loadQueuedFiles();
this.plugin.procQueuedFiles();
await this.plugin.saveSettings(); await this.plugin.saveSettings();
} }
@@ -334,10 +335,18 @@ Of course, we are able to disable these features.`
const ret = await confirmWithMessage(this.plugin, "Database adapter", message, choices, CHOICE_YES, 10); const ret = await confirmWithMessage(this.plugin, "Database adapter", message, choices, CHOICE_YES, 10);
if (ret == CHOICE_YES) { if (ret == CHOICE_YES) {
this.plugin.settings.useIndexedDBAdapter = false; this.plugin.settings.useIndexedDBAdapter = true;
} }
} }
} }
async resetLocalDatabase() {
if (this.plugin.settings.isConfigured && this.plugin.settings.additionalSuffixOfDatabaseName == "") {
// Discard the non-suffixed database
await this.plugin.resetLocalDatabase();
}
this.plugin.settings.additionalSuffixOfDatabaseName = `${("appId" in this.app ? this.app.appId : "")}`
await this.plugin.resetLocalDatabase();
}
async fetchRemoteChunks() { async fetchRemoteChunks() {
if (!this.plugin.settings.doNotSuspendOnFetching && this.plugin.settings.readChunksOnline) { if (!this.plugin.settings.doNotSuspendOnFetching && this.plugin.settings.readChunksOnline) {
Logger(`Fetching chunks`, LOG_LEVEL_NOTICE); Logger(`Fetching chunks`, LOG_LEVEL_NOTICE);
@@ -352,10 +361,11 @@ Of course, we are able to disable these features.`
} }
async fetchLocal() { async fetchLocal() {
this.suspendExtraSync(); this.suspendExtraSync();
this.askUseNewAdapter(); await this.askUseNewAdapter();
this.plugin.settings.isConfigured = true;
await this.suspendReflectingDatabase(); await this.suspendReflectingDatabase();
await this.plugin.realizeSettingSyncMode(); await this.plugin.realizeSettingSyncMode();
await this.plugin.resetLocalDatabase(); await this.resetLocalDatabase();
await delay(1000); await delay(1000);
await this.plugin.markRemoteResolved(); await this.plugin.markRemoteResolved();
await this.plugin.openDatabase(); await this.plugin.openDatabase();
@@ -370,10 +380,11 @@ Of course, we are able to disable these features.`
} }
async fetchLocalWithKeepLocal() { async fetchLocalWithKeepLocal() {
this.suspendExtraSync(); this.suspendExtraSync();
this.askUseNewAdapter(); await this.askUseNewAdapter();
this.plugin.settings.isConfigured = true;
await this.suspendReflectingDatabase(); await this.suspendReflectingDatabase();
await this.plugin.realizeSettingSyncMode(); await this.plugin.realizeSettingSyncMode();
await this.plugin.resetLocalDatabase(); await this.resetLocalDatabase();
await delay(1000); await delay(1000);
await this.plugin.initializeDatabase(true); await this.plugin.initializeDatabase(true);
await this.plugin.markRemoteResolved(); await this.plugin.markRemoteResolved();
@@ -389,6 +400,7 @@ Of course, we are able to disable these features.`
} }
async rebuildRemote() { async rebuildRemote() {
this.suspendExtraSync(); this.suspendExtraSync();
this.plugin.settings.isConfigured = true;
await this.plugin.realizeSettingSyncMode(); await this.plugin.realizeSettingSyncMode();
await this.plugin.markRemoteLocked(); await this.plugin.markRemoteLocked();
await this.plugin.tryResetRemoteDatabase(); await this.plugin.tryResetRemoteDatabase();
@@ -402,9 +414,10 @@ Of course, we are able to disable these features.`
} }
async rebuildEverything() { async rebuildEverything() {
this.suspendExtraSync(); this.suspendExtraSync();
this.askUseNewAdapter(); await this.askUseNewAdapter();
this.plugin.settings.isConfigured = true;
await this.plugin.realizeSettingSyncMode(); await this.plugin.realizeSettingSyncMode();
await this.plugin.resetLocalDatabase(); await this.resetLocalDatabase();
await delay(1000); await delay(1000);
await this.plugin.initializeDatabase(true); await this.plugin.initializeDatabase(true);
await this.plugin.markRemoteLocked(); await this.plugin.markRemoteLocked();

View File

@@ -1,23 +1,41 @@
import { App, Modal } from "./deps"; import { App, Modal } from "./deps";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch"; import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
import { type diff_result } from "./lib/src/types"; import { CANCELLED, LEAVE_TO_SUBSEQUENT, RESULT_TIMED_OUT, type diff_result } from "./lib/src/types";
import { escapeStringToHTML } from "./lib/src/strbin"; import { escapeStringToHTML } from "./lib/src/strbin";
import { delay, sendValue, waitForValue } from "./lib/src/utils";
export type MergeDialogResult = typeof LEAVE_TO_SUBSEQUENT | typeof CANCELLED | string;
export class ConflictResolveModal extends Modal { export class ConflictResolveModal extends Modal {
// result: Array<[number, string]>;
result: diff_result; result: diff_result;
filename: string; filename: string;
callback: (remove_rev: string) => Promise<void>;
constructor(app: App, filename: string, diff: diff_result, callback: (remove_rev: string) => Promise<void>) { response: MergeDialogResult = CANCELLED;
isClosed = false;
consumed = false;
constructor(app: App, filename: string, diff: diff_result) {
super(app); super(app);
this.result = diff; this.result = diff;
this.callback = callback;
this.filename = filename; this.filename = filename;
// Send cancel signal for the previous merge dialogue
// if not there, simply be ignored.
// sendValue("close-resolve-conflict:" + this.filename, false);
sendValue("cancel-resolve-conflict:" + this.filename, true);
} }
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
// Send cancel signal for the previous merge dialogue
// if not there, simply be ignored.
sendValue("cancel-resolve-conflict:" + this.filename, true);
setTimeout(async () => {
const forceClose = await waitForValue("cancel-resolve-conflict:" + this.filename);
// debugger;
if (forceClose) {
this.sendResponse(CANCELLED);
}
}, 10)
// sendValue("close-resolve-conflict:" + this.filename, false);
this.titleEl.setText("Conflicting changes"); this.titleEl.setText("Conflicting changes");
contentEl.empty(); contentEl.empty();
contentEl.createEl("span", { text: this.filename }); contentEl.createEl("span", { text: this.filename });
@@ -44,42 +62,32 @@ export class ConflictResolveModal extends Modal {
div2.innerHTML = ` div2.innerHTML = `
<span class='deleted'>A:${date1}</span><br /><span class='added'>B:${date2}</span><br> <span class='deleted'>A:${date1}</span><br /><span class='added'>B:${date2}</span><br>
`; `;
contentEl.createEl("button", { text: "Keep A" }, (e) => { contentEl.createEl("button", { text: "Keep A" }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.right.rev)));
e.addEventListener("click", async () => { contentEl.createEl("button", { text: "Keep B" }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.left.rev)));
const callback = this.callback; contentEl.createEl("button", { text: "Concat both" }, (e) => e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT)));
this.callback = null; contentEl.createEl("button", { text: "Not now" }, (e) => e.addEventListener("click", () => this.sendResponse(CANCELLED)));
this.close(); }
await callback(this.result.right.rev);
}); sendResponse(result: MergeDialogResult) {
}); this.response = result;
contentEl.createEl("button", { text: "Keep B" }, (e) => { this.close();
e.addEventListener("click", async () => {
const callback = this.callback;
this.callback = null;
this.close();
await callback(this.result.left.rev);
});
});
contentEl.createEl("button", { text: "Concat both" }, (e) => {
e.addEventListener("click", async () => {
const callback = this.callback;
this.callback = null;
this.close();
await callback("");
});
});
contentEl.createEl("button", { text: "Not now" }, (e) => {
e.addEventListener("click", () => {
this.close();
});
});
} }
onClose() { onClose() {
const { contentEl } = this; const { contentEl } = this;
contentEl.empty(); contentEl.empty();
if (this.callback != null) { if (this.consumed) {
this.callback(null); return;
} }
this.consumed = true;
sendValue("close-resolve-conflict:" + this.filename, this.response);
sendValue("cancel-resolve-conflict:" + this.filename, false);
} }
}
async waitForResult(): Promise<MergeDialogResult> {
await delay(100);
const r = await waitForValue<MergeDialogResult>("close-resolve-conflict:" + this.filename);
if (r === RESULT_TIMED_OUT) return CANCELLED;
return r;
}
}

View File

@@ -6,8 +6,35 @@ import { type DocumentID, type FilePathWithPrefix, type LoadedEntry, LOG_LEVEL_I
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
import { isErrorOfMissingDoc } from "./lib/src/utils_couchdb"; import { isErrorOfMissingDoc } from "./lib/src/utils_couchdb";
import { getDocData } from "./lib/src/utils"; import { getDocData } from "./lib/src/utils";
import { stripPrefix } from "./lib/src/path"; import { isPlainText, stripPrefix } from "./lib/src/path";
function isImage(path: string) {
const ext = path.split(".").splice(-1)[0].toLowerCase();
return ["png", "jpg", "jpeg", "gif", "bmp", "webp"].includes(ext);
}
function isComparableText(path: string) {
const ext = path.split(".").splice(-1)[0].toLowerCase();
return isPlainText(path) || ["md", "mdx", "txt", "json"].includes(ext);
}
function isComparableTextDecode(path: string) {
const ext = path.split(".").splice(-1)[0].toLowerCase();
return ["json"].includes(ext)
}
function readDocument(w: LoadedEntry) {
if (w.data.length == 0) return "";
if (isImage(w.path)) {
return new Uint8Array(decodeBinary(w.data));
}
if (w.data == "plain") return getDocData(w.data);
if (isComparableTextDecode(w.path)) return readString(new Uint8Array(decodeBinary(w.data)));
if (isComparableText(w.path)) return getDocData(w.data);
try {
return readString(new Uint8Array(decodeBinary(w.data)));
} catch (ex) {
// NO OP.
}
return getDocData(w.data);
}
export class DocumentHistoryModal extends Modal { export class DocumentHistoryModal extends Modal {
plugin: ObsidianLiveSyncPlugin; plugin: ObsidianLiveSyncPlugin;
range!: HTMLInputElement; range!: HTMLInputElement;
@@ -45,7 +72,7 @@ export class DocumentHistoryModal extends Modal {
} }
const db = this.plugin.localDatabase; const db = this.plugin.localDatabase;
try { try {
const w = await db.localDatabase.get(this.id, { revs_info: true }); const w = await db.getRaw(this.id, { 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 = `${Math.max(this.revs_info.length - 1, 0)}`; this.range.max = `${Math.max(this.revs_info.length - 1, 0)}`;
this.range.value = this.range.max; this.range.value = this.range.max;
@@ -56,7 +83,6 @@ export class DocumentHistoryModal extends Modal {
this.range.max = "0"; this.range.max = "0";
this.range.value = ""; this.range.value = "";
this.range.disabled = true; this.range.disabled = true;
this.showDiff
this.contentView.setText(`History of this file was not recorded.`); this.contentView.setText(`History of this file was not recorded.`);
} else { } else {
this.contentView.setText(`Error occurred.`); this.contentView.setText(`Error occurred.`);
@@ -76,6 +102,22 @@ export class DocumentHistoryModal extends Modal {
const rev = this.revs_info[index]; const rev = this.revs_info[index];
await this.showExactRev(rev.rev); await this.showExactRev(rev.rev);
} }
BlobURLs = new Map<string, string>();
revokeURL(key: string) {
const v = this.BlobURLs.get(key);
if (v) {
URL.revokeObjectURL(v);
}
this.BlobURLs.set(key, undefined);
}
generateBlobURL(key: string, data: Uint8Array) {
this.revokeURL(key);
const v = URL.createObjectURL(new Blob([data], { endings: "transparent", type: "application/octet-stream" }));
this.BlobURLs.set(key, v);
return v;
}
async showExactRev(rev: string) { async showExactRev(rev: string) {
const db = this.plugin.localDatabase; const db = this.plugin.localDatabase;
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true); const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
@@ -88,42 +130,67 @@ export class DocumentHistoryModal extends Modal {
} else { } else {
this.currentDoc = w; this.currentDoc = w;
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`; this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
let result = ""; let result = undefined;
const w1data = w.datatype == "plain" ? getDocData(w.data) : readString(new Uint8Array(decodeBinary(w.data))); const w1data = readDocument(w);
this.currentDeleted = !!w.deleted; this.currentDeleted = !!w.deleted;
this.currentText = w1data; // this.currentText = w1data;
if (this.showDiff) { if (this.showDiff) {
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1); const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) { if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
const oldRev = this.revs_info[prevRevIdx].rev; const oldRev = this.revs_info[prevRevIdx].rev;
const w2 = await db.getDBEntry(this.file, { rev: oldRev }, false, false, true); const w2 = await db.getDBEntry(this.file, { rev: oldRev }, false, false, true);
if (w2 != false) { if (w2 != false) {
const dmp = new diff_match_patch(); if (typeof w1data == "string") {
const w2data = w2.datatype == "plain" ? getDocData(w2.data) : readString(new Uint8Array(decodeBinary(w2.data))); result = "";
const diff = dmp.diff_main(w2data, w1data); const dmp = new diff_match_patch();
dmp.diff_cleanupSemantic(diff); const w2data = readDocument(w2) as string;
for (const v of diff) { const diff = dmp.diff_main(w2data, w1data);
const x1 = v[0]; dmp.diff_cleanupSemantic(diff);
const x2 = v[1]; for (const v of diff) {
if (x1 == DIFF_DELETE) { const x1 = v[0];
result += "<span class='history-deleted'>" + escapeStringToHTML(x2) + "</span>"; const x2 = v[1];
} else if (x1 == DIFF_EQUAL) { if (x1 == DIFF_DELETE) {
result += "<span class='history-normal'>" + escapeStringToHTML(x2) + "</span>"; result += "<span class='history-deleted'>" + escapeStringToHTML(x2) + "</span>";
} else if (x1 == DIFF_INSERT) { } else if (x1 == DIFF_EQUAL) {
result += "<span class='history-added'>" + escapeStringToHTML(x2) + "</span>"; result += "<span class='history-normal'>" + escapeStringToHTML(x2) + "</span>";
} else if (x1 == DIFF_INSERT) {
result += "<span class='history-added'>" + escapeStringToHTML(x2) + "</span>";
}
} }
result = result.replace(/\n/g, "<br>");
} else if (isImage(this.file)) {
const src = this.generateBlobURL("base", w1data);
const overlay = this.generateBlobURL("overlay", readDocument(w2) as Uint8Array);
result =
`<div class='ls-imgdiff-wrap'>
<div class='overlay'>
<img class='img-base' src="${src}">
<img class='img-overlay' src='${overlay}'>
</div>
</div>`;
this.contentView.removeClass("op-pre");
} }
}
}
result = result.replace(/\n/g, "<br>"); }
} else { if (result == undefined) {
result = escapeStringToHTML(w1data); if (typeof w1data != "string") {
if (isImage(this.file)) {
const src = this.generateBlobURL("base", w1data);
result =
`<div class='ls-imgdiff-wrap'>
<div class='overlay'>
<img class='img-base' src="${src}">
</div>
</div>`;
this.contentView.removeClass("op-pre");
} }
} else { } else {
result = escapeStringToHTML(w1data); result = escapeStringToHTML(w1data);
} }
} else {
result = escapeStringToHTML(w1data);
} }
if (result == undefined) result = typeof w1data == "string" ? escapeStringToHTML(w1data) : "Binary file";
this.contentView.innerHTML = (this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result; this.contentView.innerHTML = (this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result;
} }
} }
@@ -200,11 +267,11 @@ export class DocumentHistoryModal extends Modal {
Logger("Path is not valid to write content.", LOG_LEVEL_INFO); Logger("Path is not valid to write content.", LOG_LEVEL_INFO);
} }
if (this.currentDoc?.datatype == "plain") { if (this.currentDoc?.datatype == "plain") {
await this.app.vault.adapter.write(pathToWrite, getDocData(this.currentDoc.data)); await this.plugin.vaultAccess.adapterWrite(pathToWrite, getDocData(this.currentDoc.data));
await focusFile(pathToWrite); await focusFile(pathToWrite);
this.close(); this.close();
} else if (this.currentDoc?.datatype == "newnote") { } else if (this.currentDoc?.datatype == "newnote") {
await this.app.vault.adapter.writeBinary(pathToWrite, decodeBinary(this.currentDoc.data)); await this.plugin.vaultAccess.adapterWrite(pathToWrite, decodeBinary(this.currentDoc.data));
await focusFile(pathToWrite); await focusFile(pathToWrite);
this.close(); this.close();
} else { } else {
@@ -217,5 +284,9 @@ export class DocumentHistoryModal extends Modal {
onClose() { onClose() {
const { contentEl } = this; const { contentEl } = this;
contentEl.empty(); contentEl.empty();
this.BlobURLs.forEach(value => {
console.log(value);
if (value) URL.revokeObjectURL(value);
})
} }
} }

View File

@@ -2,12 +2,11 @@
import ObsidianLiveSyncPlugin from "./main"; import ObsidianLiveSyncPlugin from "./main";
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import type { AnyEntry, FilePathWithPrefix } from "./lib/src/types"; import type { AnyEntry, FilePathWithPrefix } from "./lib/src/types";
import { getDocData, isDocContentSame } from "./lib/src/utils"; import { createBinaryBlob, getDocData, isDocContentSame } from "./lib/src/utils";
import { diff_match_patch } from "./deps"; import { diff_match_patch } from "./deps";
import { DocumentHistoryModal } from "./DocumentHistoryModal"; import { DocumentHistoryModal } from "./DocumentHistoryModal";
import { isPlainText, stripAllPrefixes } from "./lib/src/path"; import { isPlainText, stripAllPrefixes } from "./lib/src/path";
import { TFile } from "./deps"; import { TFile } from "./deps";
import { encodeBinary } from "./lib/src/strbin";
export let plugin: ObsidianLiveSyncPlugin; export let plugin: ObsidianLiveSyncPlugin;
let showDiffInfo = false; let showDiffInfo = false;
@@ -67,10 +66,7 @@
for (const revInfo of reversedRevs) { for (const revInfo of reversedRevs) {
if (revInfo.status == "available") { if (revInfo.status == "available") {
const doc = const doc = (!isPlain && showDiffInfo) || (checkStorageDiff && revInfo.rev == docA._rev) ? await db.getDBEntry(path, { rev: revInfo.rev }, false, false, true) : await db.getDBEntryMeta(path, { rev: revInfo.rev }, true);
(!isPlain && showDiffInfo) || (checkStorageDiff && revInfo.rev == docA._rev)
? await db.getDBEntry(path, { rev: revInfo.rev }, false, false, true)
: await db.getDBEntryMeta(path, { rev: revInfo.rev }, true);
if (doc === false) continue; if (doc === false) continue;
const rev = revInfo.rev; const rev = revInfo.rev;
@@ -108,16 +104,16 @@
} }
if (rev == docA._rev) { if (rev == docA._rev) {
if (checkStorageDiff) { if (checkStorageDiff) {
const abs = plugin.app.vault.getAbstractFileByPath(stripAllPrefixes(plugin.getPath(docA))); const abs = plugin.vaultAccess.getAbstractFileByPath(stripAllPrefixes(plugin.getPath(docA)));
if (abs instanceof TFile) { if (abs instanceof TFile) {
let result = false; let result = false;
if (isPlainText(docA.path)) { if (isPlainText(docA.path)) {
const data = await plugin.app.vault.read(abs); const data = await plugin.vaultAccess.adapterRead(abs);
result = isDocContentSame(data, doc.data); result = await isDocContentSame(data, doc.data);
} else { } else {
const data = await plugin.app.vault.readBinary(abs); const data = await plugin.vaultAccess.adapterReadBinary(abs);
const dataEEncoded = await encodeBinary(data, plugin.settings.useV1); const dataEEncoded = createBinaryBlob(data);
result = isDocContentSame(dataEEncoded, doc.data); result = await isDocContentSame(dataEEncoded, doc.data);
} }
if (result) { if (result) {
diffDetail += " ⚖️"; diffDetail += " ⚖️";

View File

@@ -1,6 +1,7 @@
import { App, Modal } from "./deps"; import { App, Modal } from "./deps";
import { type FilePath, type LoadedEntry } from "./lib/src/types"; import { type FilePath, type LoadedEntry } from "./lib/src/types";
import JsonResolvePane from "./JsonResolvePane.svelte"; import JsonResolvePane from "./JsonResolvePane.svelte";
import { waitForSignal } from "./lib/src/utils";
export class JsonResolveModal extends Modal { export class JsonResolveModal extends Modal {
// result: Array<[number, string]>; // result: Array<[number, string]>;
@@ -20,6 +21,7 @@ export class JsonResolveModal extends Modal {
this.nameA = nameA; this.nameA = nameA;
this.nameB = nameB; this.nameB = nameB;
this.defaultSelect = defaultSelect; this.defaultSelect = defaultSelect;
waitForSignal(`cancel-internal-conflict:${filename}`).then(() => this.close());
} }
async UICallback(keepRev: string, mergedStr?: string) { async UICallback(keepRev: string, mergedStr?: string) {
this.close(); this.close();

View File

@@ -14,6 +14,9 @@ export abstract class LiveSyncCommands {
get localDatabase() { get localDatabase() {
return this.plugin.localDatabase; return this.plugin.localDatabase;
} }
get vaultAccess() {
return this.plugin.vaultAccess;
}
id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix { id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
return this.plugin.id2path(id, entry, stripPrefix); return this.plugin.id2path(id, entry, stripPrefix);
} }

View File

@@ -1,5 +1,6 @@
import { App, Modal } from "./deps"; import { App, Modal } from "./deps";
import { logMessageStore } from "./lib/src/stores"; import type { ReactiveInstance, } from "./lib/src/reactive";
import { logMessages } from "./lib/src/stores";
import { escapeStringToHTML } from "./lib/src/strbin"; import { escapeStringToHTML } from "./lib/src/strbin";
import ObsidianLiveSyncPlugin from "./main"; import ObsidianLiveSyncPlugin from "./main";
@@ -21,14 +22,16 @@ export class LogDisplayModal extends Modal {
div.addClass("op-scrollable"); div.addClass("op-scrollable");
div.addClass("op-pre"); div.addClass("op-pre");
this.logEl = div; this.logEl = div;
this.unsubscribe = logMessageStore.observe((e) => { function updateLog(logs: ReactiveInstance<string[]>) {
const e = logs.value;
let msg = ""; let msg = "";
for (const v of e) { for (const v of e) {
msg += escapeStringToHTML(v) + "<br>"; msg += escapeStringToHTML(v) + "<br>";
} }
this.logEl.innerHTML = msg; this.logEl.innerHTML = msg;
}) }
logMessageStore.invalidate(); logMessages.onChanged(updateLog);
this.unsubscribe = () => logMessages.offChanged(updateLog);
} }
onClose() { onClose() {
const { contentEl } = this; const { contentEl } = this;

View File

@@ -1,26 +1,27 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import { logMessageStore } from "./lib/src/stores"; import { logMessages } from "./lib/src/stores";
import type { ReactiveInstance } from "./lib/src/reactive";
import { Logger } from "./lib/src/logger";
let unsubscribe: () => void; let unsubscribe: () => void;
let messages = [] as string[]; let messages = [] as string[];
let wrapRight = false; let wrapRight = false;
let autoScroll = true; let autoScroll = true;
let suspended = false; let suspended = false;
function updateLog(logs: ReactiveInstance<string[]>) {
const e = logs.value;
if (!suspended) {
messages = [...e];
setTimeout(() => {
if (scroll) scroll.scrollTop = scroll.scrollHeight;
}, 10);
}
}
onMount(async () => { onMount(async () => {
unsubscribe = logMessageStore.observe((e) => { logMessages.onChanged(updateLog);
if (!suspended) { Logger("Log window opened");
messages = [...e]; unsubscribe = () => logMessages.offChanged(updateLog);
if (autoScroll) {
if (scroll) scroll.scrollTop = scroll.scrollHeight;
}
}
});
logMessageStore.invalidate();
setTimeout(() => {
if (scroll) scroll.scrollTop = scroll.scrollHeight;
}, 100);
}); });
onDestroy(() => { onDestroy(() => {
if (unsubscribe) unsubscribe(); if (unsubscribe) unsubscribe();

View File

@@ -1,13 +1,13 @@
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "./deps"; import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "./deps";
import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings, type ConfigPassphraseStore, type RemoteDBSettings, type FilePathWithPrefix, type HashAlgorithm, type DocumentID, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, LOG_LEVEL_INFO } from "./lib/src/types"; import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings, type ConfigPassphraseStore, type RemoteDBSettings, type FilePathWithPrefix, type HashAlgorithm, type DocumentID, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, LOG_LEVEL_INFO } from "./lib/src/types";
import { delay } from "./lib/src/utils"; import { createBinaryBlob, createTextBlob, delay, isDocContentSame } from "./lib/src/utils";
import { Semaphore } from "./lib/src/semaphore";
import { versionNumberString2Number } from "./lib/src/strbin"; import { versionNumberString2Number } from "./lib/src/strbin";
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
import { checkSyncInfo, isCloudantURI } from "./lib/src/utils_couchdb.js"; import { checkSyncInfo, isCloudantURI } from "./lib/src/utils_couchdb";
import { testCrypt } from "./lib/src/e2ee_v2"; import { testCrypt } from "./lib/src/e2ee_v2";
import ObsidianLiveSyncPlugin from "./main"; import ObsidianLiveSyncPlugin from "./main";
import { askYesNo, performRebuildDB, requestToCouchDB, scheduleTask } from "./utils"; import { askYesNo, performRebuildDB, requestToCouchDB, scheduleTask } from "./utils";
import { request, type ButtonComponent } from "obsidian";
export class ObsidianLiveSyncSettingTab extends PluginSettingTab { export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
@@ -26,6 +26,18 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
} }
this.plugin.addLog(`Connected to ${db.info.db_name}`, LOG_LEVEL_NOTICE); this.plugin.addLog(`Connected to ${db.info.db_name}`, LOG_LEVEL_NOTICE);
} }
askReload() {
scheduleTask("configReload", 250, async () => {
if (await askYesNo(this.app, "Do you want to restart and reload Obsidian now?") == "yes") {
// @ts-ignore
this.app.commands.executeCommandById("app:reload")
}
})
}
closeSetting() {
// @ts-ignore
this.plugin.app.setting.close()
}
display(): void { display(): void {
const { containerEl } = this; const { containerEl } = this;
let encrypt = this.plugin.settings.encrypt; let encrypt = this.plugin.settings.encrypt;
@@ -34,6 +46,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
containerEl.empty(); containerEl.empty();
// const preferred_setting = isCloudantURI(this.plugin.settings.couchDB_URI) ? PREFERRED_SETTING_CLOUDANT : PREFERRED_SETTING_SELF_HOSTED;
// const default_setting = { ...DEFAULT_SETTINGS };
containerEl.createEl("h2", { text: "Settings for Self-hosted LiveSync." }); containerEl.createEl("h2", { text: "Settings for Self-hosted LiveSync." });
containerEl.addClass("sls-setting"); containerEl.addClass("sls-setting");
containerEl.removeClass("isWizard"); containerEl.removeClass("isWizard");
@@ -132,29 +148,27 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
const setupWizardEl = containerEl.createDiv(); const setupWizardEl = containerEl.createDiv();
setupWizardEl.createEl("h3", { text: "Setup wizard" }); setupWizardEl.createEl("h3", { text: "Setup wizard" });
new Setting(setupWizardEl) new Setting(setupWizardEl)
.setName("Discard the existing configuration and set up") .setName("Use the copied setup URI")
.setDesc("To setup Self-hosted LiveSync, this method is the most preferred one.")
.addButton((text) => { .addButton((text) => {
// eslint-disable-next-line require-await text.setButtonText("Use").onClick(async () => {
text.setButtonText("Next").onClick(async () => { this.closeSetting();
if (JSON.stringify(this.plugin.settings) != JSON.stringify(DEFAULT_SETTINGS)) { await this.plugin.addOnSetup.command_openSetupURI();
this.plugin.replicator.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")
}
}) })
}) })
if (this.plugin.settings.isConfigured) {
new Setting(setupWizardEl)
.setName("Copy current settings as a new setup URI")
.addButton((text) => {
text.setButtonText("Copy").onClick(async () => {
await this.plugin.addOnSetup.command_copySetupURI();
})
})
}
new Setting(setupWizardEl) new Setting(setupWizardEl)
.setName("Do not discard the existing configuration and set up again") .setName("Minimal setup")
.addButton((text) => { .addButton((text) => {
text.setButtonText("Next").onClick(async () => { text.setButtonText("Start").onClick(async () => {
this.plugin.settings.liveSync = false; this.plugin.settings.liveSync = false;
this.plugin.settings.periodicReplication = false; this.plugin.settings.periodicReplication = false;
this.plugin.settings.syncOnSave = false; this.plugin.settings.syncOnSave = false;
@@ -168,28 +182,85 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
applyDisplayEnabled(); applyDisplayEnabled();
inWizard = true; inWizard = true;
changeDisplay("0") changeDisplay("0")
}) })
}) })
const infoWarnForSubsequent = setupWizardEl.createEl("div", { text: `To set up second or subsequent device, please use 'Copy setup URI' and 'Open setup URI'` }); if (!this.plugin.settings.isConfigured) {
infoWarnForSubsequent.addClass("op-warn-info"); new Setting(setupWizardEl)
new Setting(setupWizardEl) .setName("Enable LiveSync on this device as the set-up was completed manually")
.setName("Copy setup URI") .addButton((text) => {
.addButton((text) => { text.setButtonText("Enable").onClick(async () => {
text.setButtonText("Copy setup URI").onClick(() => { this.plugin.settings.isConfigured = true;
// @ts-ignore await this.plugin.saveSettings();
this.plugin.app.commands.executeCommandById("obsidian-livesync:livesync-copysetupuri") this.askReload();
})
})
}
if (this.plugin.settings.isConfigured) {
new Setting(setupWizardEl)
.setName("Discard exist settings and databases")
.addButton((text) => {
text.setButtonText("Discard").onClick(async () => {
if (await askYesNo(this.plugin.app, "Do you really want to discard exist settings and databases?") == "yes") {
this.plugin.settings = { ...DEFAULT_SETTINGS };
await this.plugin.saveSettingData();
await this.plugin.resetLocalDatabase();
// await this.plugin.initializeDatabase();
this.askReload();
}
}).setWarning()
})
}
setupWizardEl.createEl("h3", { text: "Online Tips" });
const repo = "vrtmrz/obsidian-livesync";
const topPath = "/docs/troubleshooting.md";
const rawRepoURI = `https://raw.githubusercontent.com/${repo}/main`;
setupWizardEl.createEl("div", "", el => el.innerHTML = `<a href='https://github.com/${repo}/blob/main${topPath}' target="_blank">Open in browser</a>`);
const troubleShootEl = setupWizardEl.createEl("div", { text: "", cls: "sls-troubleshoot-preview" });
const loadMarkdownPage = async (pathAll: string, basePathParam: string = "") => {
troubleShootEl.style.minHeight = troubleShootEl.clientHeight + "px";
troubleShootEl.empty();
const fullPath = pathAll.startsWith("/") ? pathAll : `${basePathParam}/${pathAll}`;
const directoryArr = fullPath.split("/");
const filename = directoryArr.pop();
const directly = directoryArr.join("/");
const basePath = directly;
let remoteTroubleShootMDSrc = "";
try {
remoteTroubleShootMDSrc = await request(`${rawRepoURI}${basePath}/${filename}`);
} catch (ex) {
remoteTroubleShootMDSrc = "Error Occurred!!\n" + ex.toString();
}
const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace(/\((.*?(.png)|(.jpg))\)/g, `(${rawRepoURI}${basePath}/$1)`)
// Render markdown
await MarkdownRenderer.render(this.plugin.app, `<a class='sls-troubleshoot-anchor'></a> [Tips and Troubleshooting](${topPath}) [PageTop](${filename})\n\n${remoteTroubleShootMD}`, troubleShootEl, `${rawRepoURI}`, this.plugin);
// Menu
troubleShootEl.querySelector<HTMLAnchorElement>(".sls-troubleshoot-anchor")
.parentElement.setCssStyles({ position: "sticky", top: "-1em", backgroundColor: "var(--modal-background)" });
// Trap internal links.
troubleShootEl.querySelectorAll<HTMLAnchorElement>("a.internal-link").forEach((anchorEl) => {
anchorEl.addEventListener("click", async (evt) => {
const uri = anchorEl.getAttr("data-href");
if (uri.startsWith("#")) {
evt.preventDefault();
const elements = Array.from(troubleShootEl.querySelectorAll<HTMLHeadingElement>("[data-heading]"))
const p = elements.find(e => e.getAttr("data-heading").toLowerCase().split(" ").join("-") == uri.substring(1).toLowerCase());
if (p) {
p.setCssStyles({ scrollMargin: "3em" });
p.scrollIntoView({ behavior: "instant", block: "start" });
}
} else {
evt.preventDefault();
await loadMarkdownPage(uri, basePath);
troubleShootEl.setCssStyles({ scrollMargin: "1em" });
troubleShootEl.scrollIntoView({ behavior: "instant", block: "start" });
}
}) })
}) })
.addButton((text) => { troubleShootEl.style.minHeight = "";
text.setButtonText("Open setup URI").onClick(() => { }
// @ts-ignore loadMarkdownPage(topPath);
this.plugin.app.commands.executeCommandById("obsidian-livesync:livesync-opensetupuri")
})
})
addScreenElement("110", setupWizardEl); addScreenElement("110", setupWizardEl);
const containerRemoteDatabaseEl = containerEl.createDiv(); const containerRemoteDatabaseEl = containerEl.createDiv();
@@ -284,6 +355,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
new Setting(containerRemoteDatabaseEl) new Setting(containerRemoteDatabaseEl)
.setName("Test Database Connection") .setName("Test Database Connection")
.setClass("wizardHidden")
.setDesc("Open database connection. If the remote database is not found and you have the privilege to create a database, the database will be created.") .setDesc("Open database connection. If the remote database is not found and you have the privilege to create a database, the database will be created.")
.addButton((button) => .addButton((button) =>
button button
@@ -295,8 +367,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
); );
new Setting(containerRemoteDatabaseEl) new Setting(containerRemoteDatabaseEl)
.setName("Check database configuration") .setName("Check and Fix database configuration")
// .setDesc("Open database connection. If the remote database is not found and you have the privilege to create a database, the database will be created.") .setDesc("Check the database configuration, and fix if there are any problems.")
.addButton((button) => .addButton((button) =>
button button
.setButtonText("Check") .setButtonText("Check")
@@ -503,18 +575,18 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
passphraseSetting?.controlEl.toggleClass("sls-item-dirty", passphrase != this.plugin.settings.passphrase); passphraseSetting?.controlEl.toggleClass("sls-item-dirty", passphrase != this.plugin.settings.passphrase);
dynamicIteration?.controlEl.toggleClass("sls-item-dirty", useDynamicIterationCount != this.plugin.settings.useDynamicIterationCount); dynamicIteration?.controlEl.toggleClass("sls-item-dirty", useDynamicIterationCount != this.plugin.settings.useDynamicIterationCount);
usePathObfuscationEl?.controlEl.toggleClass("sls-item-dirty", usePathObfuscation != this.plugin.settings.usePathObfuscation); usePathObfuscationEl?.controlEl.toggleClass("sls-item-dirty", usePathObfuscation != this.plugin.settings.usePathObfuscation);
if (encrypt != this.plugin.settings.encrypt ||
passphrase != this.plugin.settings.passphrase ||
useDynamicIterationCount != this.plugin.settings.useDynamicIterationCount ||
usePathObfuscation != this.plugin.settings.usePathObfuscation) {
applyE2EButtons.settingEl.removeClass("sls-setting-hidden");
} else {
applyE2EButtons.settingEl.addClass("sls-setting-hidden");
}
} else { } else {
passphraseSetting.settingEl.addClass("sls-setting-hidden"); passphraseSetting.settingEl.addClass("sls-setting-hidden");
dynamicIteration.settingEl.addClass("sls-setting-hidden"); dynamicIteration.settingEl.addClass("sls-setting-hidden");
usePathObfuscationEl.settingEl.addClass("sls-setting-hidden"); usePathObfuscationEl.settingEl.addClass("sls-setting-hidden");
}
if (encrypt != this.plugin.settings.encrypt ||
passphrase != this.plugin.settings.passphrase ||
useDynamicIterationCount != this.plugin.settings.useDynamicIterationCount ||
usePathObfuscation != this.plugin.settings.usePathObfuscation) {
applyE2EButtons.settingEl.removeClass("sls-setting-hidden");
} else {
applyE2EButtons.settingEl.addClass("sls-setting-hidden"); applyE2EButtons.settingEl.addClass("sls-setting-hidden");
} }
} }
@@ -649,6 +721,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.plugin.settings.passphrase = passphrase; this.plugin.settings.passphrase = passphrase;
this.plugin.settings.useDynamicIterationCount = useDynamicIterationCount; this.plugin.settings.useDynamicIterationCount = useDynamicIterationCount;
this.plugin.settings.usePathObfuscation = usePathObfuscation; this.plugin.settings.usePathObfuscation = usePathObfuscation;
this.plugin.settings.isConfigured = true;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
updateE2EControls(); updateE2EControls();
if (sendToServer) { if (sendToServer) {
@@ -677,26 +750,23 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.plugin.settings.passphrase = passphrase; this.plugin.settings.passphrase = passphrase;
this.plugin.settings.useDynamicIterationCount = useDynamicIterationCount; this.plugin.settings.useDynamicIterationCount = useDynamicIterationCount;
this.plugin.settings.usePathObfuscation = usePathObfuscation; this.plugin.settings.usePathObfuscation = usePathObfuscation;
this.plugin.settings.isConfigured = true;
Logger("All synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE) Logger("All synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE)
await this.plugin.saveSettings(); await this.plugin.saveSettings();
updateE2EControls(); updateE2EControls();
applyDisplayEnabled(); applyDisplayEnabled();
// @ts-ignore this.closeSetting();
this.plugin.app.setting.close();
await delay(2000); await delay(2000);
await performRebuildDB(this.plugin, method); await performRebuildDB(this.plugin, method);
} }
let rebuildRemote = false;
new Setting(containerRemoteDatabaseEl) new Setting(containerRemoteDatabaseEl)
.setName("")
.setClass("wizardOnly") .setClass("wizardOnly")
.addButton((button) => .addButton((button) =>
button button
.setButtonText("Next") .setButtonText("Next")
.setClass("mod-cta") .setCta()
.setDisabled(false) .setDisabled(false)
.onClick(() => { .onClick(() => {
if (!this.plugin.settings.encrypt) { if (!this.plugin.settings.encrypt) {
@@ -705,33 +775,13 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
if (isCloudantURI(this.plugin.settings.couchDB_URI)) { if (isCloudantURI(this.plugin.settings.couchDB_URI)) {
this.plugin.settings.customChunkSize = 0; this.plugin.settings.customChunkSize = 0;
} else { } else {
this.plugin.settings.customChunkSize = 100; this.plugin.settings.customChunkSize = 50;
} }
rebuildRemote = false;
changeDisplay("30") changeDisplay("30")
}) })
); )
new Setting(containerRemoteDatabaseEl) ;
.setName("")
.setClass("wizardOnly")
.addButton((button) =>
button
.setButtonText("Discard existing database and proceed")
.setDisabled(false)
.setWarning()
.onClick(() => {
if (!this.plugin.settings.encrypt) {
this.plugin.settings.passphrase = "";
}
if (isCloudantURI(this.plugin.settings.couchDB_URI)) {
this.plugin.settings.customChunkSize = 0;
} else {
this.plugin.settings.customChunkSize = 100;
}
rebuildRemote = true;
changeDisplay("30")
})
);
addScreenElement("0", containerRemoteDatabaseEl); addScreenElement("0", containerRemoteDatabaseEl);
const containerGeneralSettingsEl = containerEl.createDiv(); const containerGeneralSettingsEl = containerEl.createDiv();
@@ -746,8 +796,20 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
toggle.setValue(this.plugin.settings.showStatusOnEditor).onChange(async (value) => { toggle.setValue(this.plugin.settings.showStatusOnEditor).onChange(async (value) => {
this.plugin.settings.showStatusOnEditor = value; this.plugin.settings.showStatusOnEditor = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
this.display();
}) })
); );
if (this.plugin.settings.showStatusOnEditor) {
new Setting(containerGeneralSettingsEl)
.setName("Show status as icons only")
.setDesc("")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showOnlyIconsOnEditor).onChange(async (value) => {
this.plugin.settings.showOnlyIconsOnEditor = value;
await this.plugin.saveSettings();
})
);
}
containerGeneralSettingsEl.createEl("h4", { text: "Logging" }); containerGeneralSettingsEl.createEl("h4", { text: "Logging" });
new Setting(containerGeneralSettingsEl) new Setting(containerGeneralSettingsEl)
@@ -807,6 +869,53 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
text.inputEl.setAttribute("type", "number"); text.inputEl.setAttribute("type", "number");
}); });
containerGeneralSettingsEl.createEl("h4", { text: "Share settings via markdown" });
let settingSyncFile = this.plugin.settings.settingSyncFile;
let buttonApplyFilename: ButtonComponent;
new Setting(containerGeneralSettingsEl)
.setName("Filename")
.setDesc("If you set this, all settings are saved in a markdown file. You will also be notified when new settings were arrived. You can set different files by the platform.")
.addText((text) => {
text.setPlaceholder("livesync/setting.md")
.setValue(settingSyncFile)
.onChange((value) => {
settingSyncFile = value;
if (settingSyncFile == this.plugin.settings.settingSyncFile) {
buttonApplyFilename.removeCta()
buttonApplyFilename.setDisabled(true);
} else {
buttonApplyFilename.setCta()
buttonApplyFilename.setDisabled(false);
}
})
}).addButton(button => {
button.setButtonText("Apply")
.onClick(async () => {
this.plugin.settings.settingSyncFile = settingSyncFile;
await this.plugin.saveSettings();
this.display();
})
buttonApplyFilename = button;
})
new Setting(containerGeneralSettingsEl)
.setName("Write credentials in the file")
.setDesc("(Not recommended) If set, credentials will be stored in the file.")
.addToggle(toggle => {
toggle.setValue(this.plugin.settings.writeCredentialsForSettingSync)
.onChange(async (value) => {
this.plugin.settings.writeCredentialsForSettingSync = value;
await this.plugin.saveSettings();
})
});
new Setting(containerGeneralSettingsEl)
.setName("Notify all setting files")
.addToggle(toggle => {
toggle.setValue(this.plugin.settings.notifyAllSettingSyncFile)
.onChange(async (value) => {
this.plugin.settings.notifyAllSettingSyncFile = value;
await this.plugin.saveSettings();
})
});
containerGeneralSettingsEl.createEl("h4", { text: "Advanced Confidentiality" }); containerGeneralSettingsEl.createEl("h4", { text: "Advanced Confidentiality" });
@@ -936,25 +1045,20 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
...presetAllDisabled ...presetAllDisabled
} }
} }
this.plugin.saveSettings(); await this.plugin.saveSettings();
this.display(); this.display();
await this.plugin.realizeSettingSyncMode(); await this.plugin.realizeSettingSyncMode();
if (inWizard) { if (inWizard) {
// @ts-ignore this.closeSetting();
this.plugin.app.setting.close() if (!this.plugin.settings.isConfigured) {
await this.plugin.resetLocalDatabase(); this.plugin.settings.isConfigured = true;
await this.plugin.initializeDatabase(true); await this.plugin.saveSettings();
if (rebuildRemote) { await rebuildDB("localOnly");
await this.plugin.markRemoteLocked(); Logger("All done! Please set up subsequent devices with 'Copy current settings as a new setup URI' and 'Use the copied setup URI'.", LOG_LEVEL_NOTICE);
await this.plugin.tryResetRemoteDatabase(); await this.plugin.addOnSetup.command_openSetupURI();
await this.plugin.markRemoteLocked(); } else {
await this.plugin.markRemoteResolved(); this.askReload();
} }
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")
} }
}) })
); );
@@ -1104,7 +1208,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
); );
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Postpone resolution of unopened files") .setName("Postpone resolution of inactive files")
.setClass("wizardHidden") .setClass("wizardHidden")
.addToggle((toggle) => .addToggle((toggle) =>
toggle.setValue(this.plugin.settings.checkConflictOnlyOnOpen).onChange(async (value) => { toggle.setValue(this.plugin.settings.checkConflictOnlyOnOpen).onChange(async (value) => {
@@ -1112,6 +1216,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new Setting(containerSyncSettingEl)
.setName("Postpone manual resolution of inactive files")
.setClass("wizardHidden")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showMergeDialogOnlyOnActive).onChange(async (value) => {
this.plugin.settings.showMergeDialogOnlyOnActive = value;
await this.plugin.saveSettings();
})
);
containerSyncSettingEl.createEl("h4", { text: "Compatibility" }).addClass("wizardHidden"); containerSyncSettingEl.createEl("h4", { text: "Compatibility" }).addClass("wizardHidden");
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Always resolve conflict manually") .setName("Always resolve conflict manually")
@@ -1164,24 +1277,21 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.addButton((button) => { .addButton((button) => {
button.setButtonText("Merge") button.setButtonText("Merge")
.onClick(async () => { .onClick(async () => {
// @ts-ignore this.closeSetting()
this.plugin.app.setting.close()
await this.plugin.addOnSetup.configureHiddenFileSync("MERGE"); await this.plugin.addOnSetup.configureHiddenFileSync("MERGE");
}) })
}) })
.addButton((button) => { .addButton((button) => {
button.setButtonText("Fetch") button.setButtonText("Fetch")
.onClick(async () => { .onClick(async () => {
// @ts-ignore this.closeSetting()
this.plugin.app.setting.close()
await this.plugin.addOnSetup.configureHiddenFileSync("FETCH"); await this.plugin.addOnSetup.configureHiddenFileSync("FETCH");
}) })
}) })
.addButton((button) => { .addButton((button) => {
button.setButtonText("Overwrite") button.setButtonText("Overwrite")
.onClick(async () => { .onClick(async () => {
// @ts-ignore this.closeSetting()
this.plugin.app.setting.close()
await this.plugin.addOnSetup.configureHiddenFileSync("OVERWRITE"); await this.plugin.addOnSetup.configureHiddenFileSync("OVERWRITE");
}) })
}); });
@@ -1217,7 +1327,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}); });
let skipPatternTextArea: TextAreaComponent = null; let skipPatternTextArea: TextAreaComponent = null;
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/"; const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/";
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$"; const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$,\\/workspace-mobile.json$";
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Folders and files to ignore") .setName("Folders and files to ignore")
.setDesc( .setDesc(
@@ -1301,11 +1411,35 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}); });
containerSyncSettingEl.createEl("h4", { containerSyncSettingEl.createEl("h4", {
text: sanitizeHTMLToDom(`Synchronization target filters`), text: sanitizeHTMLToDom(`Targets`),
}).addClass("wizardHidden"); }).addClass("wizardHidden");
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Regular expression to ignore files") .setName("Synchronising files")
.setDesc("If this is set, any changes to local and remote files that match this will be skipped.") .setDesc("(RegExp) Empty to sync all files. set filter as a regular expression to limit synchronising files.")
.setClass("wizardHidden")
.addTextArea((text) => {
text
.setValue(this.plugin.settings.syncOnlyRegEx)
.setPlaceholder("\\.md$|\\.txt")
.onChange(async (value) => {
let isValidRegExp = false;
try {
new RegExp(value);
isValidRegExp = true;
} catch (_) {
// NO OP.
}
if (isValidRegExp || value.trim() == "") {
this.plugin.settings.syncOnlyRegEx = value;
await this.plugin.saveSettings();
}
})
return text;
}
);
new Setting(containerSyncSettingEl)
.setName("Non-Synchronising files")
.setDesc("(RegExp) If this is set, any changes to local and remote files that match this will be skipped.")
.setClass("wizardHidden") .setClass("wizardHidden")
.addTextArea((text) => { .addTextArea((text) => {
text text
@@ -1328,29 +1462,22 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
} }
); );
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Regular expression for restricting synchronization targets") .setName("Maximum file size")
.setDesc("If this is set, changes to local and remote files that only match this will be processed.") .setDesc("(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.")
.setClass("wizardHidden") .setClass("wizardHidden")
.addTextArea((text) => { .addText((text) => {
text text.setPlaceholder("")
.setValue(this.plugin.settings.syncOnlyRegEx) .setValue(this.plugin.settings.syncMaxSizeInMB + "")
.setPlaceholder("\\.md$|\\.txt")
.onChange(async (value) => { .onChange(async (value) => {
let isValidRegExp = false; let v = Number(value);
try { if (isNaN(v) || v < 1) {
new RegExp(value); v = 0;
isValidRegExp = true;
} catch (_) {
// NO OP.
} }
if (isValidRegExp || value.trim() == "") { this.plugin.settings.syncMaxSizeInMB = v;
this.plugin.settings.syncOnlyRegEx = value; await this.plugin.saveSettings();
await this.plugin.saveSettings(); });
} text.inputEl.setAttribute("type", "number");
}) });
return text;
}
);
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("(Beta) Use ignore files") .setName("(Beta) Use ignore files")
.setDesc("If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.") .setDesc("If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.")
@@ -1391,15 +1518,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}).addClass("wizardHidden"); }).addClass("wizardHidden");
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Batch size") .setName("Batch size")
.setDesc("Number of change feed items to process at a time. Defaults to 50.") .setDesc("Number of change feed items to process at a time. Defaults to 50. Minimum is 2.")
.setClass("wizardHidden") .setClass("wizardHidden")
.addText((text) => { .addText((text) => {
text.setPlaceholder("") text.setPlaceholder("")
.setValue(this.plugin.settings.batch_size + "") .setValue(this.plugin.settings.batch_size + "")
.onChange(async (value) => { .onChange(async (value) => {
let v = Number(value); let v = Number(value);
if (isNaN(v) || v < 10) { if (isNaN(v) || v < 2) {
v = 10; v = 2;
} }
this.plugin.settings.batch_size = v; this.plugin.settings.batch_size = v;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
@@ -1409,15 +1536,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Batch limit") .setName("Batch limit")
.setDesc("Number of batches to process at a time. Defaults to 40. This along with batch size controls how many docs are kept in memory at a time.") .setDesc("Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.")
.setClass("wizardHidden") .setClass("wizardHidden")
.addText((text) => { .addText((text) => {
text.setPlaceholder("") text.setPlaceholder("")
.setValue(this.plugin.settings.batches_limit + "") .setValue(this.plugin.settings.batches_limit + "")
.onChange(async (value) => { .onChange(async (value) => {
let v = Number(value); let v = Number(value);
if (isNaN(v) || v < 10) { if (isNaN(v) || v < 2) {
v = 10; v = 2;
} }
this.plugin.settings.batches_limit = v; this.plugin.settings.batches_limit = v;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
@@ -1514,8 +1641,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
pluginConfig.encryptedPassphrase = REDACTED; pluginConfig.encryptedPassphrase = REDACTED;
pluginConfig.encryptedCouchDBConnection = REDACTED; pluginConfig.encryptedCouchDBConnection = REDACTED;
pluginConfig.pluginSyncExtendedSetting = {}; pluginConfig.pluginSyncExtendedSetting = {};
const obsidianInfo = navigator.userAgent;
const msgConfig = `----remote config---- const msgConfig = `---- Obsidian info ----
${obsidianInfo}
---- remote config ----
${stringifyYaml(responseConfig)} ${stringifyYaml(responseConfig)}
---- Plug-in config --- ---- Plug-in config ---
version:${manifestVersion} version:${manifestVersion}
@@ -1553,43 +1682,78 @@ ${stringifyYaml(pluginConfig)}`;
c.addClass("op-warn"); c.addClass("op-warn");
} }
} }
new Setting(containerHatchEl)
.setName("Back to non-configured")
.addButton((button) =>
button
.setButtonText("Back")
.setDisabled(false)
.onClick(async () => {
this.plugin.settings.isConfigured = false;
await this.plugin.saveSettings();
this.askReload();
}));
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).` }); 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"); hatchWarn.addClass("op-warn-info");
new Setting(containerHatchEl) new Setting(containerHatchEl)
.setName("Verify and repair all files") .setName("Verify and repair all files")
.setDesc("Verify and repair all files and update database without restoring") .setDesc("Compare the content of files between on local database and storage. If not matched, you will asked which one want to keep.")
.addButton((button) => .addButton((button) =>
button button
.setButtonText("Verify and repair") .setButtonText("Verify all")
.setDisabled(false) .setDisabled(false)
.setWarning() .setWarning()
.onClick(async () => { .onClick(async () => {
const semaphore = Semaphore(10);
const files = this.app.vault.getFiles(); const files = this.app.vault.getFiles();
let i = 0; let i = 0;
const processes = files.map(e => (async (file) => { for (const file of files) {
const releaser = await semaphore.acquire(1, "verifyAndRepair"); i++;
Logger(`${i}/${files.length}\n${file.path}`, LOG_LEVEL_NOTICE, "verify");
if (!await this.plugin.isTargetFile(file)) continue;
const fileOnDB = await this.plugin.localDatabase.getDBEntry(file.path as FilePathWithPrefix);
if (!fileOnDB) {
Logger(`Compare: Not found on local database: ${file.path}`, LOG_LEVEL_NOTICE);
continue;
}
let content: Blob;
if (fileOnDB.type == "newnote") {
content = createBinaryBlob(await this.plugin.vaultAccess.vaultReadBinary(file));
} else {
content = createTextBlob(await this.plugin.vaultAccess.vaultRead(file));
}
if (isDocContentSame(content, fileOnDB.data)) {
Logger(`Compare: SAME: ${file.path}`)
} else {
Logger(`Compare: CONTENT IS NOT MATCHED! ${file.path}`, LOG_LEVEL_NOTICE);
resultArea.appendChild(resultArea.createEl("div", {}, el => {
el.appendChild(el.createEl("h6", { text: file.path }));
el.appendChild(el.createEl("div", {}, infoGroupEl => {
infoGroupEl.appendChild(infoGroupEl.createEl("div", { text: `Storage : Modified: ${new Date(file.stat.mtime).toLocaleString()}, Size:${file.stat.size}` }))
infoGroupEl.appendChild(infoGroupEl.createEl("div", { text: `Database: Modified: ${new Date(fileOnDB.mtime).toLocaleString()}, Size:${content.size}` }))
}));
try { el.appendChild(el.createEl("button", { text: "Storage -> Database" }, buttonEl => {
Logger(`UPDATE DATABASE ${file.path}`); buttonEl.onClickEvent(() => {
await this.plugin.updateIntoDB(file, false, null, true); this.plugin.updateIntoDB(file, false, undefined, true);
i++; el.remove();
Logger(`${i}/${files.length}\n${file.path}`, LOG_LEVEL_NOTICE, "verify"); })
}))
} catch (ex) { el.appendChild(el.createEl("button", { text: "Database -> Storage" }, buttonEl => {
i++; buttonEl.onClickEvent(() => {
Logger(`Error while verifyAndRepair`, LOG_LEVEL_NOTICE); this.plugin.pullFile(file.path as FilePathWithPrefix, [], true, undefined, false);
Logger(ex); el.remove();
} finally { })
releaser(); }))
return el;
}))
} }
} }
)(e));
await Promise.all(processes);
Logger("done", LOG_LEVEL_NOTICE, "verify"); Logger("done", LOG_LEVEL_NOTICE, "verify");
}) })
); );
const resultArea = containerHatchEl.createDiv({ text: "" });
new Setting(containerHatchEl) new Setting(containerHatchEl)
.setName("Check and convert non-path-obfuscated files") .setName("Check and convert non-path-obfuscated files")
.setDesc("") .setDesc("")
@@ -1632,7 +1796,7 @@ ${stringifyYaml(pluginConfig)}`;
if ((await this.plugin.localDatabase.putRaw(doc)).ok) { if ((await this.plugin.localDatabase.putRaw(doc)).ok) {
Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE); Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE);
} }
await this.plugin.showIfConflicted(docName as FilePathWithPrefix); await this.plugin.queueConflictCheck(docName as FilePathWithPrefix);
} else { } else {
Logger(`Converting ${docName} Failed!`, LOG_LEVEL_NOTICE); Logger(`Converting ${docName} Failed!`, LOG_LEVEL_NOTICE);
Logger(ret, LOG_LEVEL_VERBOSE); Logger(ret, LOG_LEVEL_VERBOSE);
@@ -1680,12 +1844,7 @@ ${stringifyYaml(pluginConfig)}`;
toggle.setValue(this.plugin.settings.suspendFileWatching).onChange(async (value) => { toggle.setValue(this.plugin.settings.suspendFileWatching).onChange(async (value) => {
this.plugin.settings.suspendFileWatching = value; this.plugin.settings.suspendFileWatching = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
scheduleTask("configReload", 250, async () => { this.askReload();
if (await askYesNo(this.app, "Do you want to restart and reload Obsidian now?") == "yes") {
// @ts-ignore
this.app.commands.executeCommandById("app:reload")
}
})
}) })
); );
new Setting(containerHatchEl) new Setting(containerHatchEl)
@@ -1695,12 +1854,7 @@ ${stringifyYaml(pluginConfig)}`;
toggle.setValue(this.plugin.settings.suspendParseReplicationResult).onChange(async (value) => { toggle.setValue(this.plugin.settings.suspendParseReplicationResult).onChange(async (value) => {
this.plugin.settings.suspendParseReplicationResult = value; this.plugin.settings.suspendParseReplicationResult = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
scheduleTask("configReload", 250, async () => { this.askReload();
if (await askYesNo(this.app, "Do you want to restart and reload Obsidian now?") == "yes") {
// @ts-ignore
this.app.commands.executeCommandById("app:reload")
}
})
}) })
); );
new Setting(containerHatchEl) new Setting(containerHatchEl)
@@ -1762,7 +1916,7 @@ ${stringifyYaml(pluginConfig)}`;
new Setting(containerHatchEl) new Setting(containerHatchEl)
.setName("Use an old adapter for compatibility") .setName("Use an old adapter for compatibility")
.setDesc("This option is not compatible with a database made by older versions. Changing this configuration will fetch the remote database again.") .setDesc("Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.")
.setClass("wizardHidden") .setClass("wizardHidden")
.addToggle((toggle) => .addToggle((toggle) =>
toggle.setValue(!this.plugin.settings.useIndexedDBAdapter).onChange(async (value) => { toggle.setValue(!this.plugin.settings.useIndexedDBAdapter).onChange(async (value) => {
@@ -1785,7 +1939,7 @@ ${stringifyYaml(pluginConfig)}`;
let newDatabaseName = this.plugin.settings.additionalSuffixOfDatabaseName + ""; let newDatabaseName = this.plugin.settings.additionalSuffixOfDatabaseName + "";
new Setting(containerHatchEl) new Setting(containerHatchEl)
.setName("Database suffix") .setName("Database suffix")
.setDesc("LiveSync could not treat multiple vaults which have same name, please add some suffix from here.") .setDesc("LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.")
.addText((text) => { .addText((text) => {
text.setPlaceholder("") text.setPlaceholder("")
.setValue(newDatabaseName) .setValue(newDatabaseName)
@@ -1813,7 +1967,7 @@ ${stringifyYaml(pluginConfig)}`;
.setClass("wizardHidden") .setClass("wizardHidden")
.addDropdown((dropdown) => .addDropdown((dropdown) =>
dropdown dropdown
.addOptions({ "": "Old Algorithm", "xxhash32": "xxhash32 (Fast)", "xxhash64": "xxhash64 (Fastest)" } as Record<HashAlgorithm, string>) .addOptions({ "": "Old Algorithm", "xxhash32": "xxhash32 (Fast)", "xxhash64": "xxhash64 (Fastest)", "sha1": "Fallback (Without WebAssembly)" } as Record<HashAlgorithm, string>)
.setValue(this.plugin.settings.hashAlg) .setValue(this.plugin.settings.hashAlg)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.hashAlg = value as HashAlgorithm; this.plugin.settings.hashAlg = value as HashAlgorithm;
@@ -1832,15 +1986,6 @@ ${stringifyYaml(pluginConfig)}`;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new Setting(containerHatchEl)
.setName("Use binary and encryption version 1")
.setDesc("Use the previous data format for other products which shares the remote database.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.useV1).onChange(async (value) => {
this.plugin.settings.useV1 = value;
await this.plugin.saveSettings();
})
);
addScreenElement("50", containerHatchEl); addScreenElement("50", containerHatchEl);
@@ -2008,8 +2153,7 @@ ${stringifyYaml(pluginConfig)}`;
.setDisabled(false) .setDisabled(false)
.setWarning() .setWarning()
.onClick(async () => { .onClick(async () => {
// @ts-ignore this.closeSetting()
this.plugin.app.setting.close()
await this.plugin.dbGC(); await this.plugin.dbGC();
}) })
); );
@@ -2031,7 +2175,7 @@ ${stringifyYaml(pluginConfig)}`;
applyDisplayEnabled(); applyDisplayEnabled();
if (this.selectedScreen == "") { if (this.selectedScreen == "") {
if (lastVersion != this.plugin.settings.lastReadUpdates) { if (lastVersion != this.plugin.settings.lastReadUpdates) {
if (JSON.stringify(this.plugin.settings) != JSON.stringify(DEFAULT_SETTINGS)) { if (this.plugin.settings.isConfigured) {
changeDisplay("100"); changeDisplay("100");
} else { } else {
changeDisplay("110") changeDisplay("110")

View File

@@ -207,21 +207,21 @@
const local = list.find((e) => e.term == thisTerm); const local = list.find((e) => e.term == thisTerm);
const selectedItem = list.find((e) => e.term == selected); const selectedItem = list.find((e) => e.term == selected);
if (selectedItem && (await applyData(selectedItem))) { if (selectedItem && (await applyData(selectedItem))) {
scheduleTask("update-plugin-list", 250, () => addOn.updatePluginList(true, local.documentPath)); addOn.updatePluginList(true, local?.documentPath);
} }
} }
async function compareSelected() { async function compareSelected() {
const local = list.find((e) => e.term == thisTerm); const local = list.find((e) => e.term == thisTerm);
const selectedItem = list.find((e) => e.term == selected); const selectedItem = list.find((e) => e.term == selected);
if (local && selectedItem && (await compareData(local, selectedItem))) { if (local && selectedItem && (await compareData(local, selectedItem))) {
scheduleTask("update-plugin-list", 250, () => addOn.updatePluginList(true, local.documentPath)); addOn.updatePluginList(true, local.documentPath);
} }
} }
async function deleteSelected() { async function deleteSelected() {
const selectedItem = list.find((e) => e.term == selected); const selectedItem = list.find((e) => e.term == selected);
// const deletedPath = selectedItem.documentPath; // const deletedPath = selectedItem.documentPath;
if (selectedItem && (await deleteData(selectedItem))) { if (selectedItem && (await deleteData(selectedItem))) {
scheduleTask("update-plugin-list", 250, () => addOn.reloadPluginList(true)); addOn.reloadPluginList(true);
} }
} }
async function duplicateItem() { async function duplicateItem() {

View File

@@ -27,8 +27,10 @@
async function requestReload() { async function requestReload() {
await addOn.reloadPluginList(true); await addOn.reloadPluginList(true);
} }
let allTerms = [] as string[];
pluginList.subscribe((e) => { pluginList.subscribe((e) => {
list = e; list = e;
allTerms = unique(list.map((e) => e.term));
}); });
pluginIsEnumerating.subscribe((e) => { pluginIsEnumerating.subscribe((e) => {
loading = e; loading = e;
@@ -172,7 +174,7 @@
.filter((e) => `${e.category}/${e.name}` == key) .filter((e) => `${e.category}/${e.name}` == key)
.map((e) => e.files) .map((e) => e.files)
.flat() .flat()
.map((e) => e.filename) .map((e) => e.filename),
); );
automaticList.set(key, mode); automaticList.set(key, mode);
automaticListDisp = automaticList; automaticListDisp = automaticList;
@@ -218,6 +220,16 @@
.sort((a, b) => (a.displayName ?? a.name).localeCompare(b.displayName ?? b.name)) .sort((a, b) => (a.displayName ?? a.name).localeCompare(b.displayName ?? b.name))
.reduce((p, c) => ({ ...p, [c.category]: unique(c.category in p ? [...p[c.category], c.displayName ?? c.name] : [c.displayName ?? c.name]) }), {} as Record<string, string[]>); .reduce((p, c) => ({ ...p, [c.category]: unique(c.category in p ? [...p[c.category], c.displayName ?? c.name] : [c.displayName ?? c.name]) }), {} as Record<string, string[]>);
} }
let deleteTerm = "";
async function deleteAllItems(term: string) {
const deleteItems = list.filter((e) => e.term == term);
for (const item of deleteItems) {
await deleteData(item);
}
addOn.reloadPluginList(true);
}
</script> </script>
<div> <div>
@@ -322,6 +334,29 @@
</div> </div>
{/if} {/if}
</div> </div>
{#if isMaintenanceMode}
<div class="list">
<div>
<h3>Maintenance Commands</h3>
<div class="maintenancerow">
<label for="">Delete All of </label>
<select bind:value={deleteTerm}>
{#each allTerms as term}
<option value={term}>{term}</option>
{/each}
</select>
<button
class="status"
on:click={(evt) => {
deleteAllItems(deleteTerm);
}}
>
🗑️
</button>
</div>
</div>
</div>
{/if}
<div class="buttons"> <div class="buttons">
<label><span>Hide not applicable items</span><input type="checkbox" bind:checked={hideEven} /></label> <label><span>Hide not applicable items</span><input type="checkbox" bind:checked={hideEven} /></label>
</div> </div>
@@ -423,4 +458,13 @@
align-items: center; align-items: center;
min-height: 3em; min-height: 3em;
} }
.maintenancerow {
display: flex;
justify-content: flex-end;
align-items: center;
}
.maintenancerow label {
margin-right: 0.5em;
margin-left: 0.5em;
}
</style> </style>

148
src/SerializedFileAccess.ts Normal file
View File

@@ -0,0 +1,148 @@
import { type App, TFile, type DataWriteOptions, TFolder, TAbstractFile } from "./deps";
import { serialized } from "./lib/src/lock";
import type { FilePath } from "./lib/src/types";
import { createBinaryBlob, isDocContentSame } from "./lib/src/utils";
import type { InternalFileInfo } from "./types";
import { markChangesAreSame } from "./utils";
function getFileLockKey(file: TFile | TFolder | string) {
return `fl:${typeof (file) == "string" ? file : file.path}`;
}
function toArrayBuffer(arr: Uint8Array | ArrayBuffer | DataView): ArrayBufferLike {
if (arr instanceof Uint8Array) {
return arr.buffer;
}
if (arr instanceof DataView) {
return arr.buffer;
}
return arr;
}
async function processReadFile<T>(file: TFile | TFolder | string, proc: () => Promise<T>) {
const ret = await serialized(getFileLockKey(file), () => proc());
return ret;
}
async function processWriteFile<T>(file: TFile | TFolder | string, proc: () => Promise<T>) {
const ret = await serialized(getFileLockKey(file), () => proc());
return ret;
}
export class SerializedFileAccess {
app: App
constructor(app: App) {
this.app = app;
}
async adapterStat(file: TFile | string) {
const path = file instanceof TFile ? file.path : file;
return await processReadFile(file, () => this.app.vault.adapter.stat(path));
}
async adapterExists(file: TFile | string) {
const path = file instanceof TFile ? file.path : file;
return await processReadFile(file, () => this.app.vault.adapter.exists(path));
}
async adapterRemove(file: TFile | string) {
const path = file instanceof TFile ? file.path : file;
return await processReadFile(file, () => this.app.vault.adapter.remove(path));
}
async adapterRead(file: TFile | string) {
const path = file instanceof TFile ? file.path : file;
return await processReadFile(file, () => this.app.vault.adapter.read(path));
}
async adapterReadBinary(file: TFile | string) {
const path = file instanceof TFile ? file.path : file;
return await processReadFile(file, () => this.app.vault.adapter.readBinary(path));
}
async adapterWrite(file: TFile | string, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions) {
const path = file instanceof TFile ? file.path : file;
if (typeof (data) === "string") {
return await processWriteFile(file, () => this.app.vault.adapter.write(path, data, options));
} else {
return await processWriteFile(file, () => this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), options));
}
}
async vaultCacheRead(file: TFile) {
return await processReadFile(file, () => this.app.vault.cachedRead(file));
}
async vaultRead(file: TFile) {
return await processReadFile(file, () => this.app.vault.read(file));
}
async vaultReadBinary(file: TFile) {
return await processReadFile(file, () => this.app.vault.readBinary(file));
}
async vaultModify(file: TFile, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions) {
if (typeof (data) === "string") {
return await processWriteFile(file, async () => {
const oldData = await this.app.vault.read(file);
if (data === oldData) {
markChangesAreSame(file, file.stat.mtime, options.mtime);
return false
}
await this.app.vault.modify(file, data, options)
return true;
}
);
} else {
return await processWriteFile(file, async () => {
const oldData = await this.app.vault.readBinary(file);
if (await isDocContentSame(createBinaryBlob(oldData), createBinaryBlob(data))) {
markChangesAreSame(file, file.stat.mtime, options.mtime);
return false;
}
await this.app.vault.modifyBinary(file, toArrayBuffer(data), options)
return true;
});
}
}
async vaultCreate(path: string, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions): Promise<TFile> {
if (typeof (data) === "string") {
return await processWriteFile(path, () => this.app.vault.create(path, data, options));
} else {
return await processWriteFile(path, () => this.app.vault.createBinary(path, toArrayBuffer(data), options));
}
}
async delete(file: TFile | TFolder, force = false) {
return await processWriteFile(file, () => this.app.vault.delete(file, force));
}
async trash(file: TFile | TFolder, force = false) {
return await processWriteFile(file, () => this.app.vault.trash(file, force));
}
getAbstractFileByPath(path: FilePath | string): TAbstractFile | null {
// Disabled temporary.
return this.app.vault.getAbstractFileByPath(path);
// // Hidden API but so useful.
// // @ts-ignore
// if ("getAbstractFileByPathInsensitive" in app.vault && (app.vault.adapter?.insensitive ?? false)) {
// // @ts-ignore
// return app.vault.getAbstractFileByPathInsensitive(path);
// } else {
// return app.vault.getAbstractFileByPath(path);
// }
}
touchedFiles: string[] = [];
touch(file: TFile | FilePath) {
const f = file instanceof TFile ? file : this.getAbstractFileByPath(file) as TFile;
const key = `${f.path}-${f.stat.mtime}-${f.stat.size}`;
this.touchedFiles.unshift(key);
this.touchedFiles = this.touchedFiles.slice(0, 100);
}
recentlyTouched(file: TFile | InternalFileInfo) {
const key = file instanceof TFile ? `${file.path}-${file.stat.mtime}-${file.stat.size}` : `${file.path}-${file.mtime}-${file.size}`;
if (this.touchedFiles.indexOf(key) == -1) return false;
return true;
}
clearTouched() {
this.touchedFiles = [];
}
}

View File

@@ -1,36 +1,37 @@
import type { SerializedFileAccess } from "./SerializedFileAccess";
import { Plugin, TAbstractFile, TFile, TFolder } from "./deps"; import { Plugin, TAbstractFile, TFile, TFolder } from "./deps";
import { Logger } from "./lib/src/logger";
import { isPlainText, shouldBeIgnored } from "./lib/src/path"; import { isPlainText, shouldBeIgnored } from "./lib/src/path";
import { getGlobalStore } from "./lib/src/store"; import type { KeyedQueueProcessor } from "./lib/src/processor";
import { type FilePath, type ObsidianLiveSyncSettings } from "./lib/src/types"; import { LOG_LEVEL_NOTICE, type FilePath, type ObsidianLiveSyncSettings } from "./lib/src/types";
import { type FileEventItem, type FileEventType, type FileInfo, type InternalFileInfo, type queueItem } from "./types"; import { delay } from "./lib/src/utils";
import { recentlyTouched } from "./utils"; import { type FileEventItem, type FileEventType, type FileInfo, type InternalFileInfo } from "./types";
export abstract class StorageEventManager { export abstract class StorageEventManager {
abstract fetchEvent(): FileEventItem | false; abstract beginWatch(): void;
abstract cancelRelativeEvent(item: FileEventItem): void;
abstract getQueueLength(): number;
} }
type LiveSyncForStorageEventManager = Plugin & type LiveSyncForStorageEventManager = Plugin &
{ {
settings: ObsidianLiveSyncSettings settings: ObsidianLiveSyncSettings
ignoreFiles: string[], ignoreFiles: string[],
vaultAccess: SerializedFileAccess
} & { } & {
isTargetFile: (file: string | TAbstractFile) => Promise<boolean>, isTargetFile: (file: string | TAbstractFile) => Promise<boolean>,
procFileEvent: (applyBatch?: boolean) => Promise<any>, fileEventQueue: KeyedQueueProcessor<FileEventItem, any>,
isFileSizeExceeded: (size: number) => boolean;
}; };
export class StorageEventManagerObsidian extends StorageEventManager { export class StorageEventManagerObsidian extends StorageEventManager {
plugin: LiveSyncForStorageEventManager; plugin: LiveSyncForStorageEventManager;
queuedFilesStore = getGlobalStore("queuedFiles", { queuedItems: [] as queueItem[], fileEventItems: [] as FileEventItem[] });
watchedFileEventQueue = [] as FileEventItem[];
constructor(plugin: LiveSyncForStorageEventManager) { constructor(plugin: LiveSyncForStorageEventManager) {
super(); super();
this.plugin = plugin; this.plugin = plugin;
}
beginWatch() {
const plugin = this.plugin;
this.watchVaultChange = this.watchVaultChange.bind(this); this.watchVaultChange = this.watchVaultChange.bind(this);
this.watchVaultCreate = this.watchVaultCreate.bind(this); this.watchVaultCreate = this.watchVaultCreate.bind(this);
this.watchVaultDelete = this.watchVaultDelete.bind(this); this.watchVaultDelete = this.watchVaultDelete.bind(this);
@@ -42,6 +43,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
plugin.registerEvent(plugin.app.vault.on("create", this.watchVaultCreate)); plugin.registerEvent(plugin.app.vault.on("create", this.watchVaultCreate));
//@ts-ignore : Internal API //@ts-ignore : Internal API
plugin.registerEvent(plugin.app.vault.on("raw", this.watchVaultRawEvents)); plugin.registerEvent(plugin.app.vault.on("raw", this.watchVaultRawEvents));
plugin.fileEventQueue.startPipeline();
} }
watchVaultCreate(file: TAbstractFile, ctx?: any) { watchVaultCreate(file: TAbstractFile, ctx?: any) {
@@ -89,7 +91,8 @@ export class StorageEventManagerObsidian extends StorageEventManager {
} }
// Cache file and waiting to can be proceed. // Cache file and waiting to can be proceed.
async appendWatchEvent(params: { type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string }[], ctx?: any) { async appendWatchEvent(params: { type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string }[], ctx?: any) {
let forcePerform = false; if (!this.plugin.settings.isConfigured) return;
if (this.plugin.settings.suspendFileWatching) return;
for (const param of params) { for (const param of params) {
if (shouldBeIgnored(param.file.path)) { if (shouldBeIgnored(param.file.path)) {
continue; continue;
@@ -98,51 +101,29 @@ export class StorageEventManagerObsidian extends StorageEventManager {
const type = param.type; const type = param.type;
const file = param.file; const file = param.file;
const oldPath = param.oldPath; const oldPath = param.oldPath;
const size = file instanceof TFile ? file.stat.size : (file as InternalFileInfo)?.size ?? 0;
if (this.plugin.isFileSizeExceeded(size) && (type == "CREATE" || type == "CHANGED")) {
Logger(`The storage file has been changed but exceeds the maximum size. Skipping: ${param.file.path}`, LOG_LEVEL_NOTICE);
continue;
}
if (file instanceof TFolder) continue; if (file instanceof TFolder) continue;
if (!await this.plugin.isTargetFile(file.path)) continue; if (!await this.plugin.isTargetFile(file.path)) continue;
if (this.plugin.settings.suspendFileWatching) continue;
let cache: null | string | ArrayBuffer; let cache: null | string | ArrayBuffer;
// new file or something changed, cache the changes. // new file or something changed, cache the changes.
if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) { if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) {
if (recentlyTouched(file)) { // Wait for a bit while to let the writer has marked `touched` at the file.
await delay(10);
if (this.plugin.vaultAccess.recentlyTouched(file)) {
continue; continue;
} }
if (!isPlainText(file.name)) { if (!isPlainText(file.name)) {
cache = await this.plugin.app.vault.readBinary(file); cache = await this.plugin.vaultAccess.vaultReadBinary(file);
} else { } else {
// cache = await this.app.vault.read(file); cache = await this.plugin.vaultAccess.vaultCacheRead(file);
cache = await this.plugin.app.vault.cachedRead(file); if (!cache) cache = await this.plugin.vaultAccess.vaultRead(file);
if (!cache) cache = await this.plugin.app.vault.read(file);
} }
} }
if (type == "DELETE" || type == "RENAME") {
forcePerform = true;
}
if (this.plugin.settings.batchSave && !this.plugin.settings.liveSync) {
// if the latest event is the same type, omit that
// a.md MODIFY <- this should be cancelled when a.md MODIFIED
// b.md MODIFY <- this should be cancelled when b.md MODIFIED
// a.md MODIFY
// a.md CREATE
// :
let i = this.watchedFileEventQueue.length;
L1:
while (i >= 0) {
i--;
if (i < 0) break L1;
if (this.watchedFileEventQueue[i].args.file.path != file.path) {
continue L1;
}
if (this.watchedFileEventQueue[i].type != type) break L1;
this.watchedFileEventQueue.remove(this.watchedFileEventQueue[i]);
//this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
}
}
const fileInfo = file instanceof TFile ? { const fileInfo = file instanceof TFile ? {
ctime: file.stat.ctime, ctime: file.stat.ctime,
mtime: file.stat.mtime, mtime: file.stat.mtime,
@@ -150,7 +131,8 @@ export class StorageEventManagerObsidian extends StorageEventManager {
path: file.path, path: file.path,
size: file.stat.size size: file.stat.size
} as FileInfo : file as InternalFileInfo; } as FileInfo : file as InternalFileInfo;
this.watchedFileEventQueue.push({
this.plugin.fileEventQueue.enqueueWithKey(`file-${fileInfo.path}`, {
type, type,
args: { args: {
file: fileInfo, file: fileInfo,
@@ -161,21 +143,5 @@ export class StorageEventManagerObsidian extends StorageEventManager {
key: atomicKey key: atomicKey
}) })
} }
// this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
this.plugin.procFileEvent(forcePerform);
}
fetchEvent(): FileEventItem | false {
if (this.watchedFileEventQueue.length == 0) return false;
const item = this.watchedFileEventQueue.shift();
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
return item;
}
cancelRelativeEvent(item: FileEventItem) {
this.watchedFileEventQueue = [...this.watchedFileEventQueue].filter(e => e.key != item.key);
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
}
getQueueLength() {
return this.watchedFileEventQueue.length;
} }
} }

View File

@@ -58,8 +58,7 @@ export class InputStringDialog extends Modal {
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
this.titleEl.setText(this.title); this.titleEl.setText(this.title);
// For enter to submit const formEl = contentEl.createDiv();
const formEl = contentEl.createEl("form");
new Setting(formEl).setName(this.key).setClass(this.isPassword ? "password-input" : "normal-input").addText((text) => new Setting(formEl).setName(this.key).setClass(this.isPassword ? "password-input" : "normal-input").addText((text) =>
text.onChange((value) => { text.onChange((value) => {
this.result = value; this.result = value;

Submodule src/lib updated: 609c7aecf3...1c8ed1d974

File diff suppressed because it is too large Load Diff

7
src/stores.ts Normal file
View File

@@ -0,0 +1,7 @@
import { PersistentMap } from "./lib/src/PersistentMap";
export let sameChangePairs: PersistentMap<number[]>;
export function initializeStores(vaultName: string) {
sameChangePairs = new PersistentMap<number[]>(`ls-persist-same-changes-${vaultName}`);
}

View File

@@ -1,12 +1,16 @@
import { type DataWriteOptions, normalizePath, TFile, Platform, TAbstractFile, App, Plugin, type RequestUrlParam, requestUrl } from "./deps"; import { normalizePath, Platform, TAbstractFile, App, type RequestUrlParam, requestUrl, TFile } from "./deps";
import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid, stripAllPrefixes } from "./lib/src/path"; import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid, stripAllPrefixes } from "./lib/src/path";
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "./lib/src/types"; import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "./lib/src/types";
import { CHeader, ICHeader, ICHeaderLength, ICXHeader, PSCHeader } from "./types"; import { CHeader, ICHeader, ICHeaderLength, ICXHeader, PSCHeader } from "./types";
import { InputStringDialog, PopoverSelectString } from "./dialogs"; import { InputStringDialog, PopoverSelectString } from "./dialogs";
import ObsidianLiveSyncPlugin from "./main"; import type ObsidianLiveSyncPlugin from "./main";
import { writeString } from "./lib/src/strbin"; import { writeString } from "./lib/src/strbin";
import { fireAndForget } from "./lib/src/utils";
import { sameChangePairs } from "./stores";
export { scheduleTask, setPeriodicTask, cancelTask, cancelAllTasks, cancelPeriodicTask, cancelAllPeriodicTask, } from "./lib/src/task";
// For backward compatibility, using the path for determining id. // For backward compatibility, using the path for determining id.
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/". // Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
@@ -43,49 +47,6 @@ export function getPathFromTFile(file: TAbstractFile) {
return file.path as FilePath; return file.path as FilePath;
} }
const tasks: { [key: string]: ReturnType<typeof setTimeout> } = {};
export function scheduleTask(key: string, timeout: number, proc: (() => Promise<any> | void), skipIfTaskExist?: boolean) {
if (skipIfTaskExist && key in tasks) {
return;
}
cancelTask(key);
tasks[key] = setTimeout(async () => {
delete tasks[key];
await proc();
}, timeout);
}
export function cancelTask(key: string) {
if (key in tasks) {
clearTimeout(tasks[key]);
delete tasks[key];
}
}
export function cancelAllTasks() {
for (const v in tasks) {
clearTimeout(tasks[v]);
delete tasks[v];
}
}
const intervals: { [key: string]: ReturnType<typeof setInterval> } = {};
export function setPeriodicTask(key: string, timeout: number, proc: (() => Promise<any> | void)) {
cancelPeriodicTask(key);
intervals[key] = setInterval(async () => {
delete intervals[key];
await proc();
}, timeout);
}
export function cancelPeriodicTask(key: string) {
if (key in intervals) {
clearInterval(intervals[key]);
delete intervals[key];
}
}
export function cancelAllPeriodicTask() {
for (const v in intervals) {
clearInterval(intervals[v]);
delete intervals[v];
}
}
const memos: { [key: string]: any } = {}; const memos: { [key: string]: any } = {};
export function memoObject<T>(key: string, obj: T): T { export function memoObject<T>(key: string, obj: T): T {
@@ -303,20 +264,6 @@ export function flattenObject(obj: Record<string | number | symbol, any>, path:
return ret; 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) { export function isValidPath(filename: string) {
if (Platform.isDesktop) { if (Platform.isDesktop) {
@@ -332,38 +279,10 @@ export function isValidPath(filename: string) {
return isValidFilenameInWidows(filename); return isValidFilenameInWidows(filename);
} }
let touchedFiles: string[] = [];
export function getAbstractFileByPath(path: FilePath): TAbstractFile | null {
// Disabled temporary.
return app.vault.getAbstractFileByPath(path);
// // Hidden API but so useful.
// // @ts-ignore
// if ("getAbstractFileByPathInsensitive" in app.vault && (app.vault.adapter?.insensitive ?? false)) {
// // @ts-ignore
// return app.vault.getAbstractFileByPathInsensitive(path);
// } else {
// return app.vault.getAbstractFileByPath(path);
// }
}
export function trimPrefix(target: string, prefix: string) { export function trimPrefix(target: string, prefix: string) {
return target.startsWith(prefix) ? target.substring(prefix.length) : target; return target.startsWith(prefix) ? target.substring(prefix.length) : target;
} }
export function touch(file: TFile | FilePath) {
const f = file instanceof TFile ? file : getAbstractFileByPath(file) as TFile;
const key = `${f.path}-${f.stat.mtime}-${f.stat.size}`;
touchedFiles.unshift(key);
touchedFiles = touchedFiles.slice(0, 100);
}
export function recentlyTouched(file: TFile) {
const key = `${file.path}-${file.stat.mtime}-${file.stat.size}`;
if (touchedFiles.indexOf(key) == -1) return false;
return true;
}
export function clearTouched() {
touchedFiles = [];
}
/** /**
* returns is internal chunk of file * returns is internal chunk of file
@@ -419,8 +338,8 @@ export const askString = (app: App, title: string, key: string, placeholder: str
export class PeriodicProcessor { export class PeriodicProcessor {
_process: () => Promise<any>; _process: () => Promise<any>;
_timer?: number; _timer?: number;
_plugin: Plugin; _plugin: ObsidianLiveSyncPlugin;
constructor(plugin: Plugin, process: () => Promise<any>) { constructor(plugin: ObsidianLiveSyncPlugin, process: () => Promise<any>) {
this._plugin = plugin; this._plugin = plugin;
this._process = process; this._process = process;
} }
@@ -434,12 +353,19 @@ export class PeriodicProcessor {
enable(interval: number) { enable(interval: number) {
this.disable(); this.disable();
if (interval == 0) return; if (interval == 0) return;
this._timer = window.setInterval(() => this.process().then(() => { }), interval); this._timer = window.setInterval(() => fireAndForget(async () => {
await this.process();
if (this._plugin._unloaded) {
this.disable();
}
}), interval);
this._plugin.registerInterval(this._timer); this._plugin.registerInterval(this._timer);
} }
disable() { disable() {
if (this._timer !== undefined) window.clearInterval(this._timer); if (this._timer !== undefined) {
this._timer = undefined; window.clearInterval(this._timer);
this._timer = undefined;
}
} }
} }
@@ -481,7 +407,7 @@ export const requestToCouchDB = async (baseUri: string, username: string, passwo
export async function performRebuildDB(plugin: ObsidianLiveSyncPlugin, method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice") { export async function performRebuildDB(plugin: ObsidianLiveSyncPlugin, method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice") {
if (method == "localOnly") { if (method == "localOnly") {
await plugin.addOnSetup.fetchLocalWithKeepLocal(); await plugin.addOnSetup.fetchLocal();
} }
if (method == "remoteOnly") { if (method == "remoteOnly") {
await plugin.addOnSetup.rebuildRemote(); await plugin.addOnSetup.rebuildRemote();
@@ -490,3 +416,48 @@ export async function performRebuildDB(plugin: ObsidianLiveSyncPlugin, method: "
await plugin.addOnSetup.rebuildEverything(); await plugin.addOnSetup.rebuildEverything();
} }
} }
export const BASE_IS_NEW = Symbol("base");
export const TARGET_IS_NEW = Symbol("target");
export const EVEN = Symbol("even");
// Why 2000? : ZIP FILE Does not have enough resolution.
const resolution = 2000;
export function compareMTime(baseMTime: number, targetMTime: number): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN {
const truncatedBaseMTime = (~~(baseMTime / resolution)) * resolution;
const truncatedTargetMTime = (~~(targetMTime / resolution)) * resolution;
// Logger(`Resolution MTime ${truncatedBaseMTime} and ${truncatedTargetMTime} `, LOG_LEVEL_VERBOSE);
if (truncatedBaseMTime == truncatedTargetMTime) return EVEN;
if (truncatedBaseMTime > truncatedTargetMTime) return BASE_IS_NEW;
if (truncatedBaseMTime < truncatedTargetMTime) return TARGET_IS_NEW;
throw new Error("Unexpected error");
}
export function markChangesAreSame(file: TFile | AnyEntry | string, mtime1: number, mtime2: number) {
if (mtime1 === mtime2) return true;
const key = typeof file == "string" ? file : file instanceof TFile ? file.path : file.path ?? file._id;
const pairs = sameChangePairs.get(key, []);
if (pairs.some(e => e == mtime1 || e == mtime2)) {
sameChangePairs.set(key, [...new Set([...pairs, mtime1, mtime2])]);
} else {
sameChangePairs.set(key, [mtime1, mtime2]);
}
}
export function isMarkedAsSameChanges(file: TFile | AnyEntry | string, mtimes: number[]) {
const key = typeof file == "string" ? file : file instanceof TFile ? file.path : file.path ?? file._id;
const pairs = sameChangePairs.get(key, []);
if (mtimes.every(e => pairs.indexOf(e) !== -1)) {
return EVEN;
}
}
export function compareFileFreshness(baseFile: TFile | AnyEntry, checkTarget: TFile | AnyEntry): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN {
const modifiedBase = baseFile instanceof TFile ? baseFile?.stat?.mtime ?? 0 : baseFile?.mtime ?? 0;
const modifiedTarget = checkTarget instanceof TFile ? checkTarget?.stat?.mtime ?? 0 : checkTarget?.mtime ?? 0;
if (modifiedBase && modifiedTarget && isMarkedAsSameChanges(baseFile, [modifiedBase, modifiedTarget])) {
return EVEN;
}
return compareMTime(modifiedBase, modifiedTarget);
}

View File

@@ -85,7 +85,6 @@
} */ } */
.sls-header-button { .sls-header-button {
margin-left: 2em; margin-left: 2em;
} }
@@ -98,9 +97,19 @@
--slsmessage: ""; --slsmessage: "";
} }
.sls-troubleshoot-preview {
max-width: max-content;
}
.sls-troubleshoot-preview img {
max-width: 100%;
}
.CodeMirror-wrap::before, .CodeMirror-wrap::before,
.cm-s-obsidian>.cm-editor::before, .markdown-preview-view.cm-s-obsidian::before,
.canvas-wrapper::before { .markdown-source-view.cm-s-obsidian::before,
.canvas-wrapper::before,
.empty-state::before {
content: attr(data-log); content: attr(data-log);
text-align: right; text-align: right;
white-space: pre-wrap; white-space: pre-wrap;
@@ -116,6 +125,19 @@
filter: grayscale(100%); filter: grayscale(100%);
} }
.empty-state::before,
.markdown-preview-view.cm-s-obsidian::before,
.markdown-source-view.cm-s-obsidian::before {
top: var(--header-height);
right: 1em;
}
.is-mobile .empty-state::before,
.is-mobile .markdown-preview-view.cm-s-obsidian::before,
.is-mobile .markdown-source-view.cm-s-obsidian::before {
top: var(--view-header-height);
right: 1em;
}
.canvas-wrapper::before { .canvas-wrapper::before {
right: 48px; right: 48px;
} }
@@ -124,7 +146,7 @@
right: 0px; right: 0px;
} }
.cm-s-obsidian>.cm-editor::before { .cm-s-obsidian > .cm-editor::before {
right: 16px; right: 16px;
} }
@@ -153,8 +175,8 @@ div.sls-setting-menu-btn {
/* width: 100%; */ /* width: 100%; */
} }
.sls-setting-tab:hover~div.sls-setting-menu-btn, .sls-setting-tab:hover ~ div.sls-setting-menu-btn,
.sls-setting-label.selected .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); background-color: var(--interactive-accent);
color: var(--text-on-accent); color: var(--text-on-accent);
} }
@@ -257,8 +279,8 @@ div.sls-setting-menu-btn {
display: none; display: none;
} }
.password-input > .setting-item-control >input { .password-input > .setting-item-control > input {
-webkit-text-security: disc; -webkit-text-security: disc;
} }
span.ls-mark-cr::after { span.ls-mark-cr::after {
@@ -270,4 +292,39 @@ span.ls-mark-cr::after {
.deleted span.ls-mark-cr::after { .deleted span.ls-mark-cr::after {
color: var(--text-on-accent); color: var(--text-on-accent);
} }
.ls-imgdiff-wrap {
display: flex;
justify-content: center;
align-items: center;
}
.ls-imgdiff-wrap .overlay {
position: relative;
}
.ls-imgdiff-wrap .overlay .img-base {
position: relative;
top: 0;
left: 0;
}
.ls-imgdiff-wrap .overlay .img-overlay {
-webkit-filter: invert(100%) opacity(50%);
filter: invert(100%) opacity(50%);
position: absolute;
top: 0;
left: 0;
animation: ls-blink-diff 0.5s cubic-bezier(0.4, 0, 1, 1) infinite alternate;
}
@keyframes ls-blink-diff {
0% {
opacity: 0;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}

View File

@@ -16,6 +16,7 @@
"importHelpers": false, "importHelpers": false,
"alwaysStrict": true, "alwaysStrict": true,
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"noEmit": true,
"lib": [ "lib": [
"es2018", "es2018",
"DOM", "DOM",

View File

@@ -1,43 +1,113 @@
### 0.20.0 ### 0.22.0
A few years passed since Self-hosted LiveSync was born, and our codebase had been very complicated. This could be patient now, but it should be a tremendous hurt.
Therefore at v0.22.0, for future maintainability, I refined task scheduling logic totally.
At 0.20.0, Self-hosted LiveSync has changed the binary file format and encrypting format, for efficient synchronisation. Of course, I think this would be our suffering in some cases. However, I would love to ask you for your cooperation and contribution.
The dialogue will be shown and asks us to decide whether to keep v1 or use v2. Once we have enabled v2, all subsequent edits will be saved in v2. Therefore, devices running 0.19 or below cannot understand this and they might say that decryption error. Please update all devices.
Then we will have an impressive performance.
Of course, these are very impactful changes. If you have any questions or troubled things, please feel free to open an issue and mention me. Sorry for being absent so much long. And thank you for your patience!
Note: if you want to roll it back to v1, please enable `Use binary and encryption version 1` on the `Hatch` pane and perform the `rebuild everything` once. Note: we got a very performance improvement.
Note at 0.22.2: **Now, to rescue mobile devices, Maximum file size is set to 50 by default**. Please configure the limit as you need. If you do not want to limit the sizes, set zero manually, please.
Extra but notable information:
This format change gives us the ability to detect some `marks` in the binary files as same as text files. Therefore, we can split binary files and some specific sort of them (i.e., PDF files) at the specific character. It means that editing the middle of files could be detected with marks.
Now only a few chunks are transferred, even if we add a comment to the PDF or put new files into the ZIP archives.
#### Version history #### Version history
- 0.20.2 - 0.22.7
- Fixed:
- No longer deleted hidden files were ignored.
- The document history dialogue is now able to process the deleted revisions.
- Deletion of a hidden file is now surely performed even if the file is already conflicted.
- 0.22.6
- Fixed:
- Fixed a problem with synchronisation taking a long time to start in some cases.
- The first synchronisation after update might take a bit longer.
- Now we can disable E2EE encryption.
- Improved:
- `Setup Wizard` is now more clear.
- `Minimal Setup` is now more simple.
- Self-hosted LiveSync now be able to use even if there are vaults with the same name.
- Database suffix will automatically added.
- Now Self-hosted LiveSync waits until set-up is complete.
- Show reload prompts when possibly recommended while settings.
- New feature: - New feature:
- We can delete all data of customization sync from the `Delete all customization sync data` on the `Hatch` pane. - A guidance dialogue prompting for settings will be shown after the installation.
- Changed
- `Open setup URI` is now `Use the copied setup URI`
- `Copy setup URI` is now `Copy current settings as a new setup URI`
- `Setup Wizard` is now `Minimal Setup`
- `Check database configuration` is now `Check and Fix database configuration`
- 0.22.5
- Fixed: - Fixed:
- Prevent keep restarting on iOS by yielding microtasks. - Some description of settings have been refined
- 0.20.1 - New feature:
- TroubleShooting is now shown in the setting dialogue.
- 0.22.4
- Fixed: - Fixed:
- No more UI freezing and keep restarting on iOS. - Now the result of conflict resolution could be surely written into the storage.
- Diff of Non-markdown documents are now shown correctly. - Deleted files can be handled correctly again in the history dialogue and conflict dialogue.
- Some wrong log messages were fixed.
- Change handling now has become more stable.
- Some event handling became to be safer.
- Improved: - Improved:
- Performance has been a bit improved. - Dumping document information shows conflicts and revisions.
- Customization sync has gotten faster. - The timestamp-only differences can be surely cached.
- However, We lost forward compatibility again (only for this feature). Please update all devices. - Timestamp difference detection can be rounded by two seconds.
- Misc - Refactored:
- Terser configuration has been more aggressive. - A bit of organisation to write the test.
- 0.20.0 - 0.22.3
- Improved:
- A New binary file handling implemented
- A new encrypted format has been implemented
- Now the chunk sizes will be adjusted for efficient sync
- Fixed: - Fixed:
- levels of exception in some logs have been fixed - No longer detects storage changes which have been caused by Self-hosted LiveSync itself.
- Tidied: - Setting sync file will be detected only if it has been configured now.
- Some Lint warnings have been suppressed. - And its log will be shown only while the verbose log is enabled.
- Customisation file enumeration has got less blingy.
- Deletion of files is now reliably synchronised.
- Fixed and improved:
- In-editor-status is now shown in the following areas:
- Note editing pane (Source mode and live-preview mode).
- New tab pane.
- Canvas pane.
- 0.22.2
- Fixed:
- Now the results of resolving conflicts are surely synchronised.
- Modified:
- Some setting items got new clear names. (`Sync Settings` -> `Targets`).
- New feature:
- We can limit the synchronising files by their size. (`Sync Settings` -> `Targets` -> `Maximum file size`).
- It depends on the size of the newer one.
- At Obsidian 1.5.3 on mobile, we should set this to around 50MB to avoid restarting Obsidian.
- Now the settings could be stored in a specific markdown file to synchronise or switch it (`General Setting` -> `Share settings via markdown`).
- [Screwdriver](https://github.com/vrtmrz/obsidian-screwdriver) is quite good, but mostly we only need this.
- Customisation of the obsoleted device is now able to be deleted at once.
- We have to put the maintenance mode in at the Customisation sync dialogue.
- 0.22.1
- New feature:
- We can perform automatic conflict resolution for inactive files, and postpone only manual ones by `Postpone manual resolution of inactive files`.
- Now we can see the image in the document history dialogue.
- We can see the difference of the image, in the document history dialogue.
- And also we can highlight differences.
- Improved:
- Hidden file sync has been stabilised.
- Now automatically reloads the conflict-resolution dialogue when new conflicted revisions have arrived.
- Fixed:
- No longer periodic process runs after unloading the plug-in.
- Now the modification of binary files is surely stored in the storage.
- 0.22.0
- Refined:
- Task scheduling logics has been rewritten.
- Screen updates are also now efficient.
- Possibly many bugs and fragile behaviour has been fixed.
- Status updates and logging have been thinned out to display.
- Fixed:
- Remote-chunk-fetching now works with keeping request intervals
- New feature:
- We can show only the icons in the editor.
- Progress indicators have been more meaningful:
- 📥 Unprocessed transferred items
- 📄 Working database operation
- 💾 Working write storage processes
- ⏳ Working read storage processes
- 🛫 Pending read storage processes
- ⚙️ Working or pending storage processes of hidden files
- 🧩 Waiting chunks
- 🔌 Working Customisation items (Configuration, snippets and plug-ins)
... To continue on to `updates_old.md`.
... To continue on to `updates_old.md`.

View File

@@ -1,3 +1,112 @@
### 0.21.0
The E2EE encryption V2 format has been reverted. That was probably the cause of the glitch.
Instead, to maintain efficiency, files are treated with Blob until just before saving. Along with this, the old-fashioned encryption format has also been discontinued.
There are both forward and backwards compatibilities, with recent versions. However, unfortunately, we lost compatibility with filesystem-livesync or some.
It will be addressed soon. Please be patient if you are using filesystem-livesync with E2EE.
- 0.21.5
- Improved:
- Now all revisions will be shown only its first a few letters.
- Now ID of the documents is shown in the log with the first 8 letters.
- Fixed:
- Check before modifying files has been implemented.
- Content change detection has been improved.
- 0.21.4
- This release had been skipped.
- 0.21.3
- Implemented:
- Now we can use SHA1 for hash function as fallback.
- 0.21.2
- IMPORTANT NOTICE: **0.21.1 CONTAINS A BUG WHILE REBUILDING THE DATABASE. IF YOU HAVE BEEN REBUILT, PLEASE MAKE SURE THAT ALL FILES ARE SANE.**
- This has been fixed in this version.
- Fixed:
- No longer files are broken while rebuilding.
- Now, Large binary files can be written correctly on a mobile platform.
- Any decoding errors now make zero-byte files.
- Modified:
- All files are processed sequentially for each.
- 0.21.1
- Fixed:
- No more infinity loops on larger files.
- Show message on decode error.
- Refactored:
- Fixed to avoid obsolete global variables.
- 0.21.0
- Changes and performance improvements:
- Now the saving files are processed by Blob.
- The V2-Format has been reverted.
- New encoding format has been enabled in default.
- WARNING: Since this version, the compatibilities with older Filesystem LiveSync have been lost.
## 0.20.0
At 0.20.0, Self-hosted LiveSync has changed the binary file format and encrypting format, for efficient synchronisation.
The dialogue will be shown and asks us to decide whether to keep v1 or use v2. Once we have enabled v2, all subsequent edits will be saved in v2. Therefore, devices running 0.19 or below cannot understand this and they might say that decryption error. Please update all devices.
Then we will have an impressive performance.
Of course, these are very impactful changes. If you have any questions or troubled things, please feel free to open an issue and mention me.
Note: if you want to roll it back to v1, please enable `Use binary and encryption version 1` on the `Hatch` pane and perform the `rebuild everything` once.
Extra but notable information:
This format change gives us the ability to detect some `marks` in the binary files as same as text files. Therefore, we can split binary files and some specific sort of them (i.e., PDF files) at the specific character. It means that editing the middle of files could be detected with marks.
Now only a few chunks are transferred, even if we add a comment to the PDF or put new files into the ZIP archives.
- 0.20.7
- Fixed
- To better replication, path obfuscation is now deterministic even if with E2EE.
Note: Compatible with previous database without any conversion. Only new files will be obfuscated in deterministic.
- 0.20.6
- Fixed
- Now empty file could be decoded.
- Local files are no longer pre-saved before fetching from a remote database.
- No longer deadlock while applying customisation sync.
- Configuration with multiple files is now able to be applied correctly.
- Deleting folder propagation now works without enabling the use of a trash bin.
- 0.20.5
- Fixed
- Now the files which having digit or character prefixes in the path will not be ignored.
- 0.20.4
- Fixed
- The text-input-dialogue is no longer broken.
- Finally, we can use the Setup URI again on mobile.
- 0.20.3
- New feature:
- We can launch Customization sync from the Ribbon if we enabled it.
- Fixed:
- Setup URI is now back to the previous spec; be encrypted by V1.
- It may avoid the trouble with iOS 17.
- The Settings dialogue is now registered at the beginning of the start-up process.
- We can change the configuration even though LiveSync could not be launched in normal.
- Improved:
- Enumerating documents has been faster.
- 0.20.2
- New feature:
- We can delete all data of customization sync from the `Delete all customization sync data` on the `Hatch` pane.
- Fixed:
- Prevent keep restarting on iOS by yielding microtasks.
- 0.20.1
- Fixed:
- No more UI freezing and keep restarting on iOS.
- Diff of Non-markdown documents are now shown correctly.
- Improved:
- Performance has been a bit improved.
- Customization sync has gotten faster.
- However, We lost forward compatibility again (only for this feature). Please update all devices.
- Misc
- Terser configuration has been more aggressive.
- 0.20.0
- Improved:
- A New binary file handling implemented
- A new encrypted format has been implemented
- Now the chunk sizes will be adjusted for efficient sync
- Fixed:
- levels of exception in some logs have been fixed
- Tidied:
- Some Lint warnings have been suppressed.
### 0.19.0 ### 0.19.0
#### Customization sync #### Customization sync

1
utils/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
fly.toml

29
utils/couchdb/couchdb-init.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
if [[ -z "$hostname" ]]; then
echo "ERROR: Hostname missing"
exit 1
fi
if [[ -z "$username" ]]; then
echo "ERROR: Username missing"
exit 1
fi
if [[ -z "$password" ]]; then
echo "ERROR: Password missing"
exit 1
fi
echo "-- Configuring CouchDB by REST APIs... -->"
until (curl -X POST "${hostname}/_cluster_setup" -H "Content-Type: application/json" -d "{\"action\":\"enable_single_node\",\"username\":\"${username}\",\"password\":\"${password}\",\"bind_address\":\"0.0.0.0\",\"port\":5984,\"singlenode\":true}" --user "${username}:${password}"); do sleep 5; done
until (curl -X PUT "${hostname}/_node/nonode@nohost/_config/chttpd/require_valid_user" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done
until (curl -X PUT "${hostname}/_node/nonode@nohost/_config/chttpd_auth/require_valid_user" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done
until (curl -X PUT "${hostname}/_node/nonode@nohost/_config/httpd/WWW-Authenticate" -H "Content-Type: application/json" -d '"Basic realm=\"couchdb\""' --user "${username}:${password}"); do sleep 5; done
until (curl -X PUT "${hostname}/_node/nonode@nohost/_config/httpd/enable_cors" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done
until (curl -X PUT "${hostname}/_node/nonode@nohost/_config/chttpd/enable_cors" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done
until (curl -X PUT "${hostname}/_node/nonode@nohost/_config/chttpd/max_http_request_size" -H "Content-Type: application/json" -d '"4294967296"' --user "${username}:${password}"); do sleep 5; done
until (curl -X PUT "${hostname}/_node/nonode@nohost/_config/couchdb/max_document_size" -H "Content-Type: application/json" -d '"50000000"' --user "${username}:${password}"); do sleep 5; done
until (curl -X PUT "${hostname}/_node/nonode@nohost/_config/cors/credentials" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done
until (curl -X PUT "${hostname}/_node/nonode@nohost/_config/cors/origins" -H "Content-Type: application/json" -d '"app://obsidian.md,capacitor://localhost,http://localhost"' --user "${username}:${password}"); do sleep 5; done
echo "<-- Configuring CouchDB by REST APIs Done!"

4
utils/flyio/delete-server.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -e
fly scale count 0 -y
fly apps destroy $(fly status -j | jq -r .Name) -y

43
utils/flyio/deploy-server.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/bin/bash
## Script for deploy and automatic setup CouchDB onto fly.io.
## We need Deno for generating the Setup-URI.
source setenv.sh $@
export hostname="https://$appname.fly.dev"
echo "-- YOUR CONFIGURATION --"
echo "URL : $hostname"
echo "username: $username"
echo "password: $password"
echo "region : $region"
echo ""
echo "-- START DEPLOYING --> "
set -e
fly launch --name=$appname --env="COUCHDB_USER=$username" --copy-config=true --detach --no-deploy --region ${region} --yes
fly secrets set COUCHDB_PASSWORD=$password
fly deploy
set +e
../couchdb/couchdb-init.sh
# flyctl deploy
echo "OK!"
if command -v deno >/dev/null 2>&1; then
echo "Setup finished! Also, we can set up Self-hosted LiveSync instantly, by the following setup uri."
echo "Passphrase of setup-uri is \`welcome\`".
echo "--- configured ---"
echo "database : ${database}"
echo "E2EE passphrase: ${passphrase}"
echo "--- setup uri ---"
deno run -A generate_setupuri.ts
else
echo "Setup finished! Here is the configured values (reprise)!"
echo "-- YOUR CONFIGURATION --"
echo "URL : $hostname"
echo "username: $username"
echo "password: $password"
echo "-- YOUR CONFIGURATION --"
echo "If we had Deno, we would got the setup uri directly!"
fi

View File

@@ -0,0 +1,40 @@
## CouchDB for fly.io image
app = ''
primary_region = 'nrt'
swap_size_mb = 512
[build]
image = "couchdb:latest"
[mounts]
source = "couchdata"
destination = "/opt/couchdb/data"
initial_size = "1GB"
auto_extend_size_threshold = 90
auto_extend_size_increment = "1GB"
auto_extend_size_limit = "2GB"
[env]
COUCHDB_USER = ""
ERL_FLAGS = "-couch_ini /opt/couchdb/etc/default.ini /opt/couchdb/etc/default.d/ /opt/couchdb/etc/local.d /opt/couchdb/etc/local.ini /opt/couchdb/data/persistence.ini"
[http_service]
internal_port = 5984
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ['app']
[[vm]]
cpu_kind = 'shared'
cpus = 1
memory_mb = 256
[[files]]
guest_path = "/docker-entrypoint2.sh"
raw_value = "#!/bin/bash\ntouch /opt/couchdb/data/persistence.ini\nchmod +w /opt/couchdb/data/persistence.ini\n/docker-entrypoint.sh $@"
[experimental]
entrypoint = ["tini", "--", "/docker-entrypoint2.sh"]

View File

@@ -0,0 +1,180 @@
import { webcrypto } from "node:crypto";
const KEY_RECYCLE_COUNT = 100;
type KeyBuffer = {
key: CryptoKey;
salt: Uint8Array;
count: number;
};
let semiStaticFieldBuffer: Uint8Array;
const nonceBuffer: Uint32Array = new Uint32Array(1);
const writeString = (string: string) => {
// Prepare enough buffer.
const buffer = new Uint8Array(string.length * 4);
const length = string.length;
let index = 0;
let chr = 0;
let idx = 0;
while (idx < length) {
chr = string.charCodeAt(idx++);
if (chr < 128) {
buffer[index++] = chr;
} else if (chr < 0x800) {
// 2 bytes
buffer[index++] = 0xC0 | (chr >>> 6);
buffer[index++] = 0x80 | (chr & 0x3F);
} else if (chr < 0xD800 || chr > 0xDFFF) {
// 3 bytes
buffer[index++] = 0xE0 | (chr >>> 12);
buffer[index++] = 0x80 | ((chr >>> 6) & 0x3F);
buffer[index++] = 0x80 | (chr & 0x3F);
} else {
// 4 bytes - surrogate pair
chr = (((chr - 0xD800) << 10) | (string.charCodeAt(idx++) - 0xDC00)) + 0x10000;
buffer[index++] = 0xF0 | (chr >>> 18);
buffer[index++] = 0x80 | ((chr >>> 12) & 0x3F);
buffer[index++] = 0x80 | ((chr >>> 6) & 0x3F);
buffer[index++] = 0x80 | (chr & 0x3F);
}
}
return buffer.slice(0, index);
};
const KeyBuffs = new Map<string, KeyBuffer>();
async function getKeyForEncrypt(passphrase: string, autoCalculateIterations: boolean): Promise<[CryptoKey, Uint8Array]> {
// For performance, the plugin reuses the key KEY_RECYCLE_COUNT times.
const buffKey = `${passphrase}-${autoCalculateIterations}`;
const f = KeyBuffs.get(buffKey);
if (f) {
f.count--;
if (f.count > 0) {
return [f.key, f.salt];
}
f.count--;
}
const passphraseLen = 15 - passphrase.length;
const iteration = autoCalculateIterations ? ((passphraseLen > 0 ? passphraseLen : 0) * 1000) + 121 - passphraseLen : 100000;
const passphraseBin = new TextEncoder().encode(passphrase);
const digest = await webcrypto.subtle.digest({ name: "SHA-256" }, passphraseBin);
const keyMaterial = await webcrypto.subtle.importKey("raw", digest, { name: "PBKDF2" }, false, ["deriveKey"]);
const salt = webcrypto.getRandomValues(new Uint8Array(16));
const key = await webcrypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: iteration,
hash: "SHA-256",
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt"]
);
KeyBuffs.set(buffKey, {
key,
salt,
count: KEY_RECYCLE_COUNT,
});
return [key, salt];
}
function getSemiStaticField(reset?: boolean) {
// return fixed field of iv.
if (semiStaticFieldBuffer != null && !reset) {
return semiStaticFieldBuffer;
}
semiStaticFieldBuffer = webcrypto.getRandomValues(new Uint8Array(12));
return semiStaticFieldBuffer;
}
function getNonce() {
// This is nonce, so do not send same thing.
nonceBuffer[0]++;
if (nonceBuffer[0] > 10000) {
// reset semi-static field.
getSemiStaticField(true);
}
return nonceBuffer;
}
function arrayBufferToBase64internalBrowser(buffer: DataView | Uint8Array): Promise<string> {
return new Promise((res, rej) => {
const blob = new Blob([buffer], { type: "application/octet-binary" });
const reader = new FileReader();
reader.onload = function (evt) {
const dataURI = evt.target?.result?.toString() || "";
if (buffer.byteLength != 0 && (dataURI == "" || dataURI == "data:")) return rej(new TypeError("Could not parse the encoded string"));
const result = dataURI.substring(dataURI.indexOf(",") + 1);
res(result);
};
reader.readAsDataURL(blob);
});
}
// Map for converting hexString
const revMap: { [key: string]: number } = {};
const numMap: { [key: number]: string } = {};
for (let i = 0; i < 256; i++) {
revMap[(`00${i.toString(16)}`.slice(-2))] = i;
numMap[i] = (`00${i.toString(16)}`.slice(-2));
}
function uint8ArrayToHexString(src: Uint8Array): string {
return [...src].map(e => numMap[e]).join("");
}
const QUANTUM = 32768;
async function arrayBufferToBase64Single(buffer: ArrayBuffer): Promise<string> {
const buf = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
if (buf.byteLength < QUANTUM) return btoa(String.fromCharCode.apply(null, [...buf]));
return await arrayBufferToBase64internalBrowser(buf);
}
export async function encrypt(input: string, passphrase: string, autoCalculateIterations: boolean) {
const [key, salt] = await getKeyForEncrypt(passphrase, autoCalculateIterations);
// Create initial vector with semi-fixed part and incremental part
// I think it's not good against related-key attacks.
const fixedPart = getSemiStaticField();
const invocationPart = getNonce();
const iv = new Uint8Array([...fixedPart, ...new Uint8Array(invocationPart.buffer)]);
const plainStringified = JSON.stringify(input);
// const plainStringBuffer: Uint8Array = tex.encode(plainStringified)
const plainStringBuffer: Uint8Array = writeString(plainStringified);
const encryptedDataArrayBuffer = await webcrypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plainStringBuffer);
const encryptedData2 = (await arrayBufferToBase64Single(encryptedDataArrayBuffer));
//return data with iv and salt.
const ret = `["${encryptedData2}","${uint8ArrayToHexString(iv)}","${uint8ArrayToHexString(salt)}"]`;
return ret;
}
const URIBASE = "obsidian://setuplivesync?settings=";
async function main() {
const conf = {
"couchDB_URI": `${Deno.env.get("hostname")}`,
"couchDB_USER": `${Deno.env.get("username")}`,
"couchDB_PASSWORD": `${Deno.env.get("password")}`,
"couchDB_DBNAME": `${Deno.env.get("database")}`,
"syncOnStart": true,
"gcDelay": 0,
"periodicReplication": true,
"syncOnFileOpen": true,
"encrypt": true,
"passphrase": `${Deno.env.get("passphrase")}`,
"usePathObfuscation": true,
"batchSave": true,
"batch_size": 50,
"batches_limit": 50,
"useHistory": true,
"disableRequestURI": true,
"customChunkSize": 50,
"syncAfterMerge": false,
"concurrencyOfReadChunksOnline": 100,
"minimumIntervalOfReadChunksOnline": 100,
}
const encryptedConf = encodeURIComponent(await encrypt(JSON.stringify(conf), "welcome", false));
const theURI = `${URIBASE}${encryptedConf}`;
console.log(theURI);
}
await main();

30
utils/flyio/setenv.sh Executable file
View File

@@ -0,0 +1,30 @@
random_num() {
echo $RANDOM
}
random_noun() {
nouns=("waterfall" "river" "breeze" "moon" "rain" "wind" "sea" "morning" "snow" "lake" "sunset" "pine" "shadow" "leaf" "dawn" "glitter" "forest" "hill" "cloud" "meadow" "sun" "glade" "bird" "brook" "butterfly" "bush" "dew" "dust" "field" "fire" "flower" "firefly" "feather" "grass" "haze" "mountain" "night" "pond" "darkness" "snowflake" "silence" "sound" "sky" "shape" "surf" "thunder" "violet" "water" "wildflower" "wave" "water" "resonance" "sun" "log" "dream" "cherry" "tree" "fog" "frost" "voice" "paper" "frog" "smoke" "star")
echo ${nouns[$(($RANDOM % ${#nouns[*]}))]}
}
random_adjective() {
adjectives=("autumn" "hidden" "bitter" "misty" "silent" "empty" "dry" "dark" "summer" "icy" "delicate" "quiet" "white" "cool" "spring" "winter" "patient" "twilight" "dawn" "crimson" "wispy" "weathered" "blue" "billowing" "broken" "cold" "damp" "falling" "frosty" "green" "long" "late" "lingering" "bold" "little" "morning" "muddy" "old" "red" "rough" "still" "small" "sparkling" "thrumming" "shy" "wandering" "withered" "wild" "black" "young" "holy" "solitary" "fragrant" "aged" "snowy" "proud" "floral" "restless" "divine" "polished" "ancient" "purple" "lively" "nameless")
echo ${adjectives[$(($RANDOM % ${#adjectives[*]}))]}
}
cp ./fly.template.toml ./fly.toml
if [ "$1" = "renew" ]; then
unset appname
unset username
unset password
unset database
unset passphrase
unset region
fi
[ -z $appname ] && export appname=$(random_adjective)-$(random_noun)-$(random_num)
[ -z $username ] && export username=$(random_adjective)-$(random_noun)-$(random_num)
[ -z $password ] && export password=$(random_adjective)-$(random_noun)-$(random_num)
[ -z $database ] && export database="obsidiannotes"
[ -z $passphrase ] && export passphrase=$(random_adjective)-$(random_noun)-$(random_num)
[ -z $region ] && export region="nrt"

164
utils/readme.md Normal file
View File

@@ -0,0 +1,164 @@
<!-- For translation: 20240206r0 -->
# Utilities
Here are some useful things.
## couchdb
### couchdb-init.sh
This script can configure CouchDB with the necessary settings by REST APIs.
#### Materials
- Mandatory: curl
#### Usage
```sh
export hostname=http://localhost:5984/
export username=couchdb-admin-username
export password=couchdb-admin-password
./couchdb-init.sh
```
curl result will be shown, however, all of them can be ignored if the script has been run completely.
## fly.io
### deploy-server.sh
A fully automated CouchDB deployment script. We can deploy CouchDB onto fly.io. The only we need is an account of it.
All omitted configurations will be determined at random. (And, it is preferred). The region is configured to `nrt`.
If Japan is not close to you, please choose a region closer to you. However, the deployed database will work if you leave it at all.
#### Materials
- Mandatory: curl, flyctl
- Recommended: deno
#### Usage
```sh
#export appname=
#export username=
#export password=
#export database=
#export passphrase=
export region=nrt #pick your nearest location
./deploy-server.sh
```
The result of this command is as follows.
```
-- YOUR CONFIGURATION --
URL : https://young-darkness-25342.fly.dev
username: billowing-cherry-22580
password: misty-dew-13571
region : nrt
-- START DEPLOYING -->
An existing fly.toml file was found
Using build strategies '[the "couchdb:latest" docker image]'. Remove [build] from fly.toml to force a rescan
Creating app in /home/vorotamoroz/dev/obsidian-livesync/utils/flyio
We're about to launch your app on Fly.io. Here's what you're getting:
Organization: vorotamoroz (fly launch defaults to the personal org)
Name: young-darkness-25342 (specified on the command line)
Region: Tokyo, Japan (specified on the command line)
App Machines: shared-cpu-1x, 256MB RAM (specified on the command line)
Postgres: <none> (not requested)
Redis: <none> (not requested)
Created app 'young-darkness-25342' in organization 'personal'
Admin URL: https://fly.io/apps/young-darkness-25342
Hostname: young-darkness-25342.fly.dev
Wrote config file fly.toml
Validating /home/vorotamoroz/dev/obsidian-livesync/utils/flyio/fly.toml
Platform: machines
✓ Configuration is valid
Your app is ready! Deploy with `flyctl deploy`
Secrets are staged for the first deployment
==> Verifying app config
Validating /home/vorotamoroz/dev/obsidian-livesync/utils/flyio/fly.toml
Platform: machines
✓ Configuration is valid
--> Verified app config
==> Building image
Searching for image 'couchdb:latest' remotely...
image found: img_ox20prk63084j1zq
Watch your deployment at https://fly.io/apps/young-darkness-25342/monitoring
Provisioning ips for young-darkness-25342
Dedicated ipv6: 2a09:8280:1::37:fde9
Shared ipv4: 66.241.124.163
Add a dedicated ipv4 with: fly ips allocate-v4
Creating a 1 GB volume named 'couchdata' for process group 'app'. Use 'fly vol extend' to increase its size
This deployment will:
* create 1 "app" machine
No machines in group app, launching a new machine
WARNING The app is not listening on the expected address and will not be reachable by fly-proxy.
You can fix this by configuring your app to listen on the following addresses:
- 0.0.0.0:5984
Found these processes inside the machine with open listening sockets:
PROCESS | ADDRESSES
-----------------*---------------------------------------
/.fly/hallpass | [fdaa:0:73b9:a7b:22e:3851:7f28:2]:22
Finished launching new machines
NOTE: The machines for [app] have services with 'auto_stop_machines = true' that will be stopped when idling
-------
Checking DNS configuration for young-darkness-25342.fly.dev
Visit your newly deployed app at https://young-darkness-25342.fly.dev/
-- Configuring CouchDB by REST APIs... -->
curl: (35) OpenSSL SSL_connect: Connection reset by peer in connection to young-darkness-25342.fly.dev:443
{"ok":true}
""
""
""
""
""
""
""
""
""
<-- Configuring CouchDB by REST APIs Done!
OK!
Setup finished! Also, we can set up Self-hosted LiveSync instantly, by the following setup uri.
Passphrase of setup-uri is `welcome`.
--- configured ---
database : obsidiannotes
E2EE passphrase: dark-wildflower-26467
--- setup uri ---
obsidian://setuplivesync?settings=%5B%22gZkBwjFbLqxbdSIbJymU%2FmTPBPAKUiHVGDRKYiNnKhW0auQeBgJOfvnxexZtMCn8sNiIUTAlxNaMGF2t%2BCEhpJoeCP%2FO%2BrwfN5LaNDQyky1Uf7E%2B64A5UWyjOYvZDOgq4iCKSdBAXp9oO%2BwKh4MQjUZ78vIVvJp8Mo6NWHfm5fkiWoAoddki1xBMvi%2BmmN%2FhZatQGcslVb9oyYWpZocduTl0a5Dv%2FQviGwlYQ%2F4NY0dVDIoOdvaYS%2FX4GhNAnLzyJKMXhPEJHo9FvR%2FEOBuwyfMdftV1SQUZ8YDCuiR3T7fh7Kn1c6OFgaFMpFm%2BWgIJ%2FZpmAyhZFpEcjpd7ty%2BN9kfd9gQsZM4%2BYyU9OwDd2DahVMBWkqoV12QIJ8OlJScHHdcUfMW5ex%2F4UZTWKNEHJsigITXBrtq11qGk3rBfHys8O0vY6sz%2FaYNM3iAOsR1aoZGyvwZm4O6VwtzK8edg0T15TL4O%2B7UajQgtCGxgKNYxb8EMOGeskv7NifYhjCWcveeTYOJzBhnIDyRbYaWbkAXQgHPBxzJRkkG%2FpBPfBBoJarj7wgjMvhLJ9xtL4FbP6sBNlr8jtAUCoq4L7LJcRNF4hlgvjJpL2BpFZMzkRNtUBcsRYR5J%2BM1X2buWi2BHncbSiRRDKEwNOQkc%2FmhMJjbAn%2F8eNKRuIICOLD5OvxD7FZNCJ0R%2BWzgrzcNV%22%2C%22ec7edc900516b4fcedb4c7cc01000000%22%2C%22fceb5fe54f6619ee266ed9a887634e07%22%5D
```
All we have to do is copy the setup-URI (`obsidian`://...`) and open it from Self-hosted LiveSync on Obsidian.
If you did not install Deno, configurations will be printed again, instead of the setup-URI. In this case, we should configure it manually.
### delete-server.sh
The pair script of `deploy-server.sh`. We can delete the deployed server by this with fly.toml.
#### Materials
- Mandatory: flyctl, jq
- Recommended: none
#### Usage
```sh
./delete-server.sh
```
```
App 'young-darkness-25342 is going to be scaled according to this plan:
-1 machines for group 'app' on region 'nrt' of size 'shared-cpu-1x'
Executing scale plan
Destroyed e28667eec57158 group:app region:nrt size:shared-cpu-1x
Destroyed app young-darkness-25342
```