mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-03-10 11:58:48 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a623b987c8 | ||
|
|
db28b9ec11 | ||
|
|
33c01fdf1e | ||
|
|
536c0426d6 | ||
|
|
2f848878c2 | ||
|
|
c4f2baef5e | ||
|
|
a5b88a8d47 |
81
docs/tips/jwt-on-couchdb.md
Normal file
81
docs/tips/jwt-on-couchdb.md
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: "JWT Authentication on CouchDB"
|
||||
livesync-version: 0.25.24
|
||||
tags:
|
||||
- tips
|
||||
- CouchDB
|
||||
- JWT
|
||||
authors:
|
||||
- vorotamoroz
|
||||
---
|
||||
|
||||
# JWT Authentication on CouchDB
|
||||
|
||||
When using CouchDB as a backend for Self-hosted LiveSync, it is possible to enhance security by employing JWT (JSON Web Token) Authentication. In particular, using asymmetric keys (ES256 and ES512) provides greater security against token interception.
|
||||
|
||||
## Setting up JWT Authentication (Asymmetrical Key Example)
|
||||
|
||||
### 1. Generate a key pair
|
||||
|
||||
We can use `openssl` to generate an EC key pair as follows:
|
||||
|
||||
```bash
|
||||
# Generate private key
|
||||
# ES512 for secp521r1 curve, we can also use ES256 for prime256v1 curve
|
||||
openssl ecparam -name secp521r1 -genkey -noout | openssl pkcs8 -topk8 -inform PEM -nocrypt -out private_key.pem
|
||||
# openssl ecparam -name prime256v1 -genkey -noout | openssl pkcs8 -topk8 -inform PEM -nocrypt -out private_key.pem
|
||||
# Generate public key in SPKI format
|
||||
openssl ec -in private_key.pem -pubout -outform PEM -out public_key.pem
|
||||
```
|
||||
|
||||
> [!More tip]
|
||||
> A key generator will be provided again in a future version of the user interface.
|
||||
|
||||
### 2. Configure CouchDB to accept JWT tokens
|
||||
|
||||
The following configuration is required:
|
||||
|
||||
| Key | Value | Note |
|
||||
| ------------------------------ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| chttpd/authentication_handlers | {chttpd_auth, jwt_authentication_handler} | In total, it may be `{chttpd_auth, jwt_authentication_handler}, {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler}`, or something similar. |
|
||||
| jwt_auth/required_claims | "exp" | |
|
||||
| jwt_keys/ec:your_key_id | Your public key in PEM (SPKI) format | Replace `your_key_id` with your actual key ID. You can decide as you like. Note that you can add multiple keys if needed. If you want to use HSxxx, you should set `jwt_keys/hmac:your_key_id` with your HMAC secret. |
|
||||
|
||||
|
||||
Note: When configuring CouchDB via web interface (Fauxton), new-lines on the public key should be replaced with `\n` for header and footer lines (So wired, but true I have tested). as follows:
|
||||
```
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
\nMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBq0irb/+K0Qzo7ayIHj0Xtthcntjz
|
||||
r665J5UYdEQMiTtku5rnp95RuN97uA2pPOJOacMBAoiVUnZ1pqEBz9xH9yoAixji
|
||||
Ju...........................................................gTt
|
||||
/xtqrJRwrEy986oRZRQ=
|
||||
\n-----END PUBLIC KEY-----
|
||||
```
|
||||
|
||||
For detailed information, please refer to the [CouchDB JWT Authentication Documentation](https://docs.couchdb.org/en/stable/api/server/authn.html#jwt-authentication).
|
||||
|
||||
### 3. Configure Self-hosted LiveSync to use JWT Authentication
|
||||
|
||||
| Setting | Description |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Use JWT Authentication | Enable this option to use JWT Authentication. |
|
||||
| JWT Algorithm | Select the JWT signing algorithm (e.g., ES256, ES512) that matches your key pair. |
|
||||
| JWT Key | Paste your private key in PEM (pkcs8) format. |
|
||||
| JWT Expiration Duration | Set the token expiration time in minutes. Locally cached tokens are also invalidated after this duration. |
|
||||
| JWT Key ID (kid) | Enter the key ID that you used when configuring CouchDB, i.e., the one that replaced `your_key_id`. |
|
||||
| JWT Subject (sub) | Set your user ID; this overrides the original `Username` setting. If you have detected access with `Username`, you have failed to authorise with JWT. |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Self-hosted LiveSync requests to CouchDB treat the user as `_admin`. If you want to restrict access, configure `jwt_auth/roles_claim_name` to a custom claim name. (Self-hosted LiveSync always sets `_couchdb.roles` with the value `["_admin"]`).
|
||||
|
||||
### 4. Test the configuration
|
||||
|
||||
Just try to `Test Settings and Continue` in the remote setup dialogue. If you have successfully authenticated, you are all set.
|
||||
|
||||
## Additional Notes
|
||||
|
||||
This feature is still experimental. Please ensure to test thoroughly in your environment before deploying to production.
|
||||
|
||||
However, we think that this is a great step towards enhancing security when using CouchDB with Self-hosted LiveSync. We shall enable this setting by default in future releases.
|
||||
|
||||
We would love to hear your feedback and any issues you encounter.
|
||||
29
docs/tips/p2p-sync-tips.md
Normal file
29
docs/tips/p2p-sync-tips.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: "Peer-to-Peer Synchronisation Tips"
|
||||
livesync-version: 0.25.24
|
||||
tags:
|
||||
- tips
|
||||
- p2p
|
||||
authors:
|
||||
- vorotamoroz
|
||||
---
|
||||
|
||||
# Peer-to-Peer Synchronisation Tips
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Peer-to-peer synchronisation is still an experimental feature. Although we have made every effort to ensure its reliability, it may not function correctly in all environments.
|
||||
|
||||
## Difficulties with Peer-to-Peer Synchronisation
|
||||
|
||||
It is often the case that peer-to-peer connections do not function correctly, for instance, when using mobile data services.
|
||||
In such circumstances, we recommend connecting all devices to a single Virtual Private Network (VPN). It is advisable to select a service, such as Tailscale, which facilitates direct communication between peers wherever possible.
|
||||
Should one be in an environment where even Tailscale is unable to connect, or where it cannot be lawfully installed, please continue reading.
|
||||
|
||||
## A More Detailed Explanation
|
||||
|
||||
The failure of a Peer-to-Peer connection via WebRTC can be attributed to several factors. These may include an unsuccessful UDP hole-punching attempt, or an intermediary gateway intentionally terminating the connection. Troubleshooting this matter is not a simple undertaking. Furthermore, and rather unfortunately, gateway administrators are typically aware of this type of network behaviour. Whilst a legitimate purpose for such traffic can be cited, such as for web conferencing, this is often insufficient to prevent it from being blocked.
|
||||
|
||||
This situation, however, is the primary reason that our project does not provide a TURN server. Although it is said that a TURN server within WebRTC does not decrypt communications, the project holds the view that the risk of a malicious party impersonating a TURN server must be avoided. Consequently, configuring a TURN server for relay communication is not currently possible through the user interface. Furthermore, there is no official project TURN server, which is to say, one that could be monitored by a third party.
|
||||
|
||||
We request that you provide your own server, using your own Fully Qualified Domain Name (FQDN), and subsequently enter its details into the advanced settings.
|
||||
For testing purposes, Cloudflare's Real-Time TURN Service is exceedingly convenient and offers a generous amount of free data. However, it must be noted that because it is a well-known destination, such traffic is highly conspicuous. There is also a significant possibility that it may be blocked by default. We advise proceeding with caution.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.25.24",
|
||||
"version": "0.25.26",
|
||||
"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",
|
||||
|
||||
32
package-lock.json
generated
32
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.24",
|
||||
"version": "0.25.26",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.24",
|
||||
"version": "0.25.26",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.808.0",
|
||||
@@ -19,8 +19,8 @@
|
||||
"fflate": "^0.8.2",
|
||||
"idb": "^8.0.3",
|
||||
"minimatch": "^10.0.2",
|
||||
"octagonal-wheels": "^0.1.42",
|
||||
"qrcode-generator": "^1.4.4",
|
||||
"octagonal-wheels": "^0.1.44",
|
||||
"qrcode-generator": "^2.0.4",
|
||||
"trystero": "^0.22.0",
|
||||
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||
},
|
||||
@@ -9159,9 +9159,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/octagonal-wheels": {
|
||||
"version": "0.1.42",
|
||||
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.42.tgz",
|
||||
"integrity": "sha512-Hc2GWCtmG4+OzY9flY5vHjozUPuwsQoY7osG+I2QzACs8iTWrlAcw1re8FgU4vDC/to9rFogWfYWI8bNbr5j2w==",
|
||||
"version": "0.1.44",
|
||||
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.44.tgz",
|
||||
"integrity": "sha512-sUn/dkYQ2AbMB0R8CubVd75BjkcsteW9B14ArO99F6wM5JRwOo/yPIBBoxCUFE7JjBFOfuWG21C9E3NTga6XrA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"idb": "^8.0.3"
|
||||
@@ -10019,9 +10019,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode-generator": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz",
|
||||
"integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-2.0.4.tgz",
|
||||
"integrity": "sha512-mZSiP6RnbHl4xL2Ap5HfkjLnmxfKcPWpWe/c+5XxCuetEenqmNFf1FH/ftXPCtFG5/TDobjsjz6sSNL0Sr8Z9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/querystringify": {
|
||||
@@ -18353,9 +18353,9 @@
|
||||
}
|
||||
},
|
||||
"octagonal-wheels": {
|
||||
"version": "0.1.42",
|
||||
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.42.tgz",
|
||||
"integrity": "sha512-Hc2GWCtmG4+OzY9flY5vHjozUPuwsQoY7osG+I2QzACs8iTWrlAcw1re8FgU4vDC/to9rFogWfYWI8bNbr5j2w==",
|
||||
"version": "0.1.44",
|
||||
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.44.tgz",
|
||||
"integrity": "sha512-sUn/dkYQ2AbMB0R8CubVd75BjkcsteW9B14ArO99F6wM5JRwOo/yPIBBoxCUFE7JjBFOfuWG21C9E3NTga6XrA==",
|
||||
"requires": {
|
||||
"idb": "^8.0.3"
|
||||
}
|
||||
@@ -18967,9 +18967,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"qrcode-generator": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz",
|
||||
"integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw=="
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-2.0.4.tgz",
|
||||
"integrity": "sha512-mZSiP6RnbHl4xL2Ap5HfkjLnmxfKcPWpWe/c+5XxCuetEenqmNFf1FH/ftXPCtFG5/TDobjsjz6sSNL0Sr8Z9g=="
|
||||
},
|
||||
"querystringify": {
|
||||
"version": "2.2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.24",
|
||||
"version": "0.25.26",
|
||||
"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",
|
||||
@@ -94,8 +94,8 @@
|
||||
"fflate": "^0.8.2",
|
||||
"idb": "^8.0.3",
|
||||
"minimatch": "^10.0.2",
|
||||
"octagonal-wheels": "^0.1.42",
|
||||
"qrcode-generator": "^1.4.4",
|
||||
"octagonal-wheels": "^0.1.44",
|
||||
"qrcode-generator": "^2.0.4",
|
||||
"trystero": "^0.22.0",
|
||||
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export const EVENT_REQUEST_CLOSE_P2P = "request-close-p2p";
|
||||
|
||||
export const EVENT_REQUEST_RUN_DOCTOR = "request-run-doctor";
|
||||
export const EVENT_REQUEST_RUN_FIX_INCOMPLETE = "request-run-fix-incomplete";
|
||||
export const EVENT_ON_UNRESOLVED_ERROR = "on-unresolved-error";
|
||||
|
||||
// export const EVENT_FILE_CHANGED = "file-changed";
|
||||
|
||||
@@ -40,6 +41,7 @@ declare global {
|
||||
[EVENT_REQUEST_SHOW_SETUP_QR]: undefined;
|
||||
[EVENT_REQUEST_RUN_DOCTOR]: string;
|
||||
[EVENT_REQUEST_RUN_FIX_INCOMPLETE]: undefined;
|
||||
[EVENT_ON_UNRESOLVED_ERROR]: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,5 +65,5 @@ export const ICHeaderLength = ICHeader.length;
|
||||
export const ICXHeader = "ix:";
|
||||
|
||||
export const FileWatchEventQueueMax = 10;
|
||||
export const configURIBase = "obsidian://setuplivesync?settings=";
|
||||
export const configURIBaseQR = "obsidian://setuplivesync?settingsQR=";
|
||||
|
||||
export { configURIBase, configURIBaseQR } from "../lib/src/common/types.ts";
|
||||
|
||||
@@ -566,119 +566,3 @@ export function updatePreviousExecutionTime(key: string, timeDelta: number = 0)
|
||||
}
|
||||
waitingTasks[key].leastNext = Math.max(Date.now() + timeDelta, waitingTasks[key].leastNext);
|
||||
}
|
||||
|
||||
const prefixMapObject = {
|
||||
s: {
|
||||
1: "V",
|
||||
2: "W",
|
||||
3: "X",
|
||||
4: "Y",
|
||||
5: "Z",
|
||||
},
|
||||
o: {
|
||||
1: "v",
|
||||
2: "w",
|
||||
3: "x",
|
||||
4: "y",
|
||||
5: "z",
|
||||
},
|
||||
} as Record<string, Record<number, string>>;
|
||||
|
||||
const decodePrefixMapObject = Object.fromEntries(
|
||||
Object.entries(prefixMapObject).flatMap(([prefix, map]) =>
|
||||
Object.entries(map).map(([len, char]) => [char, { prefix, len: parseInt(len) }])
|
||||
)
|
||||
);
|
||||
|
||||
const prefixMapNumber = {
|
||||
n: {
|
||||
1: "a",
|
||||
2: "b",
|
||||
3: "c",
|
||||
4: "d",
|
||||
5: "e",
|
||||
},
|
||||
N: {
|
||||
1: "A",
|
||||
2: "B",
|
||||
3: "C",
|
||||
4: "D",
|
||||
5: "E",
|
||||
},
|
||||
} as Record<string, Record<number, string>>;
|
||||
|
||||
const decodePrefixMapNumber = Object.fromEntries(
|
||||
Object.entries(prefixMapNumber).flatMap(([prefix, map]) =>
|
||||
Object.entries(map).map(([len, char]) => [char, { prefix, len: parseInt(len) }])
|
||||
)
|
||||
);
|
||||
export function encodeAnyArray(obj: any[]): string {
|
||||
const tempArray = obj.map((v) => {
|
||||
if (v === null) return "n";
|
||||
if (v === false) return "f";
|
||||
if (v === true) return "t";
|
||||
if (v === undefined) return "u";
|
||||
if (typeof v == "number") {
|
||||
const b36 = v.toString(36);
|
||||
const strNum = v.toString();
|
||||
const expression = b36.length < strNum.length ? "N" : "n";
|
||||
const encodedStr = expression == "N" ? b36 : strNum;
|
||||
const len = encodedStr.length.toString(36);
|
||||
const lenLen = len.length;
|
||||
|
||||
const prefix2 = prefixMapNumber[expression][lenLen];
|
||||
return prefix2 + len + encodedStr;
|
||||
}
|
||||
const str = typeof v == "string" ? v : JSON.stringify(v);
|
||||
const prefix = typeof v == "string" ? "s" : "o";
|
||||
const length = str.length.toString(36);
|
||||
const lenLen = length.length;
|
||||
|
||||
const prefix2 = prefixMapObject[prefix][lenLen];
|
||||
return prefix2 + length + str;
|
||||
});
|
||||
const w = tempArray.join("");
|
||||
return w;
|
||||
}
|
||||
|
||||
const decodeMapConstant = {
|
||||
u: undefined,
|
||||
n: null,
|
||||
f: false,
|
||||
t: true,
|
||||
} as Record<string, any>;
|
||||
export function decodeAnyArray(str: string): any[] {
|
||||
const result = [];
|
||||
let i = 0;
|
||||
while (i < str.length) {
|
||||
const char = str[i];
|
||||
i++;
|
||||
if (char in decodeMapConstant) {
|
||||
result.push(decodeMapConstant[char]);
|
||||
continue;
|
||||
}
|
||||
if (char in decodePrefixMapNumber) {
|
||||
const { prefix, len } = decodePrefixMapNumber[char];
|
||||
const lenStr = str.substring(i, i + len);
|
||||
i += len;
|
||||
const radix = prefix == "N" ? 36 : 10;
|
||||
const lenNum = parseInt(lenStr, 36);
|
||||
const value = str.substring(i, i + lenNum);
|
||||
i += lenNum;
|
||||
result.push(parseInt(value, radix));
|
||||
continue;
|
||||
}
|
||||
const { prefix, len } = decodePrefixMapObject[char];
|
||||
const lenStr = str.substring(i, i + len);
|
||||
i += len;
|
||||
const lenNum = parseInt(lenStr, 36);
|
||||
const value = str.substring(i, i + lenNum);
|
||||
i += lenNum;
|
||||
if (prefix == "s") {
|
||||
result.push(value);
|
||||
} else {
|
||||
result.push(JSON.parse(value));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 08b43da7fb...b8fb5e5e63
@@ -1,7 +1,15 @@
|
||||
import { fireAndForget, yieldMicrotask } from "octagonal-wheels/promises";
|
||||
import type { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB";
|
||||
import { AbstractModule } from "../AbstractModule";
|
||||
import { Logger, LOG_LEVEL_NOTICE, LOG_LEVEL_INFO, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||
import {
|
||||
Logger,
|
||||
LOG_LEVEL_NOTICE,
|
||||
LOG_LEVEL_INFO,
|
||||
LOG_LEVEL_VERBOSE,
|
||||
LEVEL_NOTICE,
|
||||
LEVEL_INFO,
|
||||
type LOG_LEVEL,
|
||||
} from "octagonal-wheels/common/logger";
|
||||
import { isLockAcquired, shareRunningResult, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
|
||||
import { balanceChunkPurgedDBs } from "@/lib/src/pouchdb/chunks";
|
||||
import { purgeUnreferencedChunks } from "@/lib/src/pouchdb/chunks";
|
||||
@@ -28,7 +36,7 @@ import {
|
||||
updatePreviousExecutionTime,
|
||||
} from "../../common/utils";
|
||||
import { isAnyNote } from "../../lib/src/common/utils";
|
||||
import { EVENT_FILE_SAVED, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
|
||||
import { EVENT_FILE_SAVED, EVENT_ON_UNRESOLVED_ERROR, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
|
||||
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
|
||||
|
||||
import { $msg } from "../../lib/src/common/i18n";
|
||||
@@ -40,6 +48,20 @@ const REPLICATION_ON_EVENT_FORECASTED_TIME = 5000;
|
||||
|
||||
export class ModuleReplicator extends AbstractModule {
|
||||
_replicatorType?: RemoteType;
|
||||
_previousErrors = new Set<string>();
|
||||
|
||||
showError(msg: string, max_log_level: LOG_LEVEL = LEVEL_NOTICE) {
|
||||
const level = this._previousErrors.has(msg) ? LEVEL_INFO : max_log_level;
|
||||
this._log(msg, level);
|
||||
if (!this._previousErrors.has(msg)) {
|
||||
this._previousErrors.add(msg);
|
||||
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
|
||||
}
|
||||
}
|
||||
clearErrors() {
|
||||
this._previousErrors.clear();
|
||||
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
|
||||
}
|
||||
|
||||
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
|
||||
eventHub.onEvent(EVENT_FILE_SAVED, () => {
|
||||
@@ -59,7 +81,7 @@ export class ModuleReplicator extends AbstractModule {
|
||||
async setReplicator() {
|
||||
const replicator = await this.services.replicator.getNewReplicator();
|
||||
if (!replicator) {
|
||||
this._log($msg("Replicator.Message.InitialiseFatalError"), LOG_LEVEL_NOTICE);
|
||||
this.showError($msg("Replicator.Message.InitialiseFatalError"), LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
}
|
||||
if (this.core.replicator) {
|
||||
@@ -89,7 +111,7 @@ export class ModuleReplicator extends AbstractModule {
|
||||
// Checking salt
|
||||
const replicator = this.services.replicator.getActiveReplicator();
|
||||
if (!replicator) {
|
||||
this._log($msg("Replicator.Message.InitialiseFatalError"), LOG_LEVEL_NOTICE);
|
||||
this.showError($msg("Replicator.Message.InitialiseFatalError"), LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
}
|
||||
return await replicator.ensurePBKDF2Salt(this.settings, showMessage, true);
|
||||
@@ -98,15 +120,16 @@ export class ModuleReplicator extends AbstractModule {
|
||||
async _everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
|
||||
// Checking salt
|
||||
if (!this.core.managers.networkManager.isOnline) {
|
||||
this._log("Network is offline", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
|
||||
this.showError("Network is offline", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
|
||||
return false;
|
||||
}
|
||||
// Showing message is false: that because be shown here. (And it is a fatal error, no way to hide it).
|
||||
if (!(await this.ensureReplicatorPBKDF2Salt(false))) {
|
||||
Logger("Failed to initialise the encryption key, preventing replication.", LOG_LEVEL_NOTICE);
|
||||
this.showError("Failed to initialise the encryption key, preventing replication.");
|
||||
return false;
|
||||
}
|
||||
await this.loadQueuedFiles();
|
||||
this.clearErrors();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -195,18 +218,19 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
||||
}
|
||||
|
||||
if (!(await this.services.fileProcessing.commitPendingFileEvents())) {
|
||||
Logger($msg("Replicator.Message.Pending"), LOG_LEVEL_NOTICE);
|
||||
this.showError($msg("Replicator.Message.Pending"), LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.core.managers.networkManager.isOnline) {
|
||||
this._log("Network is offline", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
|
||||
this.showError("Network is offline", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
|
||||
return false;
|
||||
}
|
||||
if (!(await this.services.replication.onBeforeReplicate(showMessage))) {
|
||||
Logger($msg("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
|
||||
this.showError($msg("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
}
|
||||
this.clearErrors();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -401,11 +425,56 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
||||
this.saveQueuedFiles();
|
||||
});
|
||||
|
||||
async checkIsChangeRequiredForDatabaseProcessing(dbDoc: LoadedEntry): Promise<boolean> {
|
||||
const path = getPath(dbDoc);
|
||||
try {
|
||||
const savedDoc = await this.localDatabase.getRaw<LoadedEntry>(dbDoc._id, {
|
||||
conflicts: true,
|
||||
revs_info: true,
|
||||
});
|
||||
const newRev = dbDoc._rev ?? "";
|
||||
const latestRev = savedDoc._rev ?? "";
|
||||
const revisions = savedDoc._revs_info?.map((e) => e.rev) ?? [];
|
||||
if (savedDoc._conflicts && savedDoc._conflicts.length > 0) {
|
||||
// There are conflicts, so we have to process it.
|
||||
return true;
|
||||
}
|
||||
if (newRev == latestRev) {
|
||||
// The latest revision. We need to process it.
|
||||
return true;
|
||||
}
|
||||
const index = revisions.indexOf(newRev);
|
||||
if (index >= 0) {
|
||||
// the revision has been inserted before.
|
||||
return false; // Already processed.
|
||||
}
|
||||
return true; // This mostly should not happen, but we have to process it just in case.
|
||||
} catch (e: any) {
|
||||
if ("status" in e && e.status == 404) {
|
||||
return true;
|
||||
// Not existing, so we have to process it.
|
||||
} else {
|
||||
Logger(
|
||||
`Failed to get existing document for ${path} (${dbDoc._id.substring(0, 8)}, ${dbDoc._rev?.substring(0, 10)}) `,
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
Logger(e, LOG_LEVEL_VERBOSE);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
databaseQueuedProcessor = new QueueProcessor(
|
||||
async (docs: EntryBody[]) => {
|
||||
const dbDoc = docs[0] as LoadedEntry; // It has no `data`
|
||||
const path = getPath(dbDoc);
|
||||
|
||||
// If the document is existing with any revision, confirm that we have to process it.
|
||||
const isRequired = await this.checkIsChangeRequiredForDatabaseProcessing(dbDoc);
|
||||
if (!isRequired) {
|
||||
Logger(`Skipped (Not latest): ${path} (${dbDoc._id.substring(0, 8)})`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
// If `Read chunks online` is disabled, chunks should be transferred before here.
|
||||
// However, in some cases, chunks are after that. So, if missing chunks exist, we have to wait for them.
|
||||
const doc = await this.localDatabase.getDBEntryFromMeta({ ...dbDoc }, false, true);
|
||||
@@ -503,6 +572,10 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
||||
return !checkResult;
|
||||
}
|
||||
|
||||
private _reportUnresolvedMessages(): Promise<string[]> {
|
||||
return Promise.resolve([...this._previousErrors]);
|
||||
}
|
||||
|
||||
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||
services.replicator.handleGetActiveReplicator(this._getReplicator.bind(this));
|
||||
services.databaseEvents.handleOnDatabaseInitialisation(this._everyOnInitializeDatabase.bind(this));
|
||||
@@ -516,5 +589,6 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
||||
services.replication.handleReplicateByEvent(this._replicateByEvent.bind(this));
|
||||
services.remote.handleReplicateAllToRemote(this._replicateAllToServer.bind(this));
|
||||
services.remote.handleReplicateAllFromRemote(this._replicateAllFromServer.bind(this));
|
||||
services.appLifecycle.reportUnresolvedMessages(this._reportUnresolvedMessages.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||
import { LOG_LEVEL_DEBUG, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||
import {
|
||||
LEVEL_INFO,
|
||||
LEVEL_NOTICE,
|
||||
LOG_LEVEL_DEBUG,
|
||||
LOG_LEVEL_NOTICE,
|
||||
LOG_LEVEL_VERBOSE,
|
||||
type LOG_LEVEL,
|
||||
} from "octagonal-wheels/common/logger";
|
||||
import { Notice, requestUrl, type RequestUrlParam, type RequestUrlResponse } from "../../deps.ts";
|
||||
import { type CouchDBCredentials, type EntryDoc, type FilePath } from "../../lib/src/common/types.ts";
|
||||
import { getPathFromTFile } from "../../common/utils.ts";
|
||||
@@ -12,6 +19,7 @@ import { ObsHttpHandler } from "./APILib/ObsHttpHandler.ts";
|
||||
import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.ts";
|
||||
import { AuthorizationHeaderGenerator } from "../../lib/src/replication/httplib.ts";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
import { EVENT_ON_UNRESOLVED_ERROR, eventHub } from "../../common/events.ts";
|
||||
|
||||
setNoticeClass(Notice);
|
||||
|
||||
@@ -24,7 +32,20 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
_customHandler!: ObsHttpHandler;
|
||||
|
||||
_authHeader = new AuthorizationHeaderGenerator();
|
||||
_previousErrors = new Set<string>();
|
||||
|
||||
showError(msg: string, max_log_level: LOG_LEVEL = LEVEL_NOTICE) {
|
||||
const level = this._previousErrors.has(msg) ? LEVEL_INFO : max_log_level;
|
||||
this._log(msg, level);
|
||||
if (!this._previousErrors.has(msg)) {
|
||||
this._previousErrors.add(msg);
|
||||
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
|
||||
}
|
||||
}
|
||||
clearErrors() {
|
||||
this._previousErrors.clear();
|
||||
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
|
||||
}
|
||||
last_successful_post = false;
|
||||
_customFetchHandler(): ObsHttpHandler {
|
||||
if (!this._customHandler) this._customHandler = new ObsHttpHandler(undefined, undefined);
|
||||
@@ -180,6 +201,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.clearErrors();
|
||||
return response;
|
||||
} catch (ex) {
|
||||
if (ex instanceof TypeError) {
|
||||
@@ -195,7 +217,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
headers,
|
||||
});
|
||||
if (resp2.status / 100 == 2) {
|
||||
this._log(
|
||||
this.showError(
|
||||
"The request was successful by API. But the native fetch API failed! Please check CORS settings on the remote database!. While this condition, you cannot enable LiveSync",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
@@ -203,7 +225,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
}
|
||||
const r2 = resp2.clone();
|
||||
const msg = await r2.text();
|
||||
this._log(`Failed to fetch by API. ${resp2.status}: ${msg}`, LOG_LEVEL_NOTICE);
|
||||
this.showError(`Failed to fetch by API. ${resp2.status}: ${msg}`, LOG_LEVEL_NOTICE);
|
||||
return resp2;
|
||||
}
|
||||
throw ex;
|
||||
@@ -211,7 +233,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
} catch (ex: any) {
|
||||
this._log(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL_VERBOSE);
|
||||
const msg = ex instanceof Error ? `${ex?.name}:${ex?.message}` : ex?.toString();
|
||||
this._log(`Failed to fetch: ${msg}`, LOG_LEVEL_NOTICE);
|
||||
this.showError(`Failed to fetch: ${msg}`); // Do not show notice, due to throwing below
|
||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
||||
// limit only in bulk_docs.
|
||||
if (url.toString().indexOf("_bulk_docs") !== -1) {
|
||||
@@ -279,6 +301,10 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
return `${"appId" in this.app ? this.app.appId : ""}`;
|
||||
}
|
||||
|
||||
private _reportUnresolvedMessages(): Promise<string[]> {
|
||||
return Promise.resolve([...this._previousErrors]);
|
||||
}
|
||||
|
||||
onBindFunction(core: LiveSyncCore, services: typeof core.services) {
|
||||
services.API.handleGetCustomFetchHandler(this._customFetchHandler.bind(this));
|
||||
services.API.handleIsLastPostFailedDueToPayloadSize(this._getLastPostFailedBySize.bind(this));
|
||||
@@ -288,5 +314,6 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
services.vault.handleVaultName(this._vaultName.bind(this));
|
||||
services.vault.handleGetActiveFilePath(this._getActiveFilePath.bind(this));
|
||||
services.API.handleGetAppID(this._anyGetAppId.bind(this));
|
||||
services.appLifecycle.reportUnresolvedMessages(this._reportUnresolvedMessages.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,12 @@ import {
|
||||
logMessages,
|
||||
} from "../../lib/src/mock_and_interop/stores.ts";
|
||||
import { eventHub } from "../../lib/src/hub/hub.ts";
|
||||
import { EVENT_FILE_RENAMED, EVENT_LAYOUT_READY, EVENT_LEAF_ACTIVE_CHANGED } from "../../common/events.ts";
|
||||
import {
|
||||
EVENT_FILE_RENAMED,
|
||||
EVENT_LAYOUT_READY,
|
||||
EVENT_LEAF_ACTIVE_CHANGED,
|
||||
EVENT_ON_UNRESOLVED_ERROR,
|
||||
} from "../../common/events.ts";
|
||||
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||
import { addIcon, normalizePath, Notice } from "../../deps.ts";
|
||||
import { LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/logger";
|
||||
@@ -198,11 +203,13 @@ export class ModuleLog extends AbstractObsidianModule {
|
||||
this.applyStatusBarText();
|
||||
}, 20);
|
||||
statusBarLabels.onChanged((label) => applyToDisplay(label.value));
|
||||
this.activeFileStatus.onChanged(() => this.updateMessageArea());
|
||||
}
|
||||
|
||||
private _everyOnload(): Promise<boolean> {
|
||||
eventHub.onEvent(EVENT_LEAF_ACTIVE_CHANGED, () => this.onActiveLeafChange());
|
||||
eventHub.onceEvent(EVENT_LAYOUT_READY, () => this.onActiveLeafChange());
|
||||
eventHub.onEvent(EVENT_ON_UNRESOLVED_ERROR, () => this.updateMessageArea());
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
@@ -234,8 +241,19 @@ export class ModuleLog extends AbstractObsidianModule {
|
||||
async setFileStatus() {
|
||||
const fileStatus = await this.getActiveFileStatus();
|
||||
this.activeFileStatus.value = fileStatus;
|
||||
this.messageArea!.innerText = this.settings.hideFileWarningNotice ? "" : fileStatus;
|
||||
}
|
||||
|
||||
async updateMessageArea() {
|
||||
if (this.messageArea) {
|
||||
const messageLines = [];
|
||||
const fileStatus = this.activeFileStatus.value;
|
||||
if (fileStatus && !this.settings.hideFileWarningNotice) messageLines.push(fileStatus);
|
||||
const messages = (await this.services.appLifecycle.getUnresolvedMessages()).flat().filter((e) => e);
|
||||
messageLines.push(...messages);
|
||||
this.messageArea.innerText = messageLines.map((e) => `⚠️ ${e}`).join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
onActiveLeafChange() {
|
||||
fireAndForget(async () => {
|
||||
this.adjustStatusDivPosition();
|
||||
|
||||
@@ -77,6 +77,9 @@ export class ModuleSetupObsidian extends AbstractObsidianModule {
|
||||
async encodeQR() {
|
||||
const settingString = encodeSettingsToQRCodeData(this.settings);
|
||||
const codeSVG = encodeQR(settingString, OutputFormat.SVG);
|
||||
if (codeSVG == "") {
|
||||
return "";
|
||||
}
|
||||
const msg = $msg("Setup.QRCode", { qr_image: codeSVG });
|
||||
await this.core.confirm.confirmWithMessage("Settings QR Code", msg, ["OK"], "OK");
|
||||
return await Promise.resolve(codeSVG);
|
||||
|
||||
@@ -41,7 +41,7 @@ export function getBucketConfigSummary(setting: ObsidianLiveSyncSettings, showAd
|
||||
*/
|
||||
export function getCouchDBConfigSummary(setting: ObsidianLiveSyncSettings, showAdvanced = false) {
|
||||
const settingTable: Partial<ObsidianLiveSyncSettings> = pickCouchDBSyncSettings(setting);
|
||||
return getSummaryFromPartialSettings(settingTable, showAdvanced);
|
||||
return getSummaryFromPartialSettings(settingTable, showAdvanced || setting.useJWT);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -223,7 +223,7 @@
|
||||
<option value="ES512">ES512</option>
|
||||
</select>
|
||||
</InputRow>
|
||||
<InputRow label="JWT Expiration Duration (seconds)">
|
||||
<InputRow label="JWT Expiration Duration (minutes)">
|
||||
<input
|
||||
type="text"
|
||||
name="couchdb-jwt-exp-duration"
|
||||
@@ -233,19 +233,25 @@
|
||||
/>
|
||||
</InputRow>
|
||||
<InputRow label="JWT Key">
|
||||
<input
|
||||
type="text"
|
||||
<textarea
|
||||
name="couchdb-jwt-key"
|
||||
rows="5"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
placeholder="Enter your JWT secret or private key"
|
||||
bind:value={syncSetting.jwtKey}
|
||||
disabled={!isUseJWT}
|
||||
/>
|
||||
></textarea>
|
||||
</InputRow>
|
||||
<InfoNote>
|
||||
For HS256/HS512 algorithms, provide the shared secret key. For ES256/ES512 algorithms, provide the pkcs8
|
||||
PEM-formatted private key.
|
||||
</InfoNote>
|
||||
<InputRow label="JWT Key ID (kid)">
|
||||
<input
|
||||
type="text"
|
||||
name="couchdb-jwt-kid"
|
||||
placeholder="Enter your JWT Key ID (optional)"
|
||||
placeholder="Enter your JWT Key ID"
|
||||
bind:value={syncSetting.jwtKid}
|
||||
disabled={!isUseJWT}
|
||||
/>
|
||||
@@ -254,7 +260,7 @@
|
||||
<input
|
||||
type="text"
|
||||
name="couchdb-jwt-sub"
|
||||
placeholder="Enter your JWT Subject (optional)"
|
||||
placeholder="Enter your JWT Subject (CouchDB Username)"
|
||||
bind:value={syncSetting.jwtSub}
|
||||
disabled={!isUseJWT}
|
||||
/>
|
||||
|
||||
11
styles.css
11
styles.css
@@ -414,12 +414,19 @@ div.workspace-leaf-content[data-type=bases] .livesync-status {
|
||||
|
||||
}
|
||||
|
||||
.livesync-status div.livesync-status-messagearea {
|
||||
.livesync-status div.livesync-status-messagearea:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.livesync-status div.livesync-status-messagearea:not(:empty) {
|
||||
opacity: 0.6;
|
||||
color: var(--text-on-accent);
|
||||
background: var(--background-modifier-error);
|
||||
border: 1px solid var(--background-modifier-error);
|
||||
background-color: rgba(var(--background-modifier-error-rgb), 0.2);
|
||||
-webkit-filter: unset;
|
||||
filter: unset;
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
99
updates.md
99
updates.md
@@ -4,6 +4,58 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
|
||||
|
||||
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
|
||||
|
||||
## 0.25.26
|
||||
|
||||
07th November, 2025
|
||||
|
||||
### Improved
|
||||
|
||||
- Some JWT notes have been added to the setting dialogue (#742).
|
||||
|
||||
### Fixed
|
||||
|
||||
- No longer wrong values encoded into the QR code.
|
||||
- We can acknowledge why the QR codes have not been generated.
|
||||
- Probably too large a dataset to encode. When this happens, please consider using Setup-URI via text instead of QR code, or reduce the settings temporarily.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Some dependencies have been updated.
|
||||
- Internal functions have been modularised into `octagonal-wheels` packages and are well tested.
|
||||
- `dataobject/Computed` for caching computed values.
|
||||
- `encodeAnyArray/decodeAnyArray` for encoding and decoding any array-like data into compact strings (#729).
|
||||
- Fixed importing from the parent project in library codes. (#729).
|
||||
|
||||
## 0.25.25
|
||||
|
||||
06th November, 2025
|
||||
|
||||
### Fixed
|
||||
|
||||
#### JWT Authentication
|
||||
|
||||
- Now we can use JWT Authentication ES512 correctly (#742).
|
||||
- Several misdirections in the Setting dialogues have been fixed (i.e., seconds and minutes confusion...).
|
||||
- The key area in the Setting dialogue has been enlarged and accepts newlines correctly.
|
||||
- Caching of JWT tokens now works correctly
|
||||
- Tokens are now cached and reused until they expire.
|
||||
- They will be kept until 10% of the expiration duration is remaining or 10 seconds, whichever is longer (but at a maximum of 1 minute).
|
||||
- JWT settings are now correctly displayed on the Setting dialogue.
|
||||
|
||||
And, tips about JWT Authentication on CouchDB have been added to the documentation (docs/tips/jwt-on-couchdb.md).
|
||||
|
||||
#### Other fixes
|
||||
|
||||
- Receiving non-latest revisions no longer causes unexpected overwrites.
|
||||
- On receiving revisions that made conflicting changes, we are still able to handle them.
|
||||
|
||||
### Improved
|
||||
|
||||
- No longer duplicated message notifications are shown when a connection to the remote server fails.
|
||||
- Instead, a single notification is shown, and it will be kept on the notification area inside the editor until the situation is resolved.
|
||||
- The notification area is no longer imposing, distracting, and overwhelming.
|
||||
- With a pale background, but bordered and with icons.
|
||||
|
||||
## 0.25.24
|
||||
|
||||
04th November, 2025
|
||||
@@ -74,52 +126,5 @@ up your own TURN server for your FQDN, if possible.
|
||||
- Please upgrade all devices to v0.25.24.beta1 or later to use this feature again.
|
||||
- This is due to security improvements in the encryption scheme.
|
||||
|
||||
## 0.25.23
|
||||
|
||||
26th October, 2025
|
||||
|
||||
The next version we are preparing (you know that as 0.25.23.beta1) is now still on beta, resulting in this rather unfortunate versioning situation. Apologies for the confusion. The next v0.25.23.beta2 will be v0.25.24.beta1. In other words, this is a v0.25.22.patch-1 actually, but possibly not allowed by Obsidian's rule.
|
||||
(Perhaps we ought to declare 1.0.0 with a little more confidence. The current minor part has been effectively a major one for a long time. If it were 1.22.1 and 1.23.0.beta1, no confusion ).
|
||||
|
||||
### Fixed
|
||||
|
||||
- We are now able to enable optional features correctly again (#732).
|
||||
- No longer oversized files have been processed, furthermore.
|
||||
|
||||
- Before creating a chunk, the file is verified as the target.
|
||||
- The behaviour upon receiving replication has been changed as follows:
|
||||
- If the remote file is oversized, it is ignored.
|
||||
- If not, but while the local file is oversized, it is also ignored.
|
||||
|
||||
- We are now able to enable optional features correctly again (#732).
|
||||
- No longer oversized files have been processed, furthermore.
|
||||
- Before creating a chunk, the file is verified as the target.
|
||||
- The behaviour upon receiving replication has been changed as follows:
|
||||
- If the remote file is oversized, it is ignored.
|
||||
- If not, but while the local file is oversized, it is also ignored.
|
||||
|
||||
## 0.25.22
|
||||
|
||||
15th October, 2025
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a bug that caused wrong event bindings and flag inversion (#727)
|
||||
- This caused following issues:
|
||||
- In some cases, settings changes were not applied or saved correctly.
|
||||
- Automatic synchronisation did not begin correctly.
|
||||
|
||||
### Improved
|
||||
|
||||
- Too large diffs are not shown in the file comparison view, due to performance reasons.
|
||||
|
||||
### Notes
|
||||
|
||||
- The checking algorithm implemented in 0.25.20 is also raised as PR (#237). And completely I merged it manually.
|
||||
- Sorry for lacking merging this PR, and let me say thanks to the great contribution, @bioluks !
|
||||
- Known issues:
|
||||
- Sync on Editor save seems not to work correctly in some cases.
|
||||
- I am investigating this issue. If you have any information, please let me know.
|
||||
|
||||
Older notes are in
|
||||
[updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md).
|
||||
|
||||
@@ -10,6 +10,53 @@ As a result, this is the first time in a while that forward compatibility has be
|
||||
|
||||
---
|
||||
|
||||
## 0.25.23
|
||||
|
||||
26th October, 2025
|
||||
|
||||
The next version we are preparing (you know that as 0.25.23.beta1) is now still on beta, resulting in this rather unfortunate versioning situation. Apologies for the confusion. The next v0.25.23.beta2 will be v0.25.24.beta1. In other words, this is a v0.25.22.patch-1 actually, but possibly not allowed by Obsidian's rule.
|
||||
(Perhaps we ought to declare 1.0.0 with a little more confidence. The current minor part has been effectively a major one for a long time. If it were 1.22.1 and 1.23.0.beta1, no confusion ).
|
||||
|
||||
### Fixed
|
||||
|
||||
- We are now able to enable optional features correctly again (#732).
|
||||
- No longer oversized files have been processed, furthermore.
|
||||
|
||||
- Before creating a chunk, the file is verified as the target.
|
||||
- The behaviour upon receiving replication has been changed as follows:
|
||||
- If the remote file is oversized, it is ignored.
|
||||
- If not, but while the local file is oversized, it is also ignored.
|
||||
|
||||
- We are now able to enable optional features correctly again (#732).
|
||||
- No longer oversized files have been processed, furthermore.
|
||||
- Before creating a chunk, the file is verified as the target.
|
||||
- The behaviour upon receiving replication has been changed as follows:
|
||||
- If the remote file is oversized, it is ignored.
|
||||
- If not, but while the local file is oversized, it is also ignored.
|
||||
|
||||
## 0.25.22
|
||||
|
||||
15th October, 2025
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a bug that caused wrong event bindings and flag inversion (#727)
|
||||
- This caused following issues:
|
||||
- In some cases, settings changes were not applied or saved correctly.
|
||||
- Automatic synchronisation did not begin correctly.
|
||||
|
||||
### Improved
|
||||
|
||||
- Too large diffs are not shown in the file comparison view, due to performance reasons.
|
||||
|
||||
### Notes
|
||||
|
||||
- The checking algorithm implemented in 0.25.20 is also raised as PR (#237). And completely I merged it manually.
|
||||
- Sorry for lacking merging this PR, and let me say thanks to the great contribution, @bioluks !
|
||||
- Known issues:
|
||||
- Sync on Editor save seems not to work correctly in some cases.
|
||||
- I am investigating this issue. If you have any information, please let me know.
|
||||
|
||||
## 0.25.21
|
||||
|
||||
13th October, 2025
|
||||
|
||||
Reference in New Issue
Block a user