Compare commits

...

5 Commits

Author SHA1 Message Date
vorotamoroz
a96e4e4472 bump 2024-07-12 10:13:04 +01:00
vorotamoroz
dd26bbfe64 Improved:
- Overall performance has been improved by using PouchDB 9.0.0.
- Configuration mismatch detection is refined. We can resolve mismatches more smoothly and naturally.
Fixed:
- Customisation Sync will be disabled when a corrupted configuration is detected.
New feature:
- We can get a notification about the storage usage of the remote database.
2024-07-12 10:11:16 +01:00
vorotamoroz
6b9bd473cf bump 2024-07-10 05:24:26 +01:00
vorotamoroz
4be4fa6cc7 Maintenance:
- Library refining (Phase 1 - step 2). There are no significant changes on the user side.
2024-07-10 05:23:34 +01:00
vorotamoroz
a9745e850e Improved:
- The passphrase of the Setup URI is now automatically generated. (#426)
2024-07-01 11:05:33 +01:00
16 changed files with 2907 additions and 3007 deletions

View File

@@ -108,13 +108,18 @@ $ 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
Your passphrase of Setup-URI is: patient-haze
This passphrase is never shown again, so please note it in a safe place.
```
Please keep your passphrase of Setup-URI.
### 2. Setup Self-hosted LiveSync to Obsidian
[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.
3. Type the previously displayed passphrase (`patient-haze`) 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.

View File

@@ -20,6 +20,7 @@
- [On the mobile device, cannot synchronise on the local network!](#on-the-mobile-device-cannot-synchronise-on-the-local-network)
- [I think that something bad happening on the vault...](#i-think-that-something-bad-happening-on-the-vault)
- [Tips](#tips)
- [How to resolve `Tweaks Mismatched of Changed`](#how-to-resolve-tweaks-mismatched-of-changed)
- [Old tips](#old-tips)
<!-- - -->
@@ -111,8 +112,28 @@ Place `redflag.md` on top of the vault, and restart Obsidian. The most simple wa
If there is `redflag.md`, Self-hosted LiveSync suspends all database and storage processes.
## Tips
### How to resolve `Tweaks Mismatched of Changed`
(Since v0.23.17)
If you have changed some configurations or tweaks which should be unified between the devices, you will be asked how to reflect (or not) other devices at the next synchronisation. It also occurs on the device itself, where changes are made, to prevent unexpected configuration changes from unwanted propagation.
(We may thank this behaviour if we have synchronised or backed up and restored Self-hosted LiveSync. At least, for me so).
Following dialogue will be shown:
![Dialogue](tweak_mismatch_dialogue.png)
- If we want to propagate the setting of the device, we should choose `Update with mine`.
- On other devices, we should choose `Use configured` to accept and use the configured configuration.
- `Dismiss` can postpone a decision. However, we cannot synchronise until we have decided.
Rest assured that in most cases we can choose `Use configured`. (Unless you are certain that you have not changed the configuration).
If we see it for the first time, it reflects the settings of the device that has been synchronised with the remote for the first time since the upgrade. Probably, we can accept that.
<!-- Add here -->
### Old tips
- 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.23.15",
"version": "0.23.17",
"minAppVersion": "0.9.12",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz",

5353
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "0.23.14",
"version": "0.23.17",
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"main": "main.js",
"type": "module",
@@ -14,9 +14,9 @@
"author": "vorotamoroz",
"license": "MIT",
"devDependencies": {
"@tsconfig/svelte": "^5.0.2",
"@tsconfig/svelte": "^5.0.4",
"@types/diff-match-patch": "^1.0.36",
"@types/node": "^20.11.28",
"@types/node": "^20.14.10",
"@types/pouchdb": "^6.4.2",
"@types/pouchdb-adapter-http": "^6.1.6",
"@types/pouchdb-adapter-idb": "^6.1.7",
@@ -25,46 +25,46 @@
"@types/pouchdb-mapreduce": "^6.1.10",
"@types/pouchdb-replication": "^6.4.7",
"@types/transform-pouch": "^1.0.6",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"builtin-modules": "^3.3.0",
"esbuild": "0.20.2",
"esbuild-svelte": "^0.8.0",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"builtin-modules": "^4.0.0",
"esbuild": "0.23.0",
"esbuild-svelte": "^0.8.1",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.29.1",
"events": "^3.3.0",
"obsidian": "^1.5.7",
"postcss": "^8.4.35",
"postcss-load-config": "^5.0.3",
"pouchdb-adapter-http": "^8.0.1",
"pouchdb-adapter-idb": "^8.0.1",
"pouchdb-adapter-indexeddb": "^8.0.1",
"pouchdb-core": "^8.0.1",
"pouchdb-errors": "^8.0.1",
"pouchdb-find": "^8.0.1",
"pouchdb-mapreduce": "^8.0.1",
"pouchdb-merge": "^8.0.1",
"pouchdb-replication": "^8.0.1",
"pouchdb-utils": "^8.0.1",
"svelte": "^4.2.16",
"svelte-preprocess": "^5.1.3",
"terser": "^5.29.2",
"postcss": "^8.4.39",
"postcss-load-config": "^6.0.1",
"pouchdb-adapter-http": "^9.0.0",
"pouchdb-adapter-idb": "^9.0.0",
"pouchdb-adapter-indexeddb": "^9.0.0",
"pouchdb-core": "^9.0.0",
"pouchdb-errors": "^9.0.0",
"pouchdb-find": "^9.0.0",
"pouchdb-mapreduce": "^9.0.0",
"pouchdb-merge": "^9.0.0",
"pouchdb-replication": "^9.0.0",
"pouchdb-utils": "^9.0.0",
"svelte": "^4.2.18",
"svelte-preprocess": "^6.0.2",
"terser": "^5.31.2",
"transform-pouch": "^2.0.0",
"tslib": "^2.6.2",
"typescript": "^5.4.2"
"tslib": "^2.6.3",
"typescript": "^5.5.3"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.556.0",
"@smithy/fetch-http-handler": "^2.5.0",
"@smithy/protocol-http": "^3.3.0",
"@smithy/querystring-builder": "^2.2.0",
"@aws-sdk/client-s3": "^3.614.0",
"@smithy/fetch-http-handler": "^3.2.1",
"@smithy/protocol-http": "^4.0.3",
"@smithy/querystring-builder": "^3.0.3",
"diff-match-patch": "^1.0.5",
"esbuild-plugin-inline-worker": "^0.1.1",
"fflate": "^0.8.2",
"idb": "^8.0.0",
"minimatch": "^9.0.3",
"octagonal-wheels": "^0.1.10",
"minimatch": "^10.0.1",
"octagonal-wheels": "^0.1.13",
"xxhash-wasm": "0.4.2",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
}

View File

@@ -1,27 +1,10 @@
{
"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"
"colab_type": "text",
"id": "view-in-github"
},
"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>"
@@ -29,12 +12,12 @@
},
{
"cell_type": "markdown",
"source": [
"- Initial version 7th Feb. 2024"
],
"metadata": {
"id": "AzLlAcLFRO5A"
}
},
"source": [
"- Initial version 7th Feb. 2024"
]
},
{
"cell_type": "code",
@@ -55,27 +38,32 @@
},
{
"cell_type": "code",
"source": [
"# Login up sign up\n",
"!flyctl auth signup"
],
"execution_count": null,
"metadata": {
"id": "mGN08BaFDviy"
},
"execution_count": null,
"outputs": []
"outputs": [],
"source": [
"# Login up sign up\n",
"!flyctl auth signup"
]
},
{
"cell_type": "markdown",
"source": [
"Select a region and execute the block."
],
"metadata": {
"id": "BBFTFOP6vA8m"
}
},
"source": [
"Select a region and execute the block."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "TNl0A603EF9E"
},
"outputs": [],
"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",
@@ -98,31 +86,29 @@
" 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",
" 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 displayed one. <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",
"metadata": {
"id": "oeIzExnEKhFp"
},
"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",
"metadata": {
"id": "sdQrqOjERN3K"
},
"source": [
"\n",
"\n",
@@ -131,21 +117,35 @@
"\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"
],
"execution_count": null,
"metadata": {
"id": "7JMSkNvVIIfg"
},
"execution_count": null,
"outputs": []
"outputs": [],
"source": [
"!./delete-server.sh"
]
}
]
}
],
"metadata": {
"colab": {
"authorship_tag": "ABX9TyMexQ5pErH5LBG2tENtEVWf",
"include_colab_link": true,
"private_outputs": true,
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}

Submodule src/lib updated: 41f1a0c737...f05b631841

View File

@@ -4,7 +4,7 @@ import { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch, stri
import { Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, type RequestUrlParam, type RequestUrlResponse, requestUrl, type MarkdownFileInfo } from "./deps";
import { type EntryDoc, type LoadedEntry, type ObsidianLiveSyncSettings, type diff_check_result, type diff_result_leaf, type EntryBody, type LOG_LEVEL, VER, DEFAULT_SETTINGS, type diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, SALT_OF_PASSPHRASE, type ConfigPassphraseStore, type CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE, type DatabaseConnectingStatus, type EntryHasPath, type DocumentID, type FilePathWithPrefix, type FilePath, type AnyEntry, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT, LOG_LEVEL_VERBOSE, type SavingEntry, MISSING_OR_ERROR, NOT_CONFLICTED, AUTO_MERGED, CANCELLED, LEAVE_TO_SUBSEQUENT, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3_HR, REMOTE_MINIO, REMOTE_COUCHDB, type BucketSyncSetting, TweakValuesShouldMatchedTemplate, confName, type TweakValues, } from "./lib/src/common/types.ts";
import { type InternalFileInfo, type CacheData, type FileEventItem } from "./common/types.ts";
import { arrayToChunkedArray, createBlob, determineTypeFromBlob, escapeMarkdownValue, extractObject, fireAndForget, getDocData, isAnyNote, isDocContentSame, isObjectDifferent, readContent, sendValue, throttle, type SimpleStore } from "./lib/src/common/utils.ts";
import { arrayToChunkedArray, createBlob, delay, determineTypeFromBlob, escapeMarkdownValue, extractObject, fireAndForget, getDocData, isAnyNote, isDocContentSame, isObjectDifferent, readContent, sendValue, sizeToHumanReadable, throttle, type SimpleStore } from "./lib/src/common/utils.ts";
import { Logger, setGlobalLogFunction } from "./lib/src/common/logger.ts";
import { PouchDB } from "./lib/src/pouchdb/pouchdb-browser.js";
import { ConflictResolveModal } from "./ui/ConflictResolveModal.ts";
@@ -640,6 +640,82 @@ Click anywhere to stop counting down.
async scanStat() {
const notes: { path: string, mtime: number }[] = [];
Logger(`Additional safety scan..`, LOG_LEVEL_VERBOSE);
Logger(`Checking storage sizes`, LOG_LEVEL_VERBOSE);
if (this.settings.notifyThresholdOfRemoteStorageSize < 0) {
const message = `Now, Self-hosted LiveSync is able to check the remote storage size on the start-up.
You can configure the threshold size for your remote storage. This will be different for your server.
Please choose the threshold size as you like.
- 0: Do not warn about storage size.
This is recommended if you have enough space on the remote storage especially you have self-hosted. And you can check the storage size and rebuild manually.
- 800: Warn if the remote storage size exceeds 800MB.
This is recommended if you are using fly.io with 1GB limit or IBM Cloudant.
- 2000: Warn if the remote storage size exceeds 2GB.
And if your actual storage size exceeds the threshold after the setup, you may warned again. But do not worry, you can enlarge the threshold (or rebuild everything to reduce the size).
`
const ANSWER_0 = "Do not warn";
const ANSWER_800 = "800MB";
const ANSWER_2000 = "2GB";
const ret = await confirmWithMessage(this, "Remote storage size threshold", message, [ANSWER_0, ANSWER_800, ANSWER_2000], ANSWER_800, 40);
if (ret == ANSWER_0) {
this.settings.notifyThresholdOfRemoteStorageSize = 0;
} else if (ret == ANSWER_800) {
this.settings.notifyThresholdOfRemoteStorageSize = 800;
} else {
this.settings.notifyThresholdOfRemoteStorageSize = 2000;
}
}
if (this.settings.notifyThresholdOfRemoteStorageSize > 0) {
const remoteStat = await this.replicator?.getRemoteStatus(this.settings);
if (remoteStat) {
const estimatedSize = remoteStat.estimatedSize;
if (estimatedSize) {
const maxSize = this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024;
if (estimatedSize > maxSize) {
const message = `Remote storage size: ${sizeToHumanReadable(estimatedSize)}. It exceeds the configured value ${sizeToHumanReadable(maxSize)}.
This may cause the storage to be full. You should enlarge the remote storage, or rebuild everything to reduce the size. \n
**Note:** If you are new to Self-hosted LiveSync, you should enlarge the threshold. \n
Self-hosted LiveSync will not release the storage automatically even if the file is deleted. This is why they need regular maintenance.\n
If you have enough space on the remote storage, you can enlarge the threshold. Otherwise, you should rebuild everything.\n
However, **Please make sure that all devices have been synchronised**. \n
\n`;
const ANSWER_ENLARGE_LIMIT = "Enlarge the limit";
const ANSWER_REBUILD = "Rebuild now";
const ANSWER_IGNORE = "Dismiss";
const ret = await confirmWithMessage(this, "Remote storage size exceeded", message, [ANSWER_ENLARGE_LIMIT, ANSWER_REBUILD, ANSWER_IGNORE,], ANSWER_IGNORE, 20);
if (ret == ANSWER_REBUILD) {
const ret = await this.askYesNo("This may take a bit of a long time. Do you really want to rebuild everything now?");
if (ret == "yes") {
Logger(`Receiving all from the server before rebuilding`, LOG_LEVEL_NOTICE);
await this.replicateAllFromServer(true);
await delay(3000);
Logger(`Obsidian will be reloaded to rebuild everything.`, LOG_LEVEL_NOTICE);
await this.vaultAccess.vaultCreate(FLAGMD_REDFLAG2_HR, "");
this.performAppReload();
}
} else if (ret == ANSWER_ENLARGE_LIMIT) {
this.settings.notifyThresholdOfRemoteStorageSize = ~~(estimatedSize / 1024 / 1024) + 100;
Logger(`Threshold has been enlarged to ${this.settings.notifyThresholdOfRemoteStorageSize}MB`, LOG_LEVEL_NOTICE);
await this.saveSettings();
} else {
// Dismiss or Close the dialog
}
Logger(`Remote storage size: ${sizeToHumanReadable(estimatedSize)} exceeded ${sizeToHumanReadable(this.settings.notifyThresholdOfRemoteStorageSize)} `, LOG_LEVEL_INFO);
} else {
Logger(`Remote storage size: ${sizeToHumanReadable(estimatedSize)}`, LOG_LEVEL_INFO);
}
}
}
}
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
if (!("_conflicts" in doc)) continue;
notes.push({ path: this.getPath(doc), mtime: doc.mtime });
@@ -1146,6 +1222,12 @@ Note: We can always able to read V1 format. It will be progressively converted.
this.settings.customChunkSize = 0;
}
this.deviceAndVaultName = localStorage.getItem(lsKey) || "";
if (this.deviceAndVaultName == "") {
if (this.settings.usePluginSync) {
Logger("Device name is not set. Plug-in sync has been disabled.", LOG_LEVEL_NOTICE);
this.settings.usePluginSync = false;
}
}
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
this.settingTab.requestReload()
}
@@ -2095,54 +2177,62 @@ We can perform a command in this file.
if (!this.replicator.tweakSettingsMismatched) {
return "OK";
}
const remoteSettings = this.replicator.mismatchedTweakValues;
const mustSettings = remoteSettings.map(e => extractObject(TweakValuesShouldMatchedTemplate, e));
const preferred = extractObject(TweakValuesShouldMatchedTemplate, this.replicator.preferredTweakValue!);
const mine = extractObject(TweakValuesShouldMatchedTemplate, this.settings);
const items = Object.entries(TweakValuesShouldMatchedTemplate);
// Making tables:
let table = `| Value name | Ours | ${mustSettings.map((_, i) => `Remote ${i + 1} |`).join("")}\n` +
`|: --- |: --- :${`|: --- :`.repeat(mustSettings.length)}|\n`
let table = `| Value name | This device | Configured | \n` +
`|: --- |: --- :|: ---- :| \n`;
// const items = [mine,preferred]
for (const v of items) {
const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate;
const value = mustSettings.map(e => e[key]);
table += `| ${confName(key)} | ${escapeMarkdownValue(this.settings[key])} | ${value.map((v) => `${escapeMarkdownValue(v)} |`).join("")}\n`;
const valueMine = escapeMarkdownValue(mine[key]);
const valuePreferred = escapeMarkdownValue(preferred[key]);
if (valueMine == valuePreferred) continue;
table += `| ${confName(key)} | ${valueMine} | ${valuePreferred} | \n`;
}
const message = `
Configuration mismatching between the clients has been detected.
This can be harmful or extra capacity consumption. We have to make these value unified.
Your configuration has not been matched with the one on the remote server.
(Which you had decided once before, or set by initially synchronised device).
Configured values:
${table}
Please select a unification method.
Please select which one you want to use.
However, even if we answer that you will \`Use mine\`, we will be prompted to accept it again on the other device and have to decide accept or not.`;
- Use configured: Update settings of this device by configured one on the remote server.
You should select this if you have changed the settings on **another device**.
- Update with mine: Update settings on the remote server by the settings of this device.
You should select this if you have changed the settings on **this device**.
- Dismiss: Ignore this message and keep the current settings.
You cannot synchronise until you resolve this issue without enabling \`Do not check configuration mismatch before replication\`.`;
//TODO: apply this settings.
const CHOICE_USE_REMOTE = "Use Remote ";
const CHOICE_USR_MINE = "Use ours";
const CHOICE_USE_REMOTE = "Use configured";
const CHOICE_USR_MINE = "Update with mine";
const CHOICE_DISMISS = "Dismiss";
// const ourConfig = extractObject(TweakValuesShouldMatchedTemplate, this.settings);
const CHOICE_AND_VALUES = [
...mustSettings.map((e, i) => [`${CHOICE_USE_REMOTE} ${i + 1}`, e]),
[CHOICE_USE_REMOTE, preferred],
[CHOICE_USR_MINE, true],
[CHOICE_DISMISS, false]
]
const CHOICES = Object.fromEntries(CHOICE_AND_VALUES) as Record<string, TweakValues | boolean>;
const retKey = await confirmWithMessage(this, "Locked", message, Object.keys(CHOICES), CHOICE_DISMISS, 60);
const retKey = await confirmWithMessage(this, "Tweaks Mismatched or Changed", message, Object.keys(CHOICES), CHOICE_DISMISS, 60);
if (!retKey) return "IGNORE";
const conf = CHOICES[retKey];
if (conf === true) {
await this.replicator.resetRemoteTweakSettings(this.settings);
Logger(`Tweak values on the remote server have been cleared, and will be overwritten in next synchronisation.`, LOG_LEVEL_NOTICE);
return "OK";
await this.replicator.setPreferredRemoteTweakSettings(this.settings);
Logger(`Tweak values on the remote server have been updated. Your other device will see this message.`, LOG_LEVEL_NOTICE);
return "CHECKAGAIN";
}
if (conf) {
this.settings = { ...this.settings, ...conf };
await this.replicator.setPreferredRemoteTweakSettings(this.settings);
await this.saveSettingData();
Logger(`Tweak Values have been overwritten by the chosen one.`, LOG_LEVEL_NOTICE);
Logger(`Configuration has been updated as configured by the other device.`, LOG_LEVEL_NOTICE);
return "CHECKAGAIN";
}
return "IGNORE";

View File

@@ -21,7 +21,7 @@ import {
statusDisplay,
type ConfigurationItem
} from "../lib/src/common/types.ts";
import { createBlob, delay, isDocContentSame, isObjectDifferent, readAsBlob, unique } from "../lib/src/common/utils.ts";
import { createBlob, delay, isDocContentSame, isObjectDifferent, readAsBlob, sizeToHumanReadable, unique } from "../lib/src/common/utils.ts";
import { versionNumberString2Number } from "../lib/src/string_and_binary/convert.ts";
import { Logger } from "../lib/src/common/logger.ts";
import { checkSyncInfo, isCloudantURI } from "../lib/src/pouchdb/utils_couchdb.ts";
@@ -586,6 +586,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
const trialSetting = { ...this.editingSettings, ...settingOverride };
const replicator = this.plugin.getNewReplicator(trialSetting);
await replicator.tryConnectRemote(trialSetting);
const status = await replicator.getRemoteStatus(trialSetting);
if (status) {
if (status.estimatedSize) {
Logger(`Estimated size: ${sizeToHumanReadable(status.estimatedSize)}`, LOG_LEVEL_NOTICE);
}
}
}
closeSetting() {
@@ -1188,6 +1194,10 @@ However, your report is needed to stabilise this. I appreciate you for your grea
.addOnUpdate(onlyOnCouchDB)
}, onlyOnCouchDB);
this.createEl(containerRemoteDatabaseEl, "h4", { text: "Notification" }).addClass("wizardHidden")
new Setting(containerRemoteDatabaseEl).autoWireNumeric("notifyThresholdOfRemoteStorageSize", {}).setClass("wizardHidden");
this.createEl(containerRemoteDatabaseEl, "h4", { text: "Effective Storage Using" }).addClass("wizardHidden")
new Setting(containerRemoteDatabaseEl).autoWireToggle("useEden").setClass("wizardHidden");

View File

@@ -320,6 +320,10 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
batchSaveMaximumDelay: {
name: "Maximum delay for batch database updating",
desc: "Saving will be performed forcefully after this number of seconds."
},
"notifyThresholdOfRemoteStorageSize": {
name: "Notify when the estimated remote storage size exceeds on start up",
desc: "MB (0 to disable)."
}
}
function translateInfo(infoSrc: ConfigurationItem | undefined | false) {

View File

@@ -18,6 +18,24 @@ I have a lot of respect for that plugin, even though it is sometimes treated as
Hooray for open source, and generous licences, and the sharing of knowledge by experts.
#### Version history
- 0.23.17:
- Improved:
- Overall performance has been improved by using PouchDB 9.0.0.
- Configuration mismatch detection is refined. We can resolve mismatches more smoothly and naturally.
More detail is on `troubleshooting.md` on the repository.
- Fixed:
- Customisation Sync will be disabled when a corrupted configuration is detected.
Therefore, the Device Name can be changed even in the event of a configuration mismatch.
- New feature:
- We can get a notification about the storage usage of the remote database.
- Default: We will be asked.
- If the remote storage usage approaches the configured value, we will be asked whether we want to Rebuild or increase the limit.
- 0.23.16:
- Maintenance Update:
- Library refining (Phase 1 - step 2). There are no significant changes on the user side.
- Including the following fixes of potentially problems:
- the problem which the path had been obfuscating twice has been resolved.
- Note: Potential problems of the library; which has not happened in Self-hosted LiveSync for some reasons.
- 0.23.15:
- Maintenance Update:
- Library refining (Phase 1). There are no significant changes on the user side.
@@ -31,20 +49,6 @@ Hooray for open source, and generous licences, and the sharing of knowledge by e
- Default: 5 seconds, the same as the previous hard-coded value. (Note: also, the previous behaviour was not correct).
- Also, we can configure the limit of delaying batch-saving.
- The performance of showing status indicators has been improved.
- 0.23.13:
- Fixed:
- No longer files have been trimmed even delimiters have been continuous.
- Fixed the toggle title to `Do not split chunks in the background` from `Do not split chunks in the foreground`.
- Non-configured item mismatches are no longer detected.
- 0.23.12:
- Improved:
- Now notes will be split into chunks in the background thread to improve smoothness.
- Default enabled, to disable, toggle `Do not split chunks in the foreground` on `Hatch` -> `Compatibility`.
- If you want to process very small notes in the foreground, please enable `Process small files in the foreground` on `Hatch` -> `Compatibility`.
- We can use a `splitting-limit-capped chunk splitter`; which performs more simple and make less amount of chunks.
- Default disabled, to enable, toggle `Use splitting-limit-capped chunk splitter` on `Sync settings` -> `Performance tweaks`
- Tidied
- Some files have been separated into multiple files to make them more explicit in what they are responsible for.
Older notes is in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md).

View File

@@ -18,6 +18,20 @@ I have a lot of respect for that plugin, even though it is sometimes treated as
Hooray for open source, and generous licences, and the sharing of knowledge by experts.
#### Version history
- 0.23.13:
- Fixed:
- No longer files have been trimmed even delimiters have been continuous.
- Fixed the toggle title to `Do not split chunks in the background` from `Do not split chunks in the foreground`.
- Non-configured item mismatches are no longer detected.
- 0.23.12:
- Improved:
- Now notes will be split into chunks in the background thread to improve smoothness.
- Default enabled, to disable, toggle `Do not split chunks in the foreground` on `Hatch` -> `Compatibility`.
- If you want to process very small notes in the foreground, please enable `Process small files in the foreground` on `Hatch` -> `Compatibility`.
- We can use a `splitting-limit-capped chunk splitter`; which performs more simple and make less amount of chunks.
- Default disabled, to enable, toggle `Use splitting-limit-capped chunk splitter` on `Sync settings` -> `Performance tweaks`
- Tidied
- Some files have been separated into multiple files to make them more explicit in what they are responsible for.
- 0.23.11:
- Fixed:
- Now we *surely* can set the device name and enable customised synchronisation.

View File

@@ -26,7 +26,7 @@ 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 "Passphrase of setup-uri will be printed only one time. Keep it safe!"
echo "--- configured ---"
echo "database : ${database}"
echo "E2EE passphrase: ${passphrase}"

View File

@@ -1,153 +1,13 @@
import { webcrypto } from "node:crypto";
import { encrypt } from "npm:octagonal-wheels@0.1.11/encryption/encryption.js";
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];
const noun = ["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"];
const 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"];
function friendlyString() {
return `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${noun[Math.floor(Math.random() * noun.length)]}`;
}
function getSemiStaticField(reset?: boolean) {
// return fixed field of iv.
if (semiStaticFieldBuffer != null && !reset) {
return semiStaticFieldBuffer;
}
semiStaticFieldBuffer = webcrypto.getRandomValues(new Uint8Array(12));
return semiStaticFieldBuffer;
}
const uri_passphrase = `${Deno.env.get("uri_passphrase") ?? friendlyString()}`;
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() {
@@ -173,8 +33,10 @@ async function main() {
"concurrencyOfReadChunksOnline": 100,
"minimumIntervalOfReadChunksOnline": 100,
}
const encryptedConf = encodeURIComponent(await encrypt(JSON.stringify(conf), "welcome", false));
const encryptedConf = encodeURIComponent(await encrypt(JSON.stringify(conf), uri_passphrase, false));
const theURI = `${URIBASE}${encryptedConf}`;
console.log(theURI);
console.log("\nYour passphrase of Setup-URI is: ", uri_passphrase);
console.log("This passphrase is never shown again, so please note it in a safe place.")
}
await main();

View File

@@ -129,12 +129,15 @@ curl: (35) OpenSSL SSL_connect: Connection reset by peer in connection to young-
<-- 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`.
Passphrase of setup-uri will be printed only one time. Keep it safe!
--- 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
Your passphrase of Setup-URI is: patient-haze
This passphrase is never shown again, so please note it in a safe place.
```
All we have to do is copy the setup-URI (`obsidian`://...`) and open it from Self-hosted LiveSync on Obsidian.