mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-07 00:01:51 +00:00
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aab0f7f034 | ||
|
|
b3a0deb0e3 | ||
|
|
b9138d1395 | ||
|
|
7eb9807aa5 | ||
|
|
91a4f234f1 | ||
|
|
82f2860938 | ||
|
|
5443317157 | ||
|
|
47fe9d2af3 | ||
|
|
8b81570035 | ||
|
|
d3e50421e4 | ||
|
|
12605f4604 | ||
|
|
2c0dd82886 | ||
|
|
f5315aacb8 | ||
|
|
5a93066870 | ||
|
|
3a73073505 | ||
|
|
ee0c0ee611 | ||
|
|
d7ea30e304 | ||
|
|
2b9ded60f7 | ||
|
|
40508822cf | ||
|
|
6f938d5f54 | ||
|
|
51dc44bfb0 | ||
|
|
7c4f2bf78a | ||
|
|
67c9b4cf06 | ||
|
|
4808876968 | ||
|
|
cccff21ecc | ||
|
|
d8415a97e5 | ||
|
|
85e9aa2978 | ||
|
|
b4eb0e4868 | ||
|
|
3ea348f468 | ||
|
|
81362816d6 | ||
|
|
d6efe4510f | ||
|
|
ca5a7ae18c | ||
|
|
a27652ac34 | ||
|
|
29b89efc47 | ||
|
|
ef3eef2d08 | ||
|
|
ffbbe32e36 | ||
|
|
0a5371cdee | ||
|
|
466bb142e2 | ||
|
|
d394a4ce7f | ||
|
|
71ce76e502 | ||
|
|
ae7a7dd456 | ||
|
|
4048186bb5 | ||
|
|
2b94fd9139 | ||
|
|
ec72ece86d | ||
|
|
e394a994c5 | ||
|
|
aa23b6a39a | ||
|
|
58e328a591 | ||
|
|
1730c39d70 | ||
|
|
dfeac201a2 | ||
|
|
b42152db5e | ||
|
|
171cfc0a38 | ||
|
|
d2787bdb6a | ||
|
|
44b022f003 | ||
|
|
58845276e7 | ||
|
|
a2cc093a9e | ||
|
|
fec203a751 | ||
|
|
1a06837769 | ||
|
|
18d1ce8ec8 | ||
|
|
2221d8c4e8 | ||
|
|
08548f8630 | ||
|
|
5d24c3b984 | ||
|
|
de8fd43c8b | ||
|
|
ed88761eaa | ||
|
|
4dcb37f5a2 | ||
|
|
db0562eda1 | ||
|
|
b610d5d959 | ||
|
|
5abba74f3b | ||
|
|
021c1fccfe | ||
|
|
341f0ab12d | ||
|
|
39340c1e1b | ||
|
|
55cdc58857 | ||
|
|
4f1a9dc4e8 | ||
|
|
013818b7d0 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -9,6 +9,7 @@ package-lock.json
|
|||||||
# build
|
# build
|
||||||
main.js
|
main.js
|
||||||
main_org.js
|
main_org.js
|
||||||
|
main_org_*.js
|
||||||
*.js.map
|
*.js.map
|
||||||
meta.json
|
meta.json
|
||||||
meta-*.json
|
meta-*.json
|
||||||
@@ -17,3 +18,6 @@ meta-*.json
|
|||||||
# obsidian
|
# obsidian
|
||||||
data.json
|
data.json
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Additionally, it supports peer-to-peer synchronisation using WebRTC now (experim
|
|||||||
- Use open-source solutions for the server.
|
- Use open-source solutions for the server.
|
||||||
- Compatible solutions are supported.
|
- Compatible solutions are supported.
|
||||||
- Support end-to-end encryption.
|
- Support end-to-end encryption.
|
||||||
- Synchronise settings, snippets, themes, and plug-ins via [Customisation Sync (Beta)](#customization-sync) or [Hidden File Sync](#hiddenfilesync).
|
- Synchronise settings, snippets, themes, and plug-ins via [Customisation Sync (Beta)](docs/settings.md#6-customization-sync-advanced) or [Hidden File Sync](docs/settings.md#7-hidden-files-advanced).
|
||||||
- Enable WebRTC peer-to-peer synchronisation without requiring a `host` (Experimental).
|
- Enable WebRTC peer-to-peer synchronisation without requiring a `host` (Experimental).
|
||||||
- This feature is still in the experimental stage. Please exercise caution when using it.
|
- This feature is still in the experimental stage. Please exercise caution when using it.
|
||||||
- WebRTC is a peer-to-peer synchronisation method, so **at least one device must be online to synchronise**.
|
- WebRTC is a peer-to-peer synchronisation method, so **at least one device must be online to synchronise**.
|
||||||
|
|||||||
@@ -5,10 +5,15 @@
|
|||||||
- [Setup a CouchDB server](#setup-a-couchdb-server)
|
- [Setup a CouchDB server](#setup-a-couchdb-server)
|
||||||
- [Table of Contents](#table-of-contents)
|
- [Table of Contents](#table-of-contents)
|
||||||
- [1. Prepare CouchDB](#1-prepare-couchdb)
|
- [1. Prepare CouchDB](#1-prepare-couchdb)
|
||||||
- [A. Using Docker container](#a-using-docker-container)
|
- [A. Using Docker](#a-using-docker)
|
||||||
- [1. Prepare](#1-prepare)
|
- [1. Prepare](#1-prepare)
|
||||||
- [2. Run docker container](#2-run-docker-container)
|
- [2. Run docker container](#2-run-docker-container)
|
||||||
- [B. Install CouchDB directly](#b-install-couchdb-directly)
|
- [B. Using Docker Compose](#b-using-docker-compose)
|
||||||
|
- [1. Prepare](#1-prepare-1)
|
||||||
|
- [2. Creating Compose file](#2-create-a-docker-composeyml-file-with-the-following-added-to-it)
|
||||||
|
- [3. Boot check](#3-run-the-docker-compose-file-to-boot-check)
|
||||||
|
- [4. Starting Docker Compose in background](#4-run-the-docker-compose-file-in-the-background)
|
||||||
|
- [C. Install CouchDB directly](#c-install-couchdb-directly)
|
||||||
- [2. Run couchdb-init.sh for initialise](#2-run-couchdb-initsh-for-initialise)
|
- [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)
|
- [3. Expose CouchDB to the Internet](#3-expose-couchdb-to-the-internet)
|
||||||
- [4. Client Setup](#4-client-setup)
|
- [4. Client Setup](#4-client-setup)
|
||||||
@@ -21,44 +26,56 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 1. Prepare CouchDB
|
## 1. Prepare CouchDB
|
||||||
### A. Using Docker container
|
### A. Using Docker
|
||||||
|
|
||||||
#### 1. Prepare
|
#### 1. Prepare
|
||||||
```bash
|
```bash
|
||||||
|
|
||||||
# Prepare environment variables.
|
# Adding environment variables.
|
||||||
export hostname=localhost:5984
|
export hostname=localhost:5984
|
||||||
export username=goojdasjdas #Please change as you like.
|
export username=goojdasjdas #Please change as you like.
|
||||||
export password=kpkdasdosakpdsa #Please change as you like
|
export password=kpkdasdosakpdsa #Please change as you like
|
||||||
|
|
||||||
# Prepare directories which save data and configurations.
|
# Creating the save data & configuration directories.
|
||||||
mkdir couchdb-data
|
mkdir couchdb-data
|
||||||
mkdir couchdb-etc
|
mkdir couchdb-etc
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. Run docker container
|
#### 2. Run docker container
|
||||||
|
|
||||||
1. Boot Check.
|
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
|
$ 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.
|
> [!WARNING]
|
||||||
Once CouchDB run, these directories will be owned by uid:`5984`. Please chown it for you again.
|
> If your container threw an error or exited unexpectedly, please check the permission of couchdb-data, and couchdb-etc.
|
||||||
|
> Once CouchDB starts, these directories will be owned by uid:`5984`. Please chown it for that uid again.
|
||||||
|
|
||||||
2. Enable it in the background
|
2. Enable it in the background
|
||||||
```
|
```
|
||||||
$ 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
|
$ 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
|
||||||
```
|
```
|
||||||
If you prefer a compose file instead of docker run, here is the equivalent below:
|
|
||||||
|
Congrats, move on to [step 2](#2-run-couchdb-initsh-for-initialise)
|
||||||
|
### B. Using Docker Compose
|
||||||
|
|
||||||
|
#### 1. Prepare
|
||||||
|
|
||||||
|
```
|
||||||
|
# Creating the save data & configuration directories.
|
||||||
|
mkdir couchdb-data
|
||||||
|
mkdir couchdb-etc
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Create a `docker-compose.yml` file with the following added to it
|
||||||
```
|
```
|
||||||
services:
|
services:
|
||||||
couchdb:
|
couchdb:
|
||||||
image: couchdb:latest
|
image: couchdb:latest
|
||||||
container_name: couchdb-for-ols
|
container_name: couchdb-for-ols
|
||||||
user: 1000:1000
|
user: 5984:5984
|
||||||
environment:
|
environment:
|
||||||
- COUCHDB_USER=${username}
|
- COUCHDB_USER=<INSERT USERNAME HERE> #Please change as you like.
|
||||||
- COUCHDB_PASSWORD=${password}
|
- COUCHDB_PASSWORD=<INSERT PASSWORD HERE> #Please change as you like.
|
||||||
volumes:
|
volumes:
|
||||||
- ./couchdb-data:/opt/couchdb/data
|
- ./couchdb-data:/opt/couchdb/data
|
||||||
- ./couchdb-etc:/opt/couchdb/etc/local.d
|
- ./couchdb-etc:/opt/couchdb/etc/local.d
|
||||||
@@ -66,7 +83,30 @@ services:
|
|||||||
- 5984:5984
|
- 5984:5984
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
### B. Install CouchDB directly
|
|
||||||
|
#### 3. Run the Docker Compose file to boot check
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose up
|
||||||
|
# Or if using the old version
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
> [!WARNING]
|
||||||
|
> If your container threw an error or exited unexpectedly, please check the permission of couchdb-data, and couchdb-etc.
|
||||||
|
> Once CouchDB starts, these directories will be owned by uid:`5984`. Please chown it for that uid again.
|
||||||
|
|
||||||
|
#### 4. Run the Docker Compose file in the background
|
||||||
|
If all went well and didn't throw any errors, `CTRL+C` out of it, and then run this command
|
||||||
|
```
|
||||||
|
docker compose up -d
|
||||||
|
# Or if using the old version
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Congrats, move on to [step 2](#2-run-couchdb-initsh-for-initialise)
|
||||||
|
|
||||||
|
|
||||||
|
### C. Install CouchDB directly
|
||||||
Please refer to the [official document](https://docs.couchdb.org/en/stable/install/index.html). However, we do not have to configure it fully. Just the administrator needs to be configured.
|
Please refer to the [official document](https://docs.couchdb.org/en/stable/install/index.html). However, we do not have to configure it fully. Just the administrator needs to be configured.
|
||||||
|
|
||||||
## 2. Run couchdb-init.sh for initialise
|
## 2. Run couchdb-init.sh for initialise
|
||||||
@@ -92,6 +132,11 @@ If it results like the following:
|
|||||||
|
|
||||||
Your CouchDB has been initialised successfully. If you want this manually, please read the script.
|
Your CouchDB has been initialised successfully. If you want this manually, please read the script.
|
||||||
|
|
||||||
|
If you are using Docker Compose and the above command does not work or displays `ERROR: Hostname missing`, you can try running the following command, replacing the placeholders with your own values:
|
||||||
|
```
|
||||||
|
curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | hostname=http://<YOUR SERVER IP>:5984 username=<INSERT USERNAME HERE> password=<INSERT PASSWORD HERE> bash
|
||||||
|
```
|
||||||
|
|
||||||
## 3. Expose CouchDB to the Internet
|
## 3. Expose CouchDB to the Internet
|
||||||
|
|
||||||
- You can skip this instruction if you using only in intranet and only with desktop devices.
|
- You can skip this instruction if you using only in intranet and only with desktop devices.
|
||||||
|
|||||||
@@ -19,6 +19,18 @@ const manifestJson = JSON.parse(fs.readFileSync("./manifest.json") + "");
|
|||||||
const packageJson = JSON.parse(fs.readFileSync("./package.json") + "");
|
const packageJson = JSON.parse(fs.readFileSync("./package.json") + "");
|
||||||
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
|
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
|
||||||
|
|
||||||
|
const PATHS_TEST_INSTALL = process.env?.PATHS_TEST_INSTALL || "";
|
||||||
|
const PATH_TEST_INSTALL = PATHS_TEST_INSTALL.split(path.delimiter).map(p => p.trim()).filter(p => p.length);
|
||||||
|
if (!prod) {
|
||||||
|
if (PATH_TEST_INSTALL) {
|
||||||
|
console.log(`Built files will be copied to ${PATH_TEST_INSTALL}`);
|
||||||
|
} else {
|
||||||
|
console.log("Development build: You can install the plug-in to Obsidian for testing by exporting the PATHS_TEST_INSTALL environment variable with the paths to your vault plugins directories separated by your system path delimiter (':' on Unix, ';' on Windows).");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Production build");
|
||||||
|
}
|
||||||
|
|
||||||
const moduleAliasPlugin = {
|
const moduleAliasPlugin = {
|
||||||
name: "module-alias",
|
name: "module-alias",
|
||||||
setup(build) {
|
setup(build) {
|
||||||
@@ -95,6 +107,21 @@ const plugins = [
|
|||||||
} else {
|
} else {
|
||||||
fs.copyFileSync("./main_org.js", "./main.js");
|
fs.copyFileSync("./main_org.js", "./main.js");
|
||||||
}
|
}
|
||||||
|
if (PATH_TEST_INSTALL) {
|
||||||
|
for (const installPath of PATH_TEST_INSTALL) {
|
||||||
|
const realPath = path.resolve(installPath);
|
||||||
|
console.log(`Copying built files to ${realPath}`);
|
||||||
|
if (!fs.existsSync(realPath)) {
|
||||||
|
console.warn(`Test install path ${installPath} does not exist`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const manifestX = JSON.parse(fs.readFileSync("./manifest.json") + "");
|
||||||
|
manifestX.version = manifestJson.version + "." + Date.now();
|
||||||
|
fs.writeFileSync(path.join(installPath, "manifest.json"), JSON.stringify(manifestX, null, 2));
|
||||||
|
fs.copyFileSync("./main.js", path.join(installPath, "main.js"));
|
||||||
|
fs.copyFileSync("./styles.css", path.join(installPath, "styles.css"));
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
1
example.env
Normal file
1
example.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
PATHS_TEST_INSTALL=your-vault-plugin-path:and-another-path
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.25.2",
|
"version": "0.25.24.beta3",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.25.6",
|
"version": "0.25.24.beta3",
|
||||||
"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",
|
||||||
|
|||||||
7324
package-lock.json
generated
7324
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.25.6",
|
"version": "0.25.24.beta3",
|
||||||
"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",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"postbakei18n": "prettier --config ./.prettierrc ./src/lib/src/common/messages/*.ts --write --log-level error",
|
"postbakei18n": "prettier --config ./.prettierrc ./src/lib/src/common/messages/*.ts --write --log-level error",
|
||||||
"posti18n:yaml2json": "npm run prettyjson",
|
"posti18n:yaml2json": "npm run prettyjson",
|
||||||
"predev": "npm run bakei18n",
|
"predev": "npm run bakei18n",
|
||||||
"dev": "node esbuild.config.mjs",
|
"dev": "node --env-file=.env esbuild.config.mjs",
|
||||||
"prebuild": "npm run bakei18n",
|
"prebuild": "npm run bakei18n",
|
||||||
"build": "node esbuild.config.mjs production",
|
"build": "node esbuild.config.mjs production",
|
||||||
"buildDev": "node esbuild.config.mjs dev",
|
"buildDev": "node esbuild.config.mjs dev",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"pretty": "npm run prettyNoWrite -- --write --log-level error",
|
"pretty": "npm run prettyNoWrite -- --write --log-level error",
|
||||||
"prettyCheck": "npm run prettyNoWrite -- --check",
|
"prettyCheck": "npm run prettyNoWrite -- --check",
|
||||||
"prettyNoWrite": "prettier --config ./.prettierrc \"**/*.js\" \"**/*.ts\" \"**/*.json\" ",
|
"prettyNoWrite": "prettier --config ./.prettierrc \"**/*.js\" \"**/*.ts\" \"**/*.json\" ",
|
||||||
"check": "npm run lint && npm run svelte-check && npm run tsc-check",
|
"check": "npm run lint && npm run svelte-check",
|
||||||
"unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/"
|
"unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@@ -34,7 +34,9 @@
|
|||||||
"@eslint/compat": "^1.2.7",
|
"@eslint/compat": "^1.2.7",
|
||||||
"@eslint/eslintrc": "^3.3.0",
|
"@eslint/eslintrc": "^3.3.0",
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.21.0",
|
||||||
"@tsconfig/svelte": "^5.0.4",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
|
"@tsconfig/svelte": "^5.0.5",
|
||||||
|
"@types/deno": "^2.3.0",
|
||||||
"@types/diff-match-patch": "^1.0.36",
|
"@types/diff-match-patch": "^1.0.36",
|
||||||
"@types/node": "^22.13.8",
|
"@types/node": "^22.13.8",
|
||||||
"@types/pouchdb": "^6.4.2",
|
"@types/pouchdb": "^6.4.2",
|
||||||
@@ -45,14 +47,15 @@
|
|||||||
"@types/pouchdb-mapreduce": "^6.1.10",
|
"@types/pouchdb-mapreduce": "^6.1.10",
|
||||||
"@types/pouchdb-replication": "^6.4.7",
|
"@types/pouchdb-replication": "^6.4.7",
|
||||||
"@types/transform-pouch": "^1.0.6",
|
"@types/transform-pouch": "^1.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "8.25.0",
|
"@typescript-eslint/eslint-plugin": "8.46.2",
|
||||||
"@typescript-eslint/parser": "8.25.0",
|
"@typescript-eslint/parser": "8.46.2",
|
||||||
"builtin-modules": "5.0.0",
|
"builtin-modules": "5.0.0",
|
||||||
"esbuild": "0.25.0",
|
"esbuild": "0.25.0",
|
||||||
"esbuild-svelte": "^0.9.0",
|
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||||
"eslint": "^9.21.0",
|
"esbuild-svelte": "^0.9.3",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint": "^9.38.0",
|
||||||
"eslint-plugin-svelte": "^3.0.2",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
|
"eslint-plugin-svelte": "^3.12.4",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"glob": "^11.0.3",
|
"glob": "^11.0.3",
|
||||||
"obsidian": "^1.8.7",
|
"obsidian": "^1.8.7",
|
||||||
@@ -70,15 +73,15 @@
|
|||||||
"pouchdb-replication": "^9.0.0",
|
"pouchdb-replication": "^9.0.0",
|
||||||
"pouchdb-utils": "^9.0.0",
|
"pouchdb-utils": "^9.0.0",
|
||||||
"prettier": "3.5.2",
|
"prettier": "3.5.2",
|
||||||
"svelte": "5.28.6",
|
"svelte": "5.41.1",
|
||||||
|
"svelte-check": "^4.3.3",
|
||||||
"svelte-preprocess": "^6.0.3",
|
"svelte-preprocess": "^6.0.3",
|
||||||
"terser": "^5.39.0",
|
"terser": "^5.39.0",
|
||||||
"transform-pouch": "^2.0.0",
|
"transform-pouch": "^2.0.0",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"tsx": "^4.19.4",
|
"tsx": "^4.20.6",
|
||||||
"typescript": "5.7.3",
|
"typescript": "5.9.3",
|
||||||
"yaml": "^2.8.0",
|
"yaml": "^2.8.0"
|
||||||
"@types/deno": "^2.3.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.808.0",
|
"@aws-sdk/client-s3": "^3.808.0",
|
||||||
@@ -88,14 +91,12 @@
|
|||||||
"@smithy/protocol-http": "^5.1.0",
|
"@smithy/protocol-http": "^5.1.0",
|
||||||
"@smithy/querystring-builder": "^4.0.2",
|
"@smithy/querystring-builder": "^4.0.2",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
"esbuild-plugin-inline-worker": "^0.1.1",
|
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"minimatch": "^10.0.1",
|
"minimatch": "^10.0.2",
|
||||||
"octagonal-wheels": "^0.1.37",
|
"octagonal-wheels": "^0.1.42",
|
||||||
"qrcode-generator": "^1.4.4",
|
"qrcode-generator": "^1.4.4",
|
||||||
"svelte-check": "^4.1.7",
|
"trystero": "^0.22.0",
|
||||||
"trystero": "^0.21.5",
|
|
||||||
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ export const OpenKeyValueDatabase = async (dbKey: string): Promise<KeyValueDatab
|
|||||||
}
|
}
|
||||||
const storeKey = dbKey;
|
const storeKey = dbKey;
|
||||||
const dbPromise = openDB(dbKey, 1, {
|
const dbPromise = openDB(dbKey, 1, {
|
||||||
upgrade(db) {
|
upgrade(db, _oldVersion, _newVersion, _transaction, _event) {
|
||||||
db.createObjectStore(storeKey);
|
return db.createObjectStore(storeKey);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const db = await dbPromise;
|
const db = await dbPromise;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const EVENT_REQUEST_OPEN_P2P = "request-open-p2p";
|
|||||||
export const EVENT_REQUEST_CLOSE_P2P = "request-close-p2p";
|
export const EVENT_REQUEST_CLOSE_P2P = "request-close-p2p";
|
||||||
|
|
||||||
export const EVENT_REQUEST_RUN_DOCTOR = "request-run-doctor";
|
export const EVENT_REQUEST_RUN_DOCTOR = "request-run-doctor";
|
||||||
|
export const EVENT_REQUEST_RUN_FIX_INCOMPLETE = "request-run-fix-incomplete";
|
||||||
|
|
||||||
// export const EVENT_FILE_CHANGED = "file-changed";
|
// export const EVENT_FILE_CHANGED = "file-changed";
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ declare global {
|
|||||||
[EVENT_REQUEST_COPY_SETUP_URI]: undefined;
|
[EVENT_REQUEST_COPY_SETUP_URI]: undefined;
|
||||||
[EVENT_REQUEST_SHOW_SETUP_QR]: undefined;
|
[EVENT_REQUEST_SHOW_SETUP_QR]: undefined;
|
||||||
[EVENT_REQUEST_RUN_DOCTOR]: string;
|
[EVENT_REQUEST_RUN_DOCTOR]: string;
|
||||||
|
[EVENT_REQUEST_RUN_FIX_INCOMPLETE]: undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PersistentMap } from "../lib/src/dataobject/PersistentMap.ts";
|
import { PersistentMap } from "octagonal-wheels/dataobject/PersistentMap";
|
||||||
|
|
||||||
export let sameChangePairs: PersistentMap<number[]>;
|
export let sameChangePairs: PersistentMap<number[]>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { type PluginManifest, TFile } from "../deps.ts";
|
import { type PluginManifest, TFile } from "../deps.ts";
|
||||||
import {
|
import { type DatabaseEntry, type EntryBody, type FilePath } from "../lib/src/common/types.ts";
|
||||||
type DatabaseEntry,
|
export type { CacheData, FileEventItem } from "../lib/src/common/types.ts";
|
||||||
type EntryBody,
|
|
||||||
type FilePath,
|
|
||||||
type UXFileInfoStub,
|
|
||||||
type UXInternalFileInfoStub,
|
|
||||||
} from "../lib/src/common/types.ts";
|
|
||||||
|
|
||||||
export interface PluginDataEntry extends DatabaseEntry {
|
export interface PluginDataEntry extends DatabaseEntry {
|
||||||
deviceVaultName: string;
|
deviceVaultName: string;
|
||||||
@@ -54,23 +49,6 @@ export type queueItem = {
|
|||||||
warned?: boolean;
|
warned?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CacheData = string | ArrayBuffer;
|
|
||||||
export type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "INTERNAL";
|
|
||||||
export type FileEventArgs = {
|
|
||||||
file: UXFileInfoStub | UXInternalFileInfoStub;
|
|
||||||
cache?: CacheData;
|
|
||||||
oldPath?: string;
|
|
||||||
ctx?: any;
|
|
||||||
};
|
|
||||||
export type FileEventItem = {
|
|
||||||
type: FileEventType;
|
|
||||||
args: FileEventArgs;
|
|
||||||
key: string;
|
|
||||||
skipBatchWait?: boolean;
|
|
||||||
cancelled?: boolean;
|
|
||||||
batched?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hidden items (Now means `chunk`)
|
// Hidden items (Now means `chunk`)
|
||||||
export const CHeader = "h:";
|
export const CHeader = "h:";
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { promiseWithResolver, type PromiseWithResolvers } from "octagonal-wheels
|
|||||||
import { AuthorizationHeaderGenerator } from "../lib/src/replication/httplib.ts";
|
import { AuthorizationHeaderGenerator } from "../lib/src/replication/httplib.ts";
|
||||||
import type { KeyValueDatabase } from "../lib/src/interfaces/KeyValueDatabase.ts";
|
import type { KeyValueDatabase } from "../lib/src/interfaces/KeyValueDatabase.ts";
|
||||||
|
|
||||||
export { scheduleTask, cancelTask, cancelAllTasks } from "../lib/src/concurrency/task.ts";
|
export { scheduleTask, cancelTask, cancelAllTasks } from "octagonal-wheels/concurrency/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 "/".
|
||||||
@@ -189,7 +189,7 @@ export class PeriodicProcessor {
|
|||||||
() =>
|
() =>
|
||||||
fireAndForget(async () => {
|
fireAndForget(async () => {
|
||||||
await this.process();
|
await this.process();
|
||||||
if (this._plugin.$$isUnloaded()) {
|
if (this._plugin.services?.appLifecycle?.hasUnloaded()) {
|
||||||
this.disable();
|
this.disable();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import {
|
|||||||
} from "../../lib/src/common/utils.ts";
|
} from "../../lib/src/common/utils.ts";
|
||||||
import { digestHash } from "../../lib/src/string_and_binary/hash.ts";
|
import { digestHash } from "../../lib/src/string_and_binary/hash.ts";
|
||||||
import { arrayBufferToBase64, decodeBinary, readString } from "../../lib/src/string_and_binary/convert.ts";
|
import { arrayBufferToBase64, decodeBinary, readString } from "../../lib/src/string_and_binary/convert.ts";
|
||||||
import { serialized, shareRunningResult } from "../../lib/src/concurrency/lock.ts";
|
import { serialized, shareRunningResult } from "octagonal-wheels/concurrency/lock";
|
||||||
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
|
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
|
||||||
import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
|
import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
|
||||||
import {
|
import {
|
||||||
@@ -62,20 +62,29 @@ import {
|
|||||||
scheduleTask,
|
scheduleTask,
|
||||||
} from "../../common/utils.ts";
|
} from "../../common/utils.ts";
|
||||||
import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts";
|
import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts";
|
||||||
import { QueueProcessor } from "../../lib/src/concurrency/processor.ts";
|
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||||
import { pluginScanningCount } from "../../lib/src/mock_and_interop/stores.ts";
|
import { pluginScanningCount } from "../../lib/src/mock_and_interop/stores.ts";
|
||||||
import type ObsidianLiveSyncPlugin from "../../main.ts";
|
import type ObsidianLiveSyncPlugin from "../../main.ts";
|
||||||
import { base64ToArrayBuffer, base64ToString } from "octagonal-wheels/binary/base64";
|
import { base64ToArrayBuffer, base64ToString } from "octagonal-wheels/binary/base64";
|
||||||
import { ConflictResolveModal } from "../../modules/features/InteractiveConflictResolving/ConflictResolveModal.ts";
|
import { ConflictResolveModal } from "../../modules/features/InteractiveConflictResolving/ConflictResolveModal.ts";
|
||||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||||
import type { IObsidianModule } from "../../modules/AbstractObsidianModule.ts";
|
|
||||||
import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "../../common/events.ts";
|
import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "../../common/events.ts";
|
||||||
import { PluginDialogModal } from "./PluginDialogModal.ts";
|
import { PluginDialogModal } from "./PluginDialogModal.ts";
|
||||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||||
|
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
const d = "\u200b";
|
const d = "\u200b";
|
||||||
const d2 = "\n";
|
const d2 = "\n";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface OPTIONAL_SYNC_FEATURES {
|
||||||
|
DISABLE: "DISABLE";
|
||||||
|
CUSTOMIZE: "CUSTOMIZE";
|
||||||
|
DISABLE_CUSTOM: "DISABLE_CUSTOM";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function serialize(data: PluginDataEx): string {
|
function serialize(data: PluginDataEx): string {
|
||||||
// For higher performance, create custom plug-in data strings.
|
// For higher performance, create custom plug-in data strings.
|
||||||
// Self-hosted LiveSync uses `\n` to split chunks. Therefore, grouping together those with similar entropy would work nicely.
|
// Self-hosted LiveSync uses `\n` to split chunks. Therefore, grouping together those with similar entropy would work nicely.
|
||||||
@@ -384,7 +393,7 @@ export type PluginDataEx = {
|
|||||||
mtime: number;
|
mtime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
export class ConfigSync extends LiveSyncCommands {
|
||||||
constructor(plugin: ObsidianLiveSyncPlugin) {
|
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||||
super(plugin);
|
super(plugin);
|
||||||
pluginScanningCount.onChanged((e) => {
|
pluginScanningCount.onChanged((e) => {
|
||||||
@@ -402,7 +411,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
get useSyncPluginEtc() {
|
get useSyncPluginEtc() {
|
||||||
return this.plugin.settings.usePluginEtc;
|
return this.plugin.settings.usePluginEtc;
|
||||||
}
|
}
|
||||||
_isThisModuleEnabled() {
|
isThisModuleEnabled() {
|
||||||
return this.plugin.settings.usePluginSync;
|
return this.plugin.settings.usePluginSync;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,7 +420,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
|
|
||||||
pluginList: IPluginDataExDisplay[] = [];
|
pluginList: IPluginDataExDisplay[] = [];
|
||||||
showPluginSyncModal() {
|
showPluginSyncModal() {
|
||||||
if (!this._isThisModuleEnabled()) {
|
if (!this.isThisModuleEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.pluginDialog) {
|
if (this.pluginDialog) {
|
||||||
@@ -482,8 +491,8 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
// Idea non-filter option?
|
// Idea non-filter option?
|
||||||
return this.getFileCategory(filePath) != "";
|
return this.getFileCategory(filePath) != "";
|
||||||
}
|
}
|
||||||
async $everyOnDatabaseInitialized(showNotice: boolean) {
|
private async _everyOnDatabaseInitialized(showNotice: boolean) {
|
||||||
if (!this._isThisModuleEnabled()) return true;
|
if (!this.isThisModuleEnabled()) return true;
|
||||||
try {
|
try {
|
||||||
this._log("Scanning customizations...");
|
this._log("Scanning customizations...");
|
||||||
await this.scanAllConfigFiles(showNotice);
|
await this.scanAllConfigFiles(showNotice);
|
||||||
@@ -494,16 +503,16 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async $everyBeforeReplicate(showNotice: boolean) {
|
async _everyBeforeReplicate(showNotice: boolean) {
|
||||||
if (!this._isThisModuleEnabled()) return true;
|
if (!this.isThisModuleEnabled()) return true;
|
||||||
if (this.settings.autoSweepPlugins) {
|
if (this.settings.autoSweepPlugins) {
|
||||||
await this.scanAllConfigFiles(showNotice);
|
await this.scanAllConfigFiles(showNotice);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async $everyOnResumeProcess(): Promise<boolean> {
|
async _everyOnResumeProcess(): Promise<boolean> {
|
||||||
if (!this._isThisModuleEnabled()) return true;
|
if (!this.isThisModuleEnabled()) return true;
|
||||||
if (this._isMainSuspended()) {
|
if (this._isMainSuspended()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -517,9 +526,9 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
$everyAfterResumeProcess(): Promise<boolean> {
|
_everyAfterResumeProcess(): Promise<boolean> {
|
||||||
const q = activeDocument.querySelector(`.livesync-ribbon-showcustom`);
|
const q = activeDocument.querySelector(`.livesync-ribbon-showcustom`);
|
||||||
q?.toggleClass("sls-hidden", !this._isThisModuleEnabled());
|
q?.toggleClass("sls-hidden", !this.isThisModuleEnabled());
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
async reloadPluginList(showMessage: boolean) {
|
async reloadPluginList(showMessage: boolean) {
|
||||||
@@ -633,7 +642,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
).startPipeline();
|
).startPipeline();
|
||||||
|
|
||||||
filenameToUnifiedKey(path: string, termOverRide?: string) {
|
filenameToUnifiedKey(path: string, termOverRide?: string) {
|
||||||
const term = termOverRide || this.plugin.$$getDeviceAndVaultName();
|
const term = termOverRide || this.services.setting.getDeviceAndVaultName();
|
||||||
const category = this.getFileCategory(path);
|
const category = this.getFileCategory(path);
|
||||||
const name =
|
const name =
|
||||||
category == "CONFIG" || category == "SNIPPET"
|
category == "CONFIG" || category == "SNIPPET"
|
||||||
@@ -645,7 +654,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
filenameWithUnifiedKey(path: string, termOverRide?: string) {
|
filenameWithUnifiedKey(path: string, termOverRide?: string) {
|
||||||
const term = termOverRide || this.plugin.$$getDeviceAndVaultName();
|
const term = termOverRide || this.services.setting.getDeviceAndVaultName();
|
||||||
const category = this.getFileCategory(path);
|
const category = this.getFileCategory(path);
|
||||||
const name =
|
const name =
|
||||||
category == "CONFIG" || category == "SNIPPET" ? path.split("/").slice(-1)[0] : path.split("/").slice(-2)[0];
|
category == "CONFIG" || category == "SNIPPET" ? path.split("/").slice(-1)[0] : path.split("/").slice(-2)[0];
|
||||||
@@ -654,7 +663,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unifiedKeyPrefixOfTerminal(termOverRide?: string) {
|
unifiedKeyPrefixOfTerminal(termOverRide?: string) {
|
||||||
const term = termOverRide || this.plugin.$$getDeviceAndVaultName();
|
const term = termOverRide || this.services.setting.getDeviceAndVaultName();
|
||||||
return `${ICXHeader}${term}/` as FilePathWithPrefix;
|
return `${ICXHeader}${term}/` as FilePathWithPrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -831,7 +840,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
const v2Path = (prefixPath + relativeFilename) as FilePathWithPrefix;
|
const v2Path = (prefixPath + relativeFilename) as FilePathWithPrefix;
|
||||||
// console.warn(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`);
|
// console.warn(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`);
|
||||||
this._log(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`, LOG_LEVEL_VERBOSE);
|
this._log(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`, LOG_LEVEL_VERBOSE);
|
||||||
const newId = await this.plugin.$$path2id(v2Path);
|
const newId = await this.services.path.path2id(v2Path);
|
||||||
// const buf =
|
// const buf =
|
||||||
|
|
||||||
const data = createBlob([DUMMY_HEAD, DUMMY_END, ...getDocDataAsArray(f.data)]);
|
const data = createBlob([DUMMY_HEAD, DUMMY_END, ...getDocDataAsArray(f.data)]);
|
||||||
@@ -861,7 +870,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise<void> {
|
async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise<void> {
|
||||||
if (!this._isThisModuleEnabled()) {
|
if (!this.isThisModuleEnabled()) {
|
||||||
this.pluginScanProcessor.clearQueue();
|
this.pluginScanProcessor.clearQueue();
|
||||||
this.pluginList = [];
|
this.pluginList = [];
|
||||||
pluginList.set(this.pluginList);
|
pluginList.set(this.pluginList);
|
||||||
@@ -999,7 +1008,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
await this.plugin.storageAccess.ensureDir(path);
|
await this.plugin.storageAccess.ensureDir(path);
|
||||||
// If the content has applied, modified time will be updated to the current time.
|
// If the content has applied, modified time will be updated to the current time.
|
||||||
await this.plugin.storageAccess.writeHiddenFileAuto(path, content);
|
await this.plugin.storageAccess.writeHiddenFileAuto(path, content);
|
||||||
await this.storeCustomisationFileV2(path, this.plugin.$$getDeviceAndVaultName());
|
await this.storeCustomisationFileV2(path, this.services.setting.getDeviceAndVaultName());
|
||||||
} else {
|
} else {
|
||||||
const files = data.files;
|
const files = data.files;
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
@@ -1042,7 +1051,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
await this.plugin.storageAccess.writeHiddenFileAuto(path, content, stat);
|
await this.plugin.storageAccess.writeHiddenFileAuto(path, content, stat);
|
||||||
}
|
}
|
||||||
this._log(`Applied ${f.filename} of ${data.displayName || data.name}..`);
|
this._log(`Applied ${f.filename} of ${data.displayName || data.name}..`);
|
||||||
await this.storeCustomisationFileV2(path, this.plugin.$$getDeviceAndVaultName());
|
await this.storeCustomisationFileV2(path, this.services.setting.getDeviceAndVaultName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@@ -1114,7 +1123,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (data.category == "CONFIG") {
|
} else if (data.category == "CONFIG") {
|
||||||
this.plugin.$$askReload();
|
this.services.appLifecycle.askRestart();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@@ -1157,15 +1166,15 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async $anyModuleParsedReplicationResultItem(docs: PouchDB.Core.ExistingDocument<EntryDoc>) {
|
async _anyModuleParsedReplicationResultItem(docs: PouchDB.Core.ExistingDocument<EntryDoc>) {
|
||||||
if (!docs._id.startsWith(ICXHeader)) return undefined;
|
if (!docs._id.startsWith(ICXHeader)) return false;
|
||||||
if (this._isThisModuleEnabled()) {
|
if (this.isThisModuleEnabled()) {
|
||||||
await this.updatePluginList(
|
await this.updatePluginList(
|
||||||
false,
|
false,
|
||||||
(docs as AnyEntry).path ? (docs as AnyEntry).path : this.getPath(docs as AnyEntry)
|
(docs as AnyEntry).path ? (docs as AnyEntry).path : this.getPath(docs as AnyEntry)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this._isThisModuleEnabled() && this.plugin.settings.notifyPluginOrSettingUpdated) {
|
if (this.isThisModuleEnabled() && this.plugin.settings.notifyPluginOrSettingUpdated) {
|
||||||
if (!this.pluginDialog || (this.pluginDialog && !this.pluginDialog.isOpened())) {
|
if (!this.pluginDialog || (this.pluginDialog && !this.pluginDialog.isOpened())) {
|
||||||
const fragment = createFragment((doc) => {
|
const fragment = createFragment((doc) => {
|
||||||
doc.createEl("span", undefined, (a) => {
|
doc.createEl("span", undefined, (a) => {
|
||||||
@@ -1205,11 +1214,11 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async $everyRealizeSettingSyncMode(): Promise<boolean> {
|
async _everyRealizeSettingSyncMode(): Promise<boolean> {
|
||||||
this.periodicPluginSweepProcessor?.disable();
|
this.periodicPluginSweepProcessor?.disable();
|
||||||
if (!this._isMainReady) return true;
|
if (!this._isMainReady) return true;
|
||||||
if (!this._isMainSuspended()) return true;
|
if (!this._isMainSuspended()) return true;
|
||||||
if (!this._isThisModuleEnabled()) return true;
|
if (!this.isThisModuleEnabled()) return true;
|
||||||
if (this.settings.autoSweepPlugins) {
|
if (this.settings.autoSweepPlugins) {
|
||||||
await this.scanAllConfigFiles(false);
|
await this.scanAllConfigFiles(false);
|
||||||
}
|
}
|
||||||
@@ -1345,7 +1354,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
async storeCustomizationFiles(path: FilePath, termOverRide?: string) {
|
async storeCustomizationFiles(path: FilePath, termOverRide?: string) {
|
||||||
const term = termOverRide || this.plugin.$$getDeviceAndVaultName();
|
const term = termOverRide || this.services.setting.getDeviceAndVaultName();
|
||||||
if (term == "") {
|
if (term == "") {
|
||||||
this._log("We have to configure the device name", LOG_LEVEL_NOTICE);
|
this._log("We have to configure the device name", LOG_LEVEL_NOTICE);
|
||||||
return;
|
return;
|
||||||
@@ -1488,14 +1497,14 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async $anyProcessOptionalFileEvent(path: FilePath): Promise<boolean | undefined> {
|
async _anyProcessOptionalFileEvent(path: FilePath): Promise<boolean> {
|
||||||
return await this.watchVaultRawEventsAsync(path);
|
return await this.watchVaultRawEventsAsync(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
async watchVaultRawEventsAsync(path: FilePath) {
|
async watchVaultRawEventsAsync(path: FilePath) {
|
||||||
if (!this._isMainReady) return false;
|
if (!this._isMainReady) return false;
|
||||||
if (this._isMainSuspended()) return false;
|
if (this._isMainSuspended()) return false;
|
||||||
if (!this._isThisModuleEnabled()) return false;
|
if (!this.isThisModuleEnabled()) return false;
|
||||||
// if (!this.isTargetPath(path)) return false;
|
// if (!this.isTargetPath(path)) return false;
|
||||||
const stat = await this.plugin.storageAccess.statHidden(path);
|
const stat = await this.plugin.storageAccess.statHidden(path);
|
||||||
// Make sure that target is a file.
|
// Make sure that target is a file.
|
||||||
@@ -1535,7 +1544,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
await shareRunningResult("scanAllConfigFiles", async () => {
|
await shareRunningResult("scanAllConfigFiles", async () => {
|
||||||
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||||
this._log("Scanning customizing files.", logLevel, "scan-all-config");
|
this._log("Scanning customizing files.", logLevel, "scan-all-config");
|
||||||
const term = this.plugin.$$getDeviceAndVaultName();
|
const term = this.services.setting.getDeviceAndVaultName();
|
||||||
if (term == "") {
|
if (term == "") {
|
||||||
this._log("We have to configure the device name", LOG_LEVEL_NOTICE);
|
this._log("We have to configure the device name", LOG_LEVEL_NOTICE);
|
||||||
return;
|
return;
|
||||||
@@ -1673,11 +1682,14 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
return filenames as FilePath[];
|
return filenames as FilePath[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async $allAskUsingOptionalSyncFeature(opt: { enableFetch?: boolean; enableOverwrite?: boolean }): Promise<boolean> {
|
private async _allAskUsingOptionalSyncFeature(opt: {
|
||||||
await this._askHiddenFileConfiguration(opt);
|
enableFetch?: boolean;
|
||||||
|
enableOverwrite?: boolean;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
await this.__askHiddenFileConfiguration(opt);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async _askHiddenFileConfiguration(opt: { enableFetch?: boolean; enableOverwrite?: boolean }) {
|
private async __askHiddenFileConfiguration(opt: { enableFetch?: boolean; enableOverwrite?: boolean }) {
|
||||||
const message = `Would you like to enable **Customization sync**?
|
const message = `Would you like to enable **Customization sync**?
|
||||||
|
|
||||||
> [!DETAILS]-
|
> [!DETAILS]-
|
||||||
@@ -1707,7 +1719,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise<boolean | "newer"> {
|
_anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise<boolean | "newer"> {
|
||||||
if (isPluginMetadata(path)) {
|
if (isPluginMetadata(path)) {
|
||||||
return Promise.resolve("newer");
|
return Promise.resolve("newer");
|
||||||
}
|
}
|
||||||
@@ -1717,7 +1729,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
$allSuspendExtraSync(): Promise<boolean> {
|
private _allSuspendExtraSync(): Promise<boolean> {
|
||||||
if (this.plugin.settings.usePluginSync || this.plugin.settings.autoSweepPlugins) {
|
if (this.plugin.settings.usePluginSync || this.plugin.settings.autoSweepPlugins) {
|
||||||
this._log(
|
this._log(
|
||||||
"Customisation sync have been temporarily disabled. Please enable them after the fetching, if you need them.",
|
"Customisation sync have been temporarily disabled. Please enable them after the fetching, if you need them.",
|
||||||
@@ -1729,10 +1741,11 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $anyConfigureOptionalSyncFeature(mode: "CUSTOMIZE" | "DISABLE" | "DISABLE_CUSTOM") {
|
private async _allConfigureOptionalSyncFeature(mode: keyof OPTIONAL_SYNC_FEATURES) {
|
||||||
await this.configureHiddenFileSync(mode);
|
await this.configureHiddenFileSync(mode);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
async configureHiddenFileSync(mode: "CUSTOMIZE" | "DISABLE" | "DISABLE_CUSTOM") {
|
async configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES) {
|
||||||
if (mode == "DISABLE") {
|
if (mode == "DISABLE") {
|
||||||
this.plugin.settings.usePluginSync = false;
|
this.plugin.settings.usePluginSync = false;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
@@ -1740,7 +1753,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mode == "CUSTOMIZE") {
|
if (mode == "CUSTOMIZE") {
|
||||||
if (!this.plugin.$$getDeviceAndVaultName()) {
|
if (!this.services.setting.getDeviceAndVaultName()) {
|
||||||
let name = await this.plugin.confirm.askString("Device name", "Please set this device name", `desktop`);
|
let name = await this.plugin.confirm.askString("Device name", "Please set this device name", `desktop`);
|
||||||
if (!name) {
|
if (!name) {
|
||||||
if (Platform.isAndroidApp) {
|
if (Platform.isAndroidApp) {
|
||||||
@@ -1764,7 +1777,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
}
|
}
|
||||||
name = name + Math.random().toString(36).slice(-4);
|
name = name + Math.random().toString(36).slice(-4);
|
||||||
}
|
}
|
||||||
this.plugin.$$setDeviceAndVaultName(name);
|
this.services.setting.setDeviceAndVaultName(name);
|
||||||
}
|
}
|
||||||
this.plugin.settings.usePluginSync = true;
|
this.plugin.settings.usePluginSync = true;
|
||||||
this.plugin.settings.useAdvancedMode = true;
|
this.plugin.settings.useAdvancedMode = true;
|
||||||
@@ -1789,4 +1802,17 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
|||||||
}
|
}
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
|
||||||
|
services.fileProcessing.handleOptionalFileEvent(this._anyProcessOptionalFileEvent.bind(this));
|
||||||
|
services.conflict.handleGetOptionalConflictCheckMethod(this._anyGetOptionalConflictCheckMethod.bind(this));
|
||||||
|
services.replication.handleProcessVirtualDocuments(this._anyModuleParsedReplicationResultItem.bind(this));
|
||||||
|
services.setting.handleOnRealiseSetting(this._everyRealizeSettingSyncMode.bind(this));
|
||||||
|
services.appLifecycle.handleOnResuming(this._everyOnResumeProcess.bind(this));
|
||||||
|
services.appLifecycle.handleOnResumed(this._everyAfterResumeProcess.bind(this));
|
||||||
|
services.replication.handleBeforeReplicate(this._everyBeforeReplicate.bind(this));
|
||||||
|
services.databaseEvents.handleDatabaseInitialised(this._everyOnDatabaseInitialized.bind(this));
|
||||||
|
services.setting.handleSuspendExtraSync(this._allSuspendExtraSync.bind(this));
|
||||||
|
services.setting.handleSuggestOptionalFeatures(this._allAskUsingOptionalSyncFeature.bind(this));
|
||||||
|
services.setting.handleEnableOptionalFeature(this._allConfigureOptionalSyncFeature.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
export let plugin: ObsidianLiveSyncPlugin;
|
export let plugin: ObsidianLiveSyncPlugin;
|
||||||
|
|
||||||
$: hideNotApplicable = false;
|
$: hideNotApplicable = false;
|
||||||
$: thisTerm = plugin.$$getDeviceAndVaultName();
|
$: thisTerm = plugin.services.setting.getDeviceAndVaultName();
|
||||||
|
|
||||||
const addOn = plugin.getAddOn(ConfigSync.name) as ConfigSync;
|
const addOn = plugin.getAddOn(ConfigSync.name) as ConfigSync;
|
||||||
if (!addOn) {
|
if (!addOn) {
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
await requestUpdate();
|
await requestUpdate();
|
||||||
}
|
}
|
||||||
async function replicate() {
|
async function replicate() {
|
||||||
await plugin.$$replicate(true);
|
await plugin.services.replication.replicate(true);
|
||||||
}
|
}
|
||||||
function selectAllNewest(selectMode: boolean) {
|
function selectAllNewest(selectMode: boolean) {
|
||||||
selectNewestPulse++;
|
selectNewestPulse++;
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
plugin.settings.pluginSyncExtendedSetting[key].files = files;
|
plugin.settings.pluginSyncExtendedSetting[key].files = files;
|
||||||
plugin.settings.pluginSyncExtendedSetting[key].mode = mode;
|
plugin.settings.pluginSyncExtendedSetting[key].mode = mode;
|
||||||
}
|
}
|
||||||
plugin.$$saveSettingData();
|
plugin.services.setting.saveSettingData();
|
||||||
}
|
}
|
||||||
function getIcon(mode: SYNC_MODE) {
|
function getIcon(mode: SYNC_MODE) {
|
||||||
if (mode in ICONS) {
|
if (mode in ICONS) {
|
||||||
|
|||||||
@@ -45,18 +45,26 @@ import {
|
|||||||
BASE_IS_NEW,
|
BASE_IS_NEW,
|
||||||
EVEN,
|
EVEN,
|
||||||
} from "../../common/utils.ts";
|
} from "../../common/utils.ts";
|
||||||
import { serialized, skipIfDuplicated } from "../../lib/src/concurrency/lock.ts";
|
import { serialized, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
|
||||||
import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts";
|
import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts";
|
||||||
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
|
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
|
||||||
import { addPrefix, stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
|
import { addPrefix, stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
|
||||||
import { QueueProcessor } from "../../lib/src/concurrency/processor.ts";
|
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||||
import { hiddenFilesEventCount, hiddenFilesProcessingCount } from "../../lib/src/mock_and_interop/stores.ts";
|
import { hiddenFilesEventCount, hiddenFilesProcessingCount } from "../../lib/src/mock_and_interop/stores.ts";
|
||||||
import type { IObsidianModule } from "../../modules/AbstractObsidianModule.ts";
|
|
||||||
import { EVENT_SETTING_SAVED, eventHub } from "../../common/events.ts";
|
import { EVENT_SETTING_SAVED, eventHub } from "../../common/events.ts";
|
||||||
import type { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB.ts";
|
|
||||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce";
|
type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface OPTIONAL_SYNC_FEATURES {
|
||||||
|
FETCH: "FETCH";
|
||||||
|
OVERWRITE: "OVERWRITE";
|
||||||
|
MERGE: "MERGE";
|
||||||
|
DISABLE: "DISABLE";
|
||||||
|
DISABLE_HIDDEN: "DISABLE_HIDDEN";
|
||||||
|
}
|
||||||
|
}
|
||||||
function getComparingMTime(
|
function getComparingMTime(
|
||||||
doc: (MetaEntry | LoadedEntry | false) | UXFileInfo | UXStat | null | undefined,
|
doc: (MetaEntry | LoadedEntry | false) | UXFileInfo | UXStat | null | undefined,
|
||||||
includeDeleted = false
|
includeDeleted = false
|
||||||
@@ -72,21 +80,21 @@ function getComparingMTime(
|
|||||||
return doc.mtime ?? 0;
|
return doc.mtime ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule {
|
export class HiddenFileSync extends LiveSyncCommands {
|
||||||
_isThisModuleEnabled() {
|
isThisModuleEnabled() {
|
||||||
return this.plugin.settings.syncInternalFiles;
|
return this.plugin.settings.syncInternalFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
periodicInternalFileScanProcessor: PeriodicProcessor = new PeriodicProcessor(
|
periodicInternalFileScanProcessor: PeriodicProcessor = new PeriodicProcessor(
|
||||||
this.plugin,
|
this.plugin,
|
||||||
async () => this._isThisModuleEnabled() && this._isDatabaseReady() && (await this.scanAllStorageChanges(false))
|
async () => this.isThisModuleEnabled() && this._isDatabaseReady() && (await this.scanAllStorageChanges(false))
|
||||||
);
|
);
|
||||||
|
|
||||||
get kvDB() {
|
get kvDB() {
|
||||||
return this.plugin.kvDB;
|
return this.plugin.kvDB;
|
||||||
}
|
}
|
||||||
getConflictedDoc(path: FilePathWithPrefix, rev: string) {
|
getConflictedDoc(path: FilePathWithPrefix, rev: string) {
|
||||||
return this.plugin.localDatabase.getConflictedDoc(path, rev);
|
return this.plugin.managers.conflictManager.getConflictedDoc(path, rev);
|
||||||
}
|
}
|
||||||
onunload() {
|
onunload() {
|
||||||
this.periodicInternalFileScanProcessor?.disable();
|
this.periodicInternalFileScanProcessor?.disable();
|
||||||
@@ -132,14 +140,18 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule
|
|||||||
this.updateSettingCache();
|
this.updateSettingCache();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async $everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
// We cannot initialise autosaveCache because kvDB is not ready yet
|
||||||
|
// async _everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
||||||
|
// this._fileInfoLastProcessed = await autosaveCache(this.kvDB, "hidden-file-lastProcessed");
|
||||||
|
// this._databaseInfoLastProcessed = await autosaveCache(this.kvDB, "hidden-file-lastProcessed-database");
|
||||||
|
// this._fileInfoLastKnown = await autosaveCache(this.kvDB, "hidden-file-lastKnown");
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
private async _everyOnDatabaseInitialized(showNotice: boolean) {
|
||||||
this._fileInfoLastProcessed = await autosaveCache(this.kvDB, "hidden-file-lastProcessed");
|
this._fileInfoLastProcessed = await autosaveCache(this.kvDB, "hidden-file-lastProcessed");
|
||||||
this._databaseInfoLastProcessed = await autosaveCache(this.kvDB, "hidden-file-lastProcessed-database");
|
this._databaseInfoLastProcessed = await autosaveCache(this.kvDB, "hidden-file-lastProcessed-database");
|
||||||
this._fileInfoLastKnown = await autosaveCache(this.kvDB, "hidden-file-lastKnown");
|
this._fileInfoLastKnown = await autosaveCache(this.kvDB, "hidden-file-lastKnown");
|
||||||
return true;
|
if (this.isThisModuleEnabled()) {
|
||||||
}
|
|
||||||
async $everyOnDatabaseInitialized(showNotice: boolean) {
|
|
||||||
if (this._isThisModuleEnabled()) {
|
|
||||||
if (this._fileInfoLastProcessed.size == 0 && this._fileInfoLastProcessed.size == 0) {
|
if (this._fileInfoLastProcessed.size == 0 && this._fileInfoLastProcessed.size == 0) {
|
||||||
this._log(`No cache found. Performing startup scan.`, LOG_LEVEL_VERBOSE);
|
this._log(`No cache found. Performing startup scan.`, LOG_LEVEL_VERBOSE);
|
||||||
await this.performStartupScan(true);
|
await this.performStartupScan(true);
|
||||||
@@ -149,9 +161,9 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async $everyBeforeReplicate(showNotice: boolean) {
|
async _everyBeforeReplicate(showNotice: boolean) {
|
||||||
if (
|
if (
|
||||||
this._isThisModuleEnabled() &&
|
this.isThisModuleEnabled() &&
|
||||||
this._isDatabaseReady() &&
|
this._isDatabaseReady() &&
|
||||||
this.settings.syncInternalFilesBeforeReplication &&
|
this.settings.syncInternalFilesBeforeReplication &&
|
||||||
!this.settings.watchInternalFileChanges
|
!this.settings.watchInternalFileChanges
|
||||||
@@ -161,7 +173,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$everyOnloadAfterLoadSettings(): Promise<boolean> {
|
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
|
||||||
this.updateSettingCache();
|
this.updateSettingCache();
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
@@ -188,7 +200,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule
|
|||||||
isReady() {
|
isReady() {
|
||||||
if (!this._isMainReady) return false;
|
if (!this._isMainReady) return false;
|
||||||
if (this._isMainSuspended()) return false;
|
if (this._isMainSuspended()) return false;
|
||||||
if (!this._isThisModuleEnabled()) return false;
|
if (!this.isThisModuleEnabled()) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
shouldSkipFile = [] as FilePathWithPrefixLC[];
|
shouldSkipFile = [] as FilePathWithPrefixLC[];
|
||||||
@@ -197,26 +209,26 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule
|
|||||||
await this.applyOfflineChanges(showNotice);
|
await this.applyOfflineChanges(showNotice);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $everyOnResumeProcess(): Promise<boolean> {
|
async _everyOnResumeProcess(): Promise<boolean> {
|
||||||
this.periodicInternalFileScanProcessor?.disable();
|
this.periodicInternalFileScanProcessor?.disable();
|
||||||
if (this._isMainSuspended()) return true;
|
if (this._isMainSuspended()) return true;
|
||||||
if (this._isThisModuleEnabled()) {
|
if (this.isThisModuleEnabled()) {
|
||||||
await this.performStartupScan(false);
|
await this.performStartupScan(false);
|
||||||
}
|
}
|
||||||
this.periodicInternalFileScanProcessor.enable(
|
this.periodicInternalFileScanProcessor.enable(
|
||||||
this._isThisModuleEnabled() && this.settings.syncInternalFilesInterval
|
this.isThisModuleEnabled() && this.settings.syncInternalFilesInterval
|
||||||
? this.settings.syncInternalFilesInterval * 1000
|
? this.settings.syncInternalFilesInterval * 1000
|
||||||
: 0
|
: 0
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$everyRealizeSettingSyncMode(): Promise<boolean> {
|
_everyRealizeSettingSyncMode(): Promise<boolean> {
|
||||||
this.periodicInternalFileScanProcessor?.disable();
|
this.periodicInternalFileScanProcessor?.disable();
|
||||||
if (this._isMainSuspended()) return Promise.resolve(true);
|
if (this._isMainSuspended()) return Promise.resolve(true);
|
||||||
if (!this.plugin.$$isReady()) return Promise.resolve(true);
|
if (!this.services.appLifecycle.isReady()) return Promise.resolve(true);
|
||||||
this.periodicInternalFileScanProcessor.enable(
|
this.periodicInternalFileScanProcessor.enable(
|
||||||
this._isThisModuleEnabled() && this.settings.syncInternalFilesInterval
|
this.isThisModuleEnabled() && this.settings.syncInternalFilesInterval
|
||||||
? this.settings.syncInternalFilesInterval * 1000
|
? this.settings.syncInternalFilesInterval * 1000
|
||||||
: 0
|
: 0
|
||||||
);
|
);
|
||||||
@@ -227,13 +239,14 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule
|
|||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $anyProcessOptionalFileEvent(path: FilePath): Promise<boolean | undefined> {
|
async _anyProcessOptionalFileEvent(path: FilePath): Promise<boolean> {
|
||||||
if (this.isReady()) {
|
if (this.isReady()) {
|
||||||
return await this.trackStorageFileModification(path);
|
return (await this.trackStorageFileModification(path)) || false;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise<boolean | "newer"> {
|
_anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise<boolean | "newer"> {
|
||||||
if (isInternalMetadata(path)) {
|
if (isInternalMetadata(path)) {
|
||||||
this.queueConflictCheck(path);
|
this.queueConflictCheck(path);
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
@@ -241,12 +254,12 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule
|
|||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $anyProcessOptionalSyncFiles(doc: LoadedEntry): Promise<boolean | undefined> {
|
async _anyProcessOptionalSyncFiles(doc: LoadedEntry): Promise<boolean> {
|
||||||
if (isInternalMetadata(doc._id)) {
|
if (isInternalMetadata(doc._id)) {
|
||||||
if (this._isThisModuleEnabled()) {
|
if (this.isThisModuleEnabled()) {
|
||||||
//system file
|
//system file
|
||||||
const filename = getPath(doc);
|
const filename = getPath(doc);
|
||||||
if (await this.plugin.$$isTargetFile(filename)) {
|
if (await this.services.vault.isTargetFile(filename)) {
|
||||||
// this.procInternalFile(filename);
|
// this.procInternalFile(filename);
|
||||||
await this.processReplicationResult(doc);
|
await this.processReplicationResult(doc);
|
||||||
return true;
|
return true;
|
||||||
@@ -699,7 +712,7 @@ Offline Changed files: ${processFiles.length}`;
|
|||||||
revFrom._revs_info
|
revFrom._revs_info
|
||||||
?.filter((e) => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo)
|
?.filter((e) => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo)
|
||||||
.first()?.rev ?? "";
|
.first()?.rev ?? "";
|
||||||
const result = await this.plugin.localDatabase.mergeObject(
|
const result = await this.plugin.managers.conflictManager.mergeObject(
|
||||||
doc.path,
|
doc.path,
|
||||||
commonBase,
|
commonBase,
|
||||||
doc._rev,
|
doc._rev,
|
||||||
@@ -1091,14 +1104,14 @@ Offline Changed files: ${files.length}`;
|
|||||||
|
|
||||||
// If something changes left, notify for reloading Obsidian.
|
// If something changes left, notify for reloading Obsidian.
|
||||||
if (updatedFolders.indexOf(this.plugin.app.vault.configDir) >= 0) {
|
if (updatedFolders.indexOf(this.plugin.app.vault.configDir) >= 0) {
|
||||||
if (!this.plugin.$$isReloadingScheduled()) {
|
if (!this.services.appLifecycle.isReloadingScheduled()) {
|
||||||
this.plugin.confirm.askInPopup(
|
this.plugin.confirm.askInPopup(
|
||||||
`updated-any-hidden`,
|
`updated-any-hidden`,
|
||||||
`Some setting files have been modified\nPress {HERE} to schedule a reload of Obsidian, or press elsewhere to dismiss this message.`,
|
`Some setting files have been modified\nPress {HERE} to schedule a reload of Obsidian, or press elsewhere to dismiss this message.`,
|
||||||
(anchor) => {
|
(anchor) => {
|
||||||
anchor.text = "HERE";
|
anchor.text = "HERE";
|
||||||
anchor.addEventListener("click", () => {
|
anchor.addEventListener("click", () => {
|
||||||
this.plugin.$$scheduleAppReload();
|
this.services.appLifecycle.scheduleRestart();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -1318,7 +1331,7 @@ Offline Changed files: ${files.length}`;
|
|||||||
async storeInternalFileToDatabase(file: InternalFileInfo | UXFileInfo, forceWrite = false) {
|
async storeInternalFileToDatabase(file: InternalFileInfo | UXFileInfo, forceWrite = false) {
|
||||||
const storeFilePath = stripAllPrefixes(file.path as FilePath);
|
const storeFilePath = stripAllPrefixes(file.path as FilePath);
|
||||||
const storageFilePath = file.path;
|
const storageFilePath = file.path;
|
||||||
if (await this.plugin.$$isIgnoredByIgnoreFiles(storageFilePath)) {
|
if (await this.services.vault.isIgnoredByIgnoreFile(storageFilePath)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const prefixedFileName = addPrefix(storeFilePath, ICHeader);
|
const prefixedFileName = addPrefix(storeFilePath, ICHeader);
|
||||||
@@ -1372,7 +1385,7 @@ Offline Changed files: ${files.length}`;
|
|||||||
const displayFileName = filenameSrc;
|
const displayFileName = filenameSrc;
|
||||||
const prefixedFileName = addPrefix(storeFilePath, ICHeader);
|
const prefixedFileName = addPrefix(storeFilePath, ICHeader);
|
||||||
const mtime = new Date().getTime();
|
const mtime = new Date().getTime();
|
||||||
if (await this.plugin.$$isIgnoredByIgnoreFiles(storageFilePath)) {
|
if (await this.services.vault.isIgnoredByIgnoreFile(storageFilePath)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return await serialized("file-" + prefixedFileName, async () => {
|
return await serialized("file-" + prefixedFileName, async () => {
|
||||||
@@ -1432,7 +1445,7 @@ Offline Changed files: ${files.length}`;
|
|||||||
includeDeletion = true
|
includeDeletion = true
|
||||||
) {
|
) {
|
||||||
const prefixedFileName = addPrefix(storageFilePath, ICHeader);
|
const prefixedFileName = addPrefix(storageFilePath, ICHeader);
|
||||||
if (await this.plugin.$$isIgnoredByIgnoreFiles(storageFilePath)) {
|
if (await this.services.vault.isIgnoredByIgnoreFile(storageFilePath)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return await serialized("file-" + prefixedFileName, async () => {
|
return await serialized("file-" + prefixedFileName, async () => {
|
||||||
@@ -1479,7 +1492,7 @@ Offline Changed files: ${files.length}`;
|
|||||||
}
|
}
|
||||||
const deleted = metaOnDB.deleted || metaOnDB._deleted || false;
|
const deleted = metaOnDB.deleted || metaOnDB._deleted || false;
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
const result = await this._deleteFile(storageFilePath);
|
const result = await this.__deleteFile(storageFilePath);
|
||||||
if (result == "OK") {
|
if (result == "OK") {
|
||||||
this.updateLastProcessedDeletion(storageFilePath, metaOnDB);
|
this.updateLastProcessedDeletion(storageFilePath, metaOnDB);
|
||||||
return true;
|
return true;
|
||||||
@@ -1493,7 +1506,7 @@ Offline Changed files: ${files.length}`;
|
|||||||
if (fileOnDB === false) {
|
if (fileOnDB === false) {
|
||||||
throw new Error(`Failed to read file from database:${storageFilePath}`);
|
throw new Error(`Failed to read file from database:${storageFilePath}`);
|
||||||
}
|
}
|
||||||
const resultStat = await this._writeFile(storageFilePath, fileOnDB, force);
|
const resultStat = await this.__writeFile(storageFilePath, fileOnDB, force);
|
||||||
if (resultStat) {
|
if (resultStat) {
|
||||||
this.updateLastProcessed(storageFilePath, metaOnDB, resultStat);
|
this.updateLastProcessed(storageFilePath, metaOnDB, resultStat);
|
||||||
this.queueNotification(storageFilePath);
|
this.queueNotification(storageFilePath);
|
||||||
@@ -1526,7 +1539,7 @@ Offline Changed files: ${files.length}`;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _writeFile(storageFilePath: FilePath, fileOnDB: LoadedEntry, force: boolean): Promise<false | UXStat> {
|
async __writeFile(storageFilePath: FilePath, fileOnDB: LoadedEntry, force: boolean): Promise<false | UXStat> {
|
||||||
try {
|
try {
|
||||||
const statBefore = await this.plugin.storageAccess.statHidden(storageFilePath);
|
const statBefore = await this.plugin.storageAccess.statHidden(storageFilePath);
|
||||||
const isExist = statBefore != null;
|
const isExist = statBefore != null;
|
||||||
@@ -1565,7 +1578,7 @@ Offline Changed files: ${files.length}`;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _deleteFile(storageFilePath: FilePath): Promise<false | "OK" | "ALREADY"> {
|
async __deleteFile(storageFilePath: FilePath): Promise<false | "OK" | "ALREADY"> {
|
||||||
const result = await this.__removeFile(storageFilePath);
|
const result = await this.__removeFile(storageFilePath);
|
||||||
if (result === false) {
|
if (result === false) {
|
||||||
this._log(`STORAGE <x- DB: ${storageFilePath}: deleting (hidden) Failed`);
|
this._log(`STORAGE <x- DB: ${storageFilePath}: deleting (hidden) Failed`);
|
||||||
@@ -1582,11 +1595,11 @@ Offline Changed files: ${files.length}`;
|
|||||||
|
|
||||||
// <-- Database To Storage Functions
|
// <-- Database To Storage Functions
|
||||||
|
|
||||||
async $allAskUsingOptionalSyncFeature(opt: { enableFetch?: boolean; enableOverwrite?: boolean }) {
|
private async _allAskUsingOptionalSyncFeature(opt: { enableFetch?: boolean; enableOverwrite?: boolean }) {
|
||||||
await this._askHiddenFileConfiguration(opt);
|
await this.__askHiddenFileConfiguration(opt);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async _askHiddenFileConfiguration(opt: { enableFetch?: boolean; enableOverwrite?: boolean }) {
|
private async __askHiddenFileConfiguration(opt: { enableFetch?: boolean; enableOverwrite?: boolean }) {
|
||||||
const messageFetch = `${opt.enableFetch ? `> - Fetch: Use the files stored from other devices. Choose this option if you have already configured hidden file synchronization on those devices and wish to accept their files.\n` : ""}`;
|
const messageFetch = `${opt.enableFetch ? `> - Fetch: Use the files stored from other devices. Choose this option if you have already configured hidden file synchronization on those devices and wish to accept their files.\n` : ""}`;
|
||||||
const messageOverwrite = `${opt.enableOverwrite ? `> - Overwrite: Use the files from this device. Select this option if you want to overwrite the files stored on other devices.\n` : ""}`;
|
const messageOverwrite = `${opt.enableOverwrite ? `> - Overwrite: Use the files from this device. Select this option if you want to overwrite the files stored on other devices.\n` : ""}`;
|
||||||
const messageMerge = `> - Merge: Merge the files from this device with those on other devices. Choose this option if you wish to combine files from multiple sources.
|
const messageMerge = `> - Merge: Merge the files from this device with those on other devices. Choose this option if you wish to combine files from multiple sources.
|
||||||
@@ -1632,7 +1645,7 @@ ${messageFetch}${messageOverwrite}${messageMerge}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$allSuspendExtraSync(): Promise<boolean> {
|
private _allSuspendExtraSync(): Promise<boolean> {
|
||||||
if (this.plugin.settings.syncInternalFiles) {
|
if (this.plugin.settings.syncInternalFiles) {
|
||||||
this._log(
|
this._log(
|
||||||
"Hidden file synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.",
|
"Hidden file synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.",
|
||||||
@@ -1644,11 +1657,12 @@ ${messageFetch}${messageOverwrite}${messageMerge}
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --> Configuration handling
|
// --> Configuration handling
|
||||||
async $anyConfigureOptionalSyncFeature(mode: "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE" | "DISABLE_HIDDEN") {
|
private async _allConfigureOptionalSyncFeature(mode: keyof OPTIONAL_SYNC_FEATURES) {
|
||||||
await this.configureHiddenFileSync(mode);
|
await this.configureHiddenFileSync(mode);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async configureHiddenFileSync(mode: "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE" | "DISABLE_HIDDEN") {
|
async configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES) {
|
||||||
if (
|
if (
|
||||||
mode != "FETCH" &&
|
mode != "FETCH" &&
|
||||||
mode != "OVERWRITE" &&
|
mode != "OVERWRITE" &&
|
||||||
@@ -1718,7 +1732,7 @@ ${messageFetch}${messageOverwrite}${messageMerge}
|
|||||||
const result: InternalFileInfo[] = [];
|
const result: InternalFileInfo[] = [];
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
const w = await f;
|
const w = await f;
|
||||||
if (await this.plugin.$$isIgnoredByIgnoreFiles(w.path)) {
|
if (await this.services.vault.isIgnoredByIgnoreFile(w.path)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const mtime = w.stat?.mtime ?? 0;
|
const mtime = w.stat?.mtime ?? 0;
|
||||||
@@ -1756,7 +1770,7 @@ ${messageFetch}${messageOverwrite}${messageMerge}
|
|||||||
if (ignoreFilter && ignoreFilter.some((ee) => ee.test(file))) {
|
if (ignoreFilter && ignoreFilter.some((ee) => ee.test(file))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (await this.plugin.$$isIgnoredByIgnoreFiles(file)) continue;
|
if (await this.services.vault.isIgnoredByIgnoreFile(file)) continue;
|
||||||
files.push(file);
|
files.push(file);
|
||||||
}
|
}
|
||||||
L1: for (const v of w.folders) {
|
L1: for (const v of w.folders) {
|
||||||
@@ -1765,13 +1779,10 @@ ${messageFetch}${messageOverwrite}${messageMerge}
|
|||||||
continue L1;
|
continue L1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (ignoreFilter && ignoreFilter.some((e) => e.test(v))) {
|
||||||
ignoreFilter &&
|
|
||||||
ignoreFilter.some((e) => (e.pattern.startsWith("/") || e.pattern.startsWith("\\/")) && e.test(v))
|
|
||||||
) {
|
|
||||||
continue L1;
|
continue L1;
|
||||||
}
|
}
|
||||||
if (await this.plugin.$$isIgnoredByIgnoreFiles(v)) {
|
if (await this.services.vault.isIgnoredByIgnoreFile(v)) {
|
||||||
continue L1;
|
continue L1;
|
||||||
}
|
}
|
||||||
files = files.concat(await this.getFiles(v, ignoreList, filter, ignoreFilter));
|
files = files.concat(await this.getFiles(v, ignoreList, filter, ignoreFilter));
|
||||||
@@ -1780,4 +1791,20 @@ ${messageFetch}${messageOverwrite}${messageMerge}
|
|||||||
}
|
}
|
||||||
|
|
||||||
// <-- Local Storage SubFunctions
|
// <-- Local Storage SubFunctions
|
||||||
|
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services) {
|
||||||
|
// No longer needed on initialisation
|
||||||
|
// services.databaseEvents.handleOnDatabaseInitialisation(this._everyOnInitializeDatabase.bind(this));
|
||||||
|
services.appLifecycle.handleOnSettingLoaded(this._everyOnloadAfterLoadSettings.bind(this));
|
||||||
|
services.fileProcessing.handleOptionalFileEvent(this._anyProcessOptionalFileEvent.bind(this));
|
||||||
|
services.conflict.handleGetOptionalConflictCheckMethod(this._anyGetOptionalConflictCheckMethod.bind(this));
|
||||||
|
services.replication.handleProcessOptionalSynchroniseResult(this._anyProcessOptionalSyncFiles.bind(this));
|
||||||
|
services.setting.handleOnRealiseSetting(this._everyRealizeSettingSyncMode.bind(this));
|
||||||
|
services.appLifecycle.handleOnResuming(this._everyOnResumeProcess.bind(this));
|
||||||
|
services.replication.handleBeforeReplicate(this._everyBeforeReplicate.bind(this));
|
||||||
|
services.databaseEvents.handleDatabaseInitialised(this._everyOnDatabaseInitialized.bind(this));
|
||||||
|
services.setting.handleSuspendExtraSync(this._allSuspendExtraSync.bind(this));
|
||||||
|
services.setting.handleSuggestOptionalFeatures(this._allAskUsingOptionalSyncFeature.bind(this));
|
||||||
|
services.setting.handleEnableOptionalFeature(this._allConfigureOptionalSyncFeature.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import {
|
|||||||
LOG_LEVEL_NOTICE,
|
LOG_LEVEL_NOTICE,
|
||||||
type AnyEntry,
|
type AnyEntry,
|
||||||
type DocumentID,
|
type DocumentID,
|
||||||
type EntryHasPath,
|
|
||||||
type FilePath,
|
type FilePath,
|
||||||
type FilePathWithPrefix,
|
type FilePathWithPrefix,
|
||||||
type LOG_LEVEL,
|
type LOG_LEVEL,
|
||||||
} from "../lib/src/common/types.ts";
|
} from "../lib/src/common/types.ts";
|
||||||
import type ObsidianLiveSyncPlugin from "../main.ts";
|
import type ObsidianLiveSyncPlugin from "../main.ts";
|
||||||
import { MARK_DONE } from "../modules/features/ModuleLog.ts";
|
import { MARK_DONE } from "../modules/features/ModuleLog.ts";
|
||||||
|
import type { LiveSyncCore } from "../main.ts";
|
||||||
|
import { __$checkInstanceBinding } from "../lib/src/dev/checks.ts";
|
||||||
|
|
||||||
let noticeIndex = 0;
|
let noticeIndex = 0;
|
||||||
export abstract class LiveSyncCommands {
|
export abstract class LiveSyncCommands {
|
||||||
@@ -25,12 +26,15 @@ export abstract class LiveSyncCommands {
|
|||||||
get localDatabase() {
|
get localDatabase() {
|
||||||
return this.plugin.localDatabase;
|
return this.plugin.localDatabase;
|
||||||
}
|
}
|
||||||
|
get services() {
|
||||||
id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
|
return this.plugin.services;
|
||||||
return this.plugin.$$id2path(id, entry, stripPrefix);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
|
||||||
|
// return this.plugin.$$id2path(id, entry, stripPrefix);
|
||||||
|
// }
|
||||||
async path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID> {
|
async path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID> {
|
||||||
return await this.plugin.$$path2id(filename, prefix);
|
return await this.services.path.path2id(filename, prefix);
|
||||||
}
|
}
|
||||||
getPath(entry: AnyEntry): FilePathWithPrefix {
|
getPath(entry: AnyEntry): FilePathWithPrefix {
|
||||||
return getPath(entry);
|
return getPath(entry);
|
||||||
@@ -38,18 +42,20 @@ export abstract class LiveSyncCommands {
|
|||||||
|
|
||||||
constructor(plugin: ObsidianLiveSyncPlugin) {
|
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
|
this.onBindFunction(plugin, plugin.services);
|
||||||
|
__$checkInstanceBinding(this);
|
||||||
}
|
}
|
||||||
abstract onunload(): void;
|
abstract onunload(): void;
|
||||||
abstract onload(): void | Promise<void>;
|
abstract onload(): void | Promise<void>;
|
||||||
|
|
||||||
_isMainReady() {
|
_isMainReady() {
|
||||||
return this.plugin.$$isReady();
|
return this.plugin.services.appLifecycle.isReady();
|
||||||
}
|
}
|
||||||
_isMainSuspended() {
|
_isMainSuspended() {
|
||||||
return this.plugin.$$isSuspended();
|
return this.services.appLifecycle.isSuspended();
|
||||||
}
|
}
|
||||||
_isDatabaseReady() {
|
_isDatabaseReady() {
|
||||||
return this.plugin.$$isDatabaseReady();
|
return this.services.database.isDatabaseReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
_log = (msg: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key?: string) => {
|
_log = (msg: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key?: string) => {
|
||||||
@@ -89,4 +95,8 @@ export abstract class LiveSyncCommands {
|
|||||||
_debug = (msg: any, key?: string) => {
|
_debug = (msg: any, key?: string) => {
|
||||||
this._log(msg, LOG_LEVEL_VERBOSE, key);
|
this._log(msg, LOG_LEVEL_VERBOSE, key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services) {
|
||||||
|
// Override if needed.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
import { sizeToHumanReadable } from "octagonal-wheels/number";
|
import { sizeToHumanReadable } from "octagonal-wheels/number";
|
||||||
import { LOG_LEVEL_NOTICE, type MetaEntry } from "../../lib/src/common/types";
|
import {
|
||||||
|
EntryTypes,
|
||||||
|
LOG_LEVEL_INFO,
|
||||||
|
LOG_LEVEL_NOTICE,
|
||||||
|
LOG_LEVEL_VERBOSE,
|
||||||
|
type DocumentID,
|
||||||
|
type EntryDoc,
|
||||||
|
type EntryLeaf,
|
||||||
|
type MetaEntry,
|
||||||
|
} from "../../lib/src/common/types";
|
||||||
import { getNoFromRev } from "../../lib/src/pouchdb/LiveSyncLocalDB";
|
import { getNoFromRev } from "../../lib/src/pouchdb/LiveSyncLocalDB";
|
||||||
import type { IObsidianModule } from "../../modules/AbstractObsidianModule";
|
|
||||||
import { LiveSyncCommands } from "../LiveSyncCommands";
|
import { LiveSyncCommands } from "../LiveSyncCommands";
|
||||||
|
import { serialized } from "octagonal-wheels/concurrency/lock_v2";
|
||||||
|
import { arrayToChunkedArray } from "octagonal-wheels/collection";
|
||||||
|
const DB_KEY_SEQ = "gc-seq";
|
||||||
|
const DB_KEY_CHUNK_SET = "chunk-set";
|
||||||
|
const DB_KEY_DOC_USAGE_MAP = "doc-usage-map";
|
||||||
|
type ChunkID = DocumentID;
|
||||||
|
type NoteDocumentID = DocumentID;
|
||||||
|
type Rev = string;
|
||||||
|
|
||||||
export class LocalDatabaseMaintenance extends LiveSyncCommands implements IObsidianModule {
|
type ChunkUsageMap = Map<NoteDocumentID, Map<Rev, Set<ChunkID>>>;
|
||||||
$everyOnload(): Promise<boolean> {
|
export class LocalDatabaseMaintenance extends LiveSyncCommands {
|
||||||
return Promise.resolve(true);
|
|
||||||
}
|
|
||||||
onunload(): void {
|
onunload(): void {
|
||||||
// NO OP.
|
// NO OP.
|
||||||
}
|
}
|
||||||
@@ -262,4 +276,213 @@ Note: **Make sure to synchronise all devices before deletion.**
|
|||||||
this.clearHash();
|
this.clearHash();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async scanUnusedChunks() {
|
||||||
|
const kvDB = this.plugin.kvDB;
|
||||||
|
const chunkSet = (await kvDB.get<Set<DocumentID>>(DB_KEY_CHUNK_SET)) || new Set();
|
||||||
|
const chunkUsageMap = (await kvDB.get<ChunkUsageMap>(DB_KEY_DOC_USAGE_MAP)) || new Map();
|
||||||
|
const KEEP_MAX_REVS = 10;
|
||||||
|
const unusedSet = new Set<DocumentID>([...chunkSet]);
|
||||||
|
for (const [, revIdMap] of chunkUsageMap) {
|
||||||
|
const sortedRevId = [...revIdMap.entries()].sort((a, b) => getNoFromRev(b[0]) - getNoFromRev(a[0]));
|
||||||
|
if (sortedRevId.length > KEEP_MAX_REVS) {
|
||||||
|
// If we have more revisions than we want to keep, we need to delete the extras
|
||||||
|
}
|
||||||
|
const keepRevID = sortedRevId.slice(0, KEEP_MAX_REVS);
|
||||||
|
keepRevID.forEach((e) => e[1].forEach((ee) => unusedSet.delete(ee)));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
chunkSet,
|
||||||
|
chunkUsageMap,
|
||||||
|
unusedSet,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Track changes in the database and update the chunk usage map for garbage collection.
|
||||||
|
* Note that this only able to perform without Fetch chunks on demand.
|
||||||
|
*/
|
||||||
|
async trackChanges(fromStart: boolean = false, showNotice: boolean = false) {
|
||||||
|
if (!this.isAvailable()) return;
|
||||||
|
const logLevel = showNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||||
|
const kvDB = this.plugin.kvDB;
|
||||||
|
|
||||||
|
const previousSeq = fromStart ? "" : await kvDB.get<string>(DB_KEY_SEQ);
|
||||||
|
const chunkSet = (await kvDB.get<Set<DocumentID>>(DB_KEY_CHUNK_SET)) || new Set();
|
||||||
|
|
||||||
|
const chunkUsageMap = (await kvDB.get<ChunkUsageMap>(DB_KEY_DOC_USAGE_MAP)) || new Map();
|
||||||
|
|
||||||
|
const db = this.localDatabase.localDatabase;
|
||||||
|
const verbose = (msg: string) => this._verbose(msg);
|
||||||
|
|
||||||
|
const processDoc = async (doc: EntryDoc, isDeleted: boolean) => {
|
||||||
|
if (!("children" in doc)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = doc._id;
|
||||||
|
const rev = doc._rev!;
|
||||||
|
const deleted = doc._deleted || isDeleted;
|
||||||
|
const softDeleted = doc.deleted;
|
||||||
|
const children = (doc.children || []) as DocumentID[];
|
||||||
|
if (!chunkUsageMap.has(id)) {
|
||||||
|
chunkUsageMap.set(id, new Map<Rev, Set<ChunkID>>());
|
||||||
|
}
|
||||||
|
for (const chunkId of children) {
|
||||||
|
if (deleted) {
|
||||||
|
chunkUsageMap.get(id)!.delete(rev);
|
||||||
|
// chunkSet.add(chunkId as DocumentID);
|
||||||
|
} else {
|
||||||
|
if (softDeleted) {
|
||||||
|
//TODO: Soft delete
|
||||||
|
chunkUsageMap.get(id)!.set(rev, (chunkUsageMap.get(id)!.get(rev) || new Set()).add(chunkId));
|
||||||
|
} else {
|
||||||
|
chunkUsageMap.get(id)!.set(rev, (chunkUsageMap.get(id)!.get(rev) || new Set()).add(chunkId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verbose(
|
||||||
|
`Tracking chunk: ${id}/${rev} (${doc?.path}), deleted: ${deleted ? "yes" : "no"} Soft-Deleted:${softDeleted ? "yes" : "no"}`
|
||||||
|
);
|
||||||
|
return await Promise.resolve();
|
||||||
|
};
|
||||||
|
// let saveQueue = 0;
|
||||||
|
const saveState = async (seq: string | number) => {
|
||||||
|
await kvDB.set(DB_KEY_SEQ, seq);
|
||||||
|
await kvDB.set(DB_KEY_CHUNK_SET, chunkSet);
|
||||||
|
await kvDB.set(DB_KEY_DOC_USAGE_MAP, chunkUsageMap);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processDocRevisions = async (doc: EntryDoc) => {
|
||||||
|
try {
|
||||||
|
const oldRevisions = await db.get(doc._id, { revs: true, revs_info: true, conflicts: true });
|
||||||
|
const allRevs = oldRevisions._revs_info?.length || 0;
|
||||||
|
const info = (oldRevisions._revs_info || [])
|
||||||
|
.filter((e) => e.status == "available" && e.rev != doc._rev)
|
||||||
|
.filter((info) => !chunkUsageMap.get(doc._id)?.has(info.rev));
|
||||||
|
const infoLength = info.length;
|
||||||
|
this._log(`Found ${allRevs} old revisions for ${doc._id} . ${infoLength} items to check `);
|
||||||
|
if (info.length > 0) {
|
||||||
|
const oldDocs = await Promise.all(
|
||||||
|
info
|
||||||
|
.filter((revInfo) => revInfo.status == "available")
|
||||||
|
.map((revInfo) => db.get(doc._id, { rev: revInfo.rev }))
|
||||||
|
).then((docs) => docs.filter((doc) => doc));
|
||||||
|
for (const oldDoc of oldDocs) {
|
||||||
|
await processDoc(oldDoc as EntryDoc, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
if ((ex as any)?.status == 404) {
|
||||||
|
this._log(`No revisions found for ${doc._id}`, LOG_LEVEL_VERBOSE);
|
||||||
|
} else {
|
||||||
|
this._log(`Error finding revisions for ${doc._id}`);
|
||||||
|
this._verbose(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const processChange = async (doc: EntryDoc, isDeleted: boolean, seq: string | number) => {
|
||||||
|
if (doc.type === EntryTypes.CHUNK) {
|
||||||
|
if (isDeleted) return;
|
||||||
|
chunkSet.add(doc._id);
|
||||||
|
} else if ("children" in doc) {
|
||||||
|
await processDoc(doc, isDeleted);
|
||||||
|
await serialized("x-process-doc", async () => await processDocRevisions(doc));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Track changes
|
||||||
|
let i = 0;
|
||||||
|
await db
|
||||||
|
.changes({
|
||||||
|
since: previousSeq || "",
|
||||||
|
live: false,
|
||||||
|
conflicts: true,
|
||||||
|
include_docs: true,
|
||||||
|
style: "all_docs",
|
||||||
|
return_docs: false,
|
||||||
|
})
|
||||||
|
.on("change", async (change) => {
|
||||||
|
// handle change
|
||||||
|
await processChange(change.doc!, change.deleted ?? false, change.seq);
|
||||||
|
if (i++ % 100 == 0) {
|
||||||
|
await saveState(change.seq);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("complete", async (info) => {
|
||||||
|
await saveState(info.last_seq);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track all changed docs and new-leafs;
|
||||||
|
|
||||||
|
const result = await this.scanUnusedChunks();
|
||||||
|
|
||||||
|
const message = `Total chunks: ${result.chunkSet.size}\nUnused chunks: ${result.unusedSet.size}`;
|
||||||
|
this._log(message, logLevel);
|
||||||
|
}
|
||||||
|
async performGC(showingNotice = false) {
|
||||||
|
if (!this.isAvailable()) return;
|
||||||
|
await this.trackChanges(false, showingNotice);
|
||||||
|
const title = "Are all devices synchronised?";
|
||||||
|
const confirmMessage = `This function deletes unused chunks from the device. If there are differences between devices, some chunks may be missing when resolving conflicts.
|
||||||
|
Be sure to synchronise before executing.
|
||||||
|
|
||||||
|
However, if you have deleted them, you may be able to recover them by performing Hatch -> Recreate missing chunks for all files.
|
||||||
|
|
||||||
|
Are you ready to delete unused chunks?`;
|
||||||
|
|
||||||
|
const logLevel = showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||||
|
|
||||||
|
const BUTTON_OK = `Yes, delete chunks`;
|
||||||
|
const BUTTON_CANCEL = "Cancel";
|
||||||
|
|
||||||
|
const result = await this.plugin.confirm.askSelectStringDialogue(
|
||||||
|
confirmMessage,
|
||||||
|
[BUTTON_OK, BUTTON_CANCEL] as const,
|
||||||
|
{
|
||||||
|
title,
|
||||||
|
defaultAction: BUTTON_CANCEL,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (result !== BUTTON_OK) {
|
||||||
|
this._log("User cancelled chunk deletion", logLevel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { unusedSet, chunkSet } = await this.scanUnusedChunks();
|
||||||
|
const deleteChunks = await this.database.allDocs({
|
||||||
|
keys: [...unusedSet],
|
||||||
|
include_docs: true,
|
||||||
|
});
|
||||||
|
for (const chunk of deleteChunks.rows) {
|
||||||
|
if ((chunk as any)?.value?.deleted) {
|
||||||
|
chunkSet.delete(chunk.key as DocumentID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const deleteDocs = deleteChunks.rows
|
||||||
|
.filter((e) => "doc" in e)
|
||||||
|
.map((e) => ({
|
||||||
|
...(e as any).doc!,
|
||||||
|
_deleted: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
this._log(`Deleting chunks: ${deleteDocs.length}`, logLevel);
|
||||||
|
const deleteChunkBatch = arrayToChunkedArray(deleteDocs, 100);
|
||||||
|
let successCount = 0;
|
||||||
|
let errored = 0;
|
||||||
|
for (const batch of deleteChunkBatch) {
|
||||||
|
const results = await this.database.bulkDocs(batch as EntryLeaf[]);
|
||||||
|
for (const result of results) {
|
||||||
|
if ("ok" in result) {
|
||||||
|
chunkSet.delete(result.id as DocumentID);
|
||||||
|
successCount++;
|
||||||
|
} else {
|
||||||
|
this._log(`Failed to delete doc: ${result.id}`, LOG_LEVEL_VERBOSE);
|
||||||
|
errored++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._log(`Deleting chunks: ${successCount} `, logLevel, "gc-preforming");
|
||||||
|
}
|
||||||
|
const message = `Garbage Collection completed.
|
||||||
|
Success: ${successCount}, Errored: ${errored}`;
|
||||||
|
this._log(message, logLevel);
|
||||||
|
const kvDB = this.plugin.kvDB;
|
||||||
|
await kvDB.set(DB_KEY_CHUNK_SET, chunkSet);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
import type { IObsidianModule } from "../../modules/AbstractObsidianModule";
|
|
||||||
import { P2PReplicatorPaneView, VIEW_TYPE_P2P } from "./P2PReplicator/P2PReplicatorPaneView.ts";
|
import { P2PReplicatorPaneView, VIEW_TYPE_P2P } from "./P2PReplicator/P2PReplicatorPaneView.ts";
|
||||||
import {
|
import {
|
||||||
AutoAccepting,
|
AutoAccepting,
|
||||||
LOG_LEVEL_NOTICE,
|
LOG_LEVEL_NOTICE,
|
||||||
|
P2P_DEFAULT_SETTINGS,
|
||||||
REMOTE_P2P,
|
REMOTE_P2P,
|
||||||
type EntryDoc,
|
type EntryDoc,
|
||||||
type P2PSyncSetting,
|
type P2PSyncSetting,
|
||||||
type RemoteDBSettings,
|
type RemoteDBSettings,
|
||||||
} from "../../lib/src/common/types.ts";
|
} from "../../lib/src/common/types.ts";
|
||||||
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
|
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
|
||||||
import { LiveSyncTrysteroReplicator } from "../../lib/src/replication/trystero/LiveSyncTrysteroReplicator.ts";
|
import {
|
||||||
|
LiveSyncTrysteroReplicator,
|
||||||
|
setReplicatorFunc,
|
||||||
|
} from "../../lib/src/replication/trystero/LiveSyncTrysteroReplicator.ts";
|
||||||
import { EVENT_REQUEST_OPEN_P2P, eventHub } from "../../common/events.ts";
|
import { EVENT_REQUEST_OPEN_P2P, eventHub } from "../../common/events.ts";
|
||||||
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator.ts";
|
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator.ts";
|
||||||
import { Logger } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_INFO, LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
|
||||||
import type { CommandShim } from "../../lib/src/replication/trystero/P2PReplicatorPaneCommon.ts";
|
import type { CommandShim } from "../../lib/src/replication/trystero/P2PReplicatorPaneCommon.ts";
|
||||||
import {
|
import {
|
||||||
P2PReplicatorMixIn,
|
addP2PEventHandlers,
|
||||||
|
closeP2PReplicator,
|
||||||
|
openP2PReplicator,
|
||||||
|
P2PLogCollector,
|
||||||
removeP2PReplicatorInstance,
|
removeP2PReplicatorInstance,
|
||||||
type P2PReplicatorBase,
|
type P2PReplicatorBase,
|
||||||
} from "../../lib/src/replication/trystero/P2PReplicatorCore.ts";
|
} from "../../lib/src/replication/trystero/P2PReplicatorCore.ts";
|
||||||
@@ -24,8 +30,11 @@ import type { Confirm } from "../../lib/src/interfaces/Confirm.ts";
|
|||||||
import type ObsidianLiveSyncPlugin from "../../main.ts";
|
import type ObsidianLiveSyncPlugin from "../../main.ts";
|
||||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||||
import { getPlatformName } from "../../lib/src/PlatformAPIs/obsidian/Environment.ts";
|
import { getPlatformName } from "../../lib/src/PlatformAPIs/obsidian/Environment.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
import { TrysteroReplicator } from "../../lib/src/replication/trystero/TrysteroReplicator.ts";
|
||||||
|
import { SETTING_KEY_P2P_DEVICE_NAME } from "../../lib/src/common/types.ts";
|
||||||
|
|
||||||
class P2PReplicatorCommandBase extends LiveSyncCommands implements P2PReplicatorBase {
|
export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase, CommandShim {
|
||||||
storeP2PStatusLine = reactiveSource("");
|
storeP2PStatusLine = reactiveSource("");
|
||||||
|
|
||||||
getSettings(): P2PSyncSetting {
|
getSettings(): P2PSyncSetting {
|
||||||
@@ -49,47 +58,124 @@ class P2PReplicatorCommandBase extends LiveSyncCommands implements P2PReplicator
|
|||||||
|
|
||||||
constructor(plugin: ObsidianLiveSyncPlugin) {
|
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||||
super(plugin);
|
super(plugin);
|
||||||
|
setReplicatorFunc(() => this._replicatorInstance);
|
||||||
|
addP2PEventHandlers(this);
|
||||||
|
this.afterConstructor();
|
||||||
|
// onBindFunction is called in super class
|
||||||
|
// this.onBindFunction(plugin, plugin.services);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleReplicatedDocuments(docs: EntryDoc[]): Promise<void> {
|
async handleReplicatedDocuments(docs: EntryDoc[]): Promise<void> {
|
||||||
// console.log("Processing Replicated Docs", docs);
|
// console.log("Processing Replicated Docs", docs);
|
||||||
return await this.plugin.$$parseReplicationResult(docs as PouchDB.Core.ExistingDocument<EntryDoc>[]);
|
return await this.services.replication.parseSynchroniseResult(
|
||||||
}
|
docs as PouchDB.Core.ExistingDocument<EntryDoc>[]
|
||||||
onunload(): void {
|
);
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
onload(): void | Promise<void> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
|
||||||
this._simpleStore = this.plugin.$$getSimpleStore("p2p-sync");
|
|
||||||
return Promise.resolve(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class P2PReplicator
|
|
||||||
extends P2PReplicatorMixIn(P2PReplicatorCommandBase)
|
|
||||||
implements IObsidianModule, CommandShim
|
|
||||||
{
|
|
||||||
storeP2PStatusLine = reactiveSource("");
|
|
||||||
$anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
|
|
||||||
const settings = { ...this.settings, ...settingOverride };
|
const settings = { ...this.settings, ...settingOverride };
|
||||||
if (settings.remoteType == REMOTE_P2P) {
|
if (settings.remoteType == REMOTE_P2P) {
|
||||||
return Promise.resolve(new LiveSyncTrysteroReplicator(this.plugin));
|
return Promise.resolve(new LiveSyncTrysteroReplicator(this.plugin));
|
||||||
}
|
}
|
||||||
return undefined!;
|
return undefined!;
|
||||||
}
|
}
|
||||||
override getPlatform(): string {
|
_replicatorInstance?: TrysteroReplicator;
|
||||||
|
p2pLogCollector = new P2PLogCollector();
|
||||||
|
|
||||||
|
afterConstructor() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async open() {
|
||||||
|
await openP2PReplicator(this);
|
||||||
|
}
|
||||||
|
async close() {
|
||||||
|
await closeP2PReplicator(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfig(key: string) {
|
||||||
|
return this.services.config.getSmallConfig(key);
|
||||||
|
}
|
||||||
|
setConfig(key: string, value: string) {
|
||||||
|
return this.services.config.setSmallConfig(key, value);
|
||||||
|
}
|
||||||
|
enableBroadcastCastings() {
|
||||||
|
return this?._replicatorInstance?.enableBroadcastChanges();
|
||||||
|
}
|
||||||
|
disableBroadcastCastings() {
|
||||||
|
return this?._replicatorInstance?.disableBroadcastChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._simpleStore = this.services.database.openSimpleStore("p2p-sync");
|
||||||
|
return Promise.resolve(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialiseP2PReplicator(): Promise<TrysteroReplicator> {
|
||||||
|
await this.init();
|
||||||
|
try {
|
||||||
|
if (this._replicatorInstance) {
|
||||||
|
await this._replicatorInstance.close();
|
||||||
|
this._replicatorInstance = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.settings.P2P_AppID) {
|
||||||
|
this.settings.P2P_AppID = P2P_DEFAULT_SETTINGS.P2P_AppID;
|
||||||
|
}
|
||||||
|
const getInitialDeviceName = () =>
|
||||||
|
this.getConfig(SETTING_KEY_P2P_DEVICE_NAME) || this.services.vault.getVaultName();
|
||||||
|
|
||||||
|
const getSettings = () => this.settings;
|
||||||
|
const store = () => this.simpleStore();
|
||||||
|
const getDB = () => this.getDB();
|
||||||
|
|
||||||
|
const getConfirm = () => this.confirm;
|
||||||
|
const getPlatform = () => this.getPlatform();
|
||||||
|
const env = {
|
||||||
|
get db() {
|
||||||
|
return getDB();
|
||||||
|
},
|
||||||
|
get confirm() {
|
||||||
|
return getConfirm();
|
||||||
|
},
|
||||||
|
get deviceName() {
|
||||||
|
return getInitialDeviceName();
|
||||||
|
},
|
||||||
|
get platform() {
|
||||||
|
return getPlatform();
|
||||||
|
},
|
||||||
|
get settings() {
|
||||||
|
return getSettings();
|
||||||
|
},
|
||||||
|
processReplicatedDocs: async (docs: EntryDoc[]): Promise<void> => {
|
||||||
|
await this.handleReplicatedDocuments(docs);
|
||||||
|
// No op. This is a client and does not need to process the docs
|
||||||
|
},
|
||||||
|
get simpleStore() {
|
||||||
|
return store();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this._replicatorInstance = new TrysteroReplicator(env);
|
||||||
|
return this._replicatorInstance;
|
||||||
|
} catch (e) {
|
||||||
|
this._log(
|
||||||
|
e instanceof Error ? e.message : "Something occurred on Initialising P2P Replicator",
|
||||||
|
LOG_LEVEL_INFO
|
||||||
|
);
|
||||||
|
this._log(e, LOG_LEVEL_VERBOSE);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getPlatform(): string {
|
||||||
return getPlatformName();
|
return getPlatformName();
|
||||||
}
|
}
|
||||||
|
|
||||||
override onunload(): void {
|
onunload(): void {
|
||||||
removeP2PReplicatorInstance();
|
removeP2PReplicatorInstance();
|
||||||
void this.close();
|
void this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
override onload(): void | Promise<void> {
|
onload(): void | Promise<void> {
|
||||||
eventHub.onEvent(EVENT_REQUEST_OPEN_P2P, () => {
|
eventHub.onEvent(EVENT_REQUEST_OPEN_P2P, () => {
|
||||||
void this.openPane();
|
void this.openPane();
|
||||||
});
|
});
|
||||||
@@ -97,12 +183,12 @@ export class P2PReplicator
|
|||||||
this.storeP2PStatusLine.value = line.value;
|
this.storeP2PStatusLine.value = line.value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async $everyOnInitializeDatabase(): Promise<boolean> {
|
async _everyOnInitializeDatabase(): Promise<boolean> {
|
||||||
await this.initialiseP2PReplicator();
|
await this.initialiseP2PReplicator();
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $allSuspendExtraSync() {
|
private async _allSuspendExtraSync() {
|
||||||
this.plugin.settings.P2P_Enabled = false;
|
this.plugin.settings.P2P_Enabled = false;
|
||||||
this.plugin.settings.P2P_AutoAccepting = AutoAccepting.NONE;
|
this.plugin.settings.P2P_AutoAccepting = AutoAccepting.NONE;
|
||||||
this.plugin.settings.P2P_AutoBroadcast = false;
|
this.plugin.settings.P2P_AutoBroadcast = false;
|
||||||
@@ -112,15 +198,15 @@ export class P2PReplicator
|
|||||||
return await Promise.resolve(true);
|
return await Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $everyOnLoadStart() {
|
// async $everyOnLoadStart() {
|
||||||
return await Promise.resolve();
|
// return await Promise.resolve();
|
||||||
}
|
// }
|
||||||
|
|
||||||
async openPane() {
|
async openPane() {
|
||||||
await this.plugin.$$showView(VIEW_TYPE_P2P);
|
await this.services.API.showWindow(VIEW_TYPE_P2P);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $everyOnloadStart(): Promise<boolean> {
|
async _everyOnloadStart(): Promise<boolean> {
|
||||||
this.plugin.registerView(VIEW_TYPE_P2P, (leaf) => new P2PReplicatorPaneView(leaf, this.plugin));
|
this.plugin.registerView(VIEW_TYPE_P2P, (leaf) => new P2PReplicatorPaneView(leaf, this.plugin));
|
||||||
this.plugin.addCommand({
|
this.plugin.addCommand({
|
||||||
id: "open-p2p-replicator",
|
id: "open-p2p-replicator",
|
||||||
@@ -170,10 +256,26 @@ export class P2PReplicator
|
|||||||
|
|
||||||
return await Promise.resolve(true);
|
return await Promise.resolve(true);
|
||||||
}
|
}
|
||||||
$everyAfterResumeProcess(): Promise<boolean> {
|
_everyAfterResumeProcess(): Promise<boolean> {
|
||||||
if (this.settings.P2P_Enabled && this.settings.P2P_AutoStart) {
|
if (this.settings.P2P_Enabled && this.settings.P2P_AutoStart) {
|
||||||
setTimeout(() => void this.open(), 100);
|
setTimeout(() => void this.open(), 100);
|
||||||
}
|
}
|
||||||
|
const rep = this._replicatorInstance;
|
||||||
|
rep?.allowReconnection();
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
_everyBeforeSuspendProcess(): Promise<boolean> {
|
||||||
|
const rep = this._replicatorInstance;
|
||||||
|
rep?.disconnectFromServer();
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.replicator.handleGetNewReplicator(this._anyNewReplicator.bind(this));
|
||||||
|
services.databaseEvents.handleOnDatabaseInitialisation(this._everyOnInitializeDatabase.bind(this));
|
||||||
|
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||||
|
services.appLifecycle.handleOnSuspending(this._everyBeforeSuspendProcess.bind(this));
|
||||||
|
services.appLifecycle.handleOnResumed(this._everyAfterResumeProcess.bind(this));
|
||||||
|
services.setting.handleSuspendExtraSync(this._allSuspendExtraSync.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
} from "../../../lib/src/replication/trystero/TrysteroReplicatorP2PServer";
|
} from "../../../lib/src/replication/trystero/TrysteroReplicatorP2PServer";
|
||||||
import { type P2PReplicatorStatus } from "../../../lib/src/replication/trystero/TrysteroReplicator";
|
import { type P2PReplicatorStatus } from "../../../lib/src/replication/trystero/TrysteroReplicator";
|
||||||
import { $msg as _msg } from "../../../lib/src/common/i18n";
|
import { $msg as _msg } from "../../../lib/src/common/i18n";
|
||||||
|
import { SETTING_KEY_P2P_DEVICE_NAME } from "../../../lib/src/common/types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
plugin: PluginShim;
|
plugin: PluginShim;
|
||||||
@@ -32,10 +33,10 @@
|
|||||||
const initialSettings = { ...plugin.settings };
|
const initialSettings = { ...plugin.settings };
|
||||||
|
|
||||||
let settings = $state<P2PSyncSetting>(initialSettings);
|
let settings = $state<P2PSyncSetting>(initialSettings);
|
||||||
// const vaultName = plugin.$$getVaultName();
|
// const vaultName = service.vault.getVaultName();
|
||||||
// const dbKey = `${vaultName}-p2p-device-name`;
|
// const dbKey = `${vaultName}-p2p-device-name`;
|
||||||
|
|
||||||
const initialDeviceName = cmdSync.getConfig("p2p_device_name") ?? plugin.$$getVaultName();
|
const initialDeviceName = cmdSync.getConfig(SETTING_KEY_P2P_DEVICE_NAME) ?? plugin.services.vault.getVaultName();
|
||||||
let deviceName = $state<string>(initialDeviceName);
|
let deviceName = $state<string>(initialDeviceName);
|
||||||
|
|
||||||
let eP2PEnabled = $state<boolean>(initialSettings.P2P_Enabled);
|
let eP2PEnabled = $state<boolean>(initialSettings.P2P_Enabled);
|
||||||
@@ -84,7 +85,7 @@
|
|||||||
P2P_AutoBroadcast: eAutoBroadcast,
|
P2P_AutoBroadcast: eAutoBroadcast,
|
||||||
};
|
};
|
||||||
plugin.settings = newSettings;
|
plugin.settings = newSettings;
|
||||||
cmdSync.setConfig("p2p_device_name", eDeviceName);
|
cmdSync.setConfig(SETTING_KEY_P2P_DEVICE_NAME, eDeviceName);
|
||||||
deviceName = eDeviceName;
|
deviceName = eDeviceName;
|
||||||
await plugin.saveSettings();
|
await plugin.saveSettings();
|
||||||
}
|
}
|
||||||
@@ -250,6 +251,9 @@
|
|||||||
};
|
};
|
||||||
cmdSync.setConfig(initialDialogStatusKey, JSON.stringify(dialogStatus));
|
cmdSync.setConfig(initialDialogStatusKey, JSON.stringify(dialogStatus));
|
||||||
});
|
});
|
||||||
|
let isObsidian = $derived.by(() => {
|
||||||
|
return plugin.services.API.getPlatform() === "obsidian";
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<article>
|
<article>
|
||||||
@@ -265,95 +269,105 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</details>
|
</details>
|
||||||
<h2>Connection Settings</h2>
|
<h2>Connection Settings</h2>
|
||||||
<details bind:open={isSettingOpened}>
|
{#if isObsidian}
|
||||||
<summary>{eRelay}</summary>
|
You can configure in the Obsidian Plugin Settings.
|
||||||
<table class="settings">
|
{:else}
|
||||||
<tbody>
|
<details bind:open={isSettingOpened}>
|
||||||
<tr>
|
<summary>{eRelay}</summary>
|
||||||
<th> Enable P2P Replicator </th>
|
<table class="settings">
|
||||||
<td>
|
<tbody>
|
||||||
<label class={{ "is-dirty": isP2PEnabledModified }}>
|
<tr>
|
||||||
<input type="checkbox" bind:checked={eP2PEnabled} />
|
<th> Enable P2P Replicator </th>
|
||||||
</label>
|
<td>
|
||||||
</td>
|
<label class={{ "is-dirty": isP2PEnabledModified }}>
|
||||||
</tr><tr>
|
<input type="checkbox" bind:checked={eP2PEnabled} />
|
||||||
<th> Relay settings </th>
|
</label>
|
||||||
<td>
|
</td>
|
||||||
<label class={{ "is-dirty": isRelayModified }}>
|
</tr><tr>
|
||||||
<input
|
<th> Relay settings </th>
|
||||||
type="text"
|
<td>
|
||||||
placeholder="wss://exp-relay.vrtmrz.net, wss://xxxxx"
|
<label class={{ "is-dirty": isRelayModified }}>
|
||||||
bind:value={eRelay}
|
<input
|
||||||
autocomplete="off"
|
type="text"
|
||||||
/>
|
placeholder="wss://exp-relay.vrtmrz.net, wss://xxxxx"
|
||||||
<button onclick={() => useDefaultRelay()}> Use vrtmrz's relay </button>
|
bind:value={eRelay}
|
||||||
</label>
|
autocomplete="off"
|
||||||
</td>
|
/>
|
||||||
</tr>
|
<button onclick={() => useDefaultRelay()}> Use vrtmrz's relay </button>
|
||||||
<tr>
|
</label>
|
||||||
<th> Room ID </th>
|
</td>
|
||||||
<td>
|
</tr>
|
||||||
<label class={{ "is-dirty": isRoomIdModified }}>
|
<tr>
|
||||||
<input
|
<th> Room ID </th>
|
||||||
type="text"
|
<td>
|
||||||
placeholder="anything-you-like"
|
<label class={{ "is-dirty": isRoomIdModified }}>
|
||||||
bind:value={eRoomId}
|
<input
|
||||||
autocomplete="off"
|
type="text"
|
||||||
spellcheck="false"
|
placeholder="anything-you-like"
|
||||||
autocorrect="off"
|
bind:value={eRoomId}
|
||||||
/>
|
autocomplete="off"
|
||||||
<button onclick={() => chooseRandom()}> Use Random Number </button>
|
spellcheck="false"
|
||||||
</label>
|
autocorrect="off"
|
||||||
<span>
|
/>
|
||||||
<small>
|
<button onclick={() => chooseRandom()}> Use Random Number </button>
|
||||||
This can isolate your connections between devices. Use the same Room ID for the same
|
</label>
|
||||||
devices.</small
|
<span>
|
||||||
>
|
<small>
|
||||||
</span>
|
This can isolate your connections between devices. Use the same Room ID for the same
|
||||||
</td>
|
devices.</small
|
||||||
</tr>
|
>
|
||||||
<tr>
|
</span>
|
||||||
<th> Password </th>
|
</td>
|
||||||
<td>
|
</tr>
|
||||||
<label class={{ "is-dirty": isPasswordModified }}>
|
<tr>
|
||||||
<input type="password" placeholder="password" bind:value={ePassword} />
|
<th> Password </th>
|
||||||
</label>
|
<td>
|
||||||
<span>
|
<label class={{ "is-dirty": isPasswordModified }}>
|
||||||
<small> This password is used to encrypt the connection. Use something long enough. </small>
|
<input type="password" placeholder="password" bind:value={ePassword} />
|
||||||
</span>
|
</label>
|
||||||
</td>
|
<span>
|
||||||
</tr>
|
<small>
|
||||||
<tr>
|
This password is used to encrypt the connection. Use something long enough.
|
||||||
<th> This device name </th>
|
</small>
|
||||||
<td>
|
</span>
|
||||||
<label class={{ "is-dirty": isDeviceNameModified }}>
|
</td>
|
||||||
<input type="text" placeholder="iphone-16" bind:value={eDeviceName} autocomplete="off" />
|
</tr>
|
||||||
</label>
|
<tr>
|
||||||
<span>
|
<th> This device name </th>
|
||||||
<small>
|
<td>
|
||||||
Device name to identify the device. Please use shorter one for the stable peer
|
<label class={{ "is-dirty": isDeviceNameModified }}>
|
||||||
detection, i.e., "iphone-16" or "macbook-2021".
|
<input
|
||||||
</small>
|
type="text"
|
||||||
</span>
|
placeholder="iphone-16"
|
||||||
</td>
|
bind:value={eDeviceName}
|
||||||
</tr>
|
autocomplete="off"
|
||||||
<tr>
|
/>
|
||||||
<th> Auto Connect </th>
|
</label>
|
||||||
<td>
|
<span>
|
||||||
<label class={{ "is-dirty": isAutoStartModified }}>
|
<small>
|
||||||
<input type="checkbox" bind:checked={eAutoStart} />
|
Device name to identify the device. Please use shorter one for the stable peer
|
||||||
</label>
|
detection, i.e., "iphone-16" or "macbook-2021".
|
||||||
</td>
|
</small>
|
||||||
</tr>
|
</span>
|
||||||
<tr>
|
</td>
|
||||||
<th> Start change-broadcasting on Connect </th>
|
</tr>
|
||||||
<td>
|
<tr>
|
||||||
<label class={{ "is-dirty": isAutoBroadcastModified }}>
|
<th> Auto Connect </th>
|
||||||
<input type="checkbox" bind:checked={eAutoBroadcast} />
|
<td>
|
||||||
</label>
|
<label class={{ "is-dirty": isAutoStartModified }}>
|
||||||
</td>
|
<input type="checkbox" bind:checked={eAutoStart} />
|
||||||
</tr>
|
</label>
|
||||||
<!-- <tr>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th> Start change-broadcasting on Connect </th>
|
||||||
|
<td>
|
||||||
|
<label class={{ "is-dirty": isAutoBroadcastModified }}>
|
||||||
|
<input type="checkbox" bind:checked={eAutoBroadcast} />
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- <tr>
|
||||||
<th> Auto Accepting </th>
|
<th> Auto Accepting </th>
|
||||||
<td>
|
<td>
|
||||||
<label class={{ "is-dirty": isAutoAcceptModified }}>
|
<label class={{ "is-dirty": isAutoAcceptModified }}>
|
||||||
@@ -361,11 +375,12 @@
|
|||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
</tr> -->
|
</tr> -->
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button disabled={!isAnyModified} class="button mod-cta" onclick={saveAndApply}>Save and Apply</button>
|
<button disabled={!isAnyModified} class="button mod-cta" onclick={saveAndApply}>Save and Apply</button>
|
||||||
<button disabled={!isAnyModified} class="button" onclick={revert}>Revert changes</button>
|
<button disabled={!isAnyModified} class="button" onclick={revert}>Revert changes</button>
|
||||||
</details>
|
</details>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2>Signaling Server Connection</h2>
|
<h2>Signaling Server Connection</h2>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ And you can also drop the local database to rebuild from the remote device.`,
|
|||||||
if (yn === DROP) {
|
if (yn === DROP) {
|
||||||
await this.plugin.rebuilder.scheduleFetch();
|
await this.plugin.rebuilder.scheduleFetch();
|
||||||
} else {
|
} else {
|
||||||
await this.plugin.$$scheduleAppReload();
|
this.plugin.services.appLifecycle.scheduleRestart();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
|
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
|
||||||
|
|||||||
2
src/lib
2
src/lib
Submodule src/lib updated: 7a0d8e449a...5e352d3093
804
src/main.ts
804
src/main.ts
@@ -1,26 +1,10 @@
|
|||||||
import { Plugin } from "./deps";
|
import { Plugin } from "./deps";
|
||||||
import {
|
import {
|
||||||
type EntryDoc,
|
type EntryDoc,
|
||||||
type LoadedEntry,
|
|
||||||
type ObsidianLiveSyncSettings,
|
type ObsidianLiveSyncSettings,
|
||||||
type LOG_LEVEL,
|
|
||||||
type diff_result,
|
|
||||||
type DatabaseConnectingStatus,
|
type DatabaseConnectingStatus,
|
||||||
type EntryHasPath,
|
|
||||||
type DocumentID,
|
|
||||||
type FilePathWithPrefix,
|
|
||||||
type FilePath,
|
|
||||||
LOG_LEVEL_INFO,
|
|
||||||
type HasSettings,
|
type HasSettings,
|
||||||
type MetaEntry,
|
|
||||||
type UXFileInfoStub,
|
|
||||||
type MISSING_OR_ERROR,
|
|
||||||
type AUTO_MERGED,
|
|
||||||
type RemoteDBSettings,
|
|
||||||
type TweakValues,
|
|
||||||
type CouchDBCredentials,
|
|
||||||
} from "./lib/src/common/types.ts";
|
} from "./lib/src/common/types.ts";
|
||||||
import { type FileEventItem } from "./common/types.ts";
|
|
||||||
import { type SimpleStore } from "./lib/src/common/utils.ts";
|
import { type SimpleStore } from "./lib/src/common/utils.ts";
|
||||||
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/pouchdb/LiveSyncLocalDB.ts";
|
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/pouchdb/LiveSyncLocalDB.ts";
|
||||||
import {
|
import {
|
||||||
@@ -31,11 +15,10 @@ import { type KeyValueDatabase } from "./lib/src/interfaces/KeyValueDatabase.ts"
|
|||||||
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
|
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
|
||||||
import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts";
|
import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts";
|
||||||
import { ConfigSync } from "./features/ConfigSync/CmdConfigSync.ts";
|
import { ConfigSync } from "./features/ConfigSync/CmdConfigSync.ts";
|
||||||
import { reactiveSource, type ReactiveValue } from "./lib/src/dataobject/reactive.js";
|
import { reactiveSource, type ReactiveValue } from "octagonal-wheels/dataobject/reactive";
|
||||||
import { type LiveSyncJournalReplicatorEnv } from "./lib/src/replication/journal/LiveSyncJournalReplicator.js";
|
import { type LiveSyncJournalReplicatorEnv } from "./lib/src/replication/journal/LiveSyncJournalReplicator.js";
|
||||||
import { type LiveSyncCouchDBReplicatorEnv } from "./lib/src/replication/couchdb/LiveSyncReplicator.js";
|
import { type LiveSyncCouchDBReplicatorEnv } from "./lib/src/replication/couchdb/LiveSyncReplicator.js";
|
||||||
import type { CheckPointInfo } from "./lib/src/replication/journal/JournalSyncTypes.js";
|
import type { CheckPointInfo } from "./lib/src/replication/journal/JournalSyncTypes.js";
|
||||||
import { ObsHttpHandler } from "./modules/essentialObsidian/APILib/ObsHttpHandler.js";
|
|
||||||
import type { IObsidianModule } from "./modules/AbstractObsidianModule.ts";
|
import type { IObsidianModule } from "./modules/AbstractObsidianModule.ts";
|
||||||
|
|
||||||
import { ModuleDev } from "./modules/extras/ModuleDev.ts";
|
import { ModuleDev } from "./modules/extras/ModuleDev.ts";
|
||||||
@@ -51,6 +34,7 @@ import { ModuleObsidianSettings } from "./modules/features/ModuleObsidianSetting
|
|||||||
import { ModuleRedFlag } from "./modules/coreFeatures/ModuleRedFlag.ts";
|
import { ModuleRedFlag } from "./modules/coreFeatures/ModuleRedFlag.ts";
|
||||||
import { ModuleObsidianMenu } from "./modules/essentialObsidian/ModuleObsidianMenu.ts";
|
import { ModuleObsidianMenu } from "./modules/essentialObsidian/ModuleObsidianMenu.ts";
|
||||||
import { ModuleSetupObsidian } from "./modules/features/ModuleSetupObsidian.ts";
|
import { ModuleSetupObsidian } from "./modules/features/ModuleSetupObsidian.ts";
|
||||||
|
import { SetupManager } from "./modules/features/SetupManager.ts";
|
||||||
import type { StorageAccess } from "./modules/interfaces/StorageAccess.ts";
|
import type { StorageAccess } from "./modules/interfaces/StorageAccess.ts";
|
||||||
import type { Confirm } from "./lib/src/interfaces/Confirm.ts";
|
import type { Confirm } from "./lib/src/interfaces/Confirm.ts";
|
||||||
import type { Rebuilder } from "./modules/interfaces/DatabaseRebuilder.ts";
|
import type { Rebuilder } from "./modules/interfaces/DatabaseRebuilder.ts";
|
||||||
@@ -59,8 +43,7 @@ import { ModuleDatabaseFileAccess } from "./modules/core/ModuleDatabaseFileAcces
|
|||||||
import { ModuleFileHandler } from "./modules/core/ModuleFileHandler.ts";
|
import { ModuleFileHandler } from "./modules/core/ModuleFileHandler.ts";
|
||||||
import { ModuleObsidianAPI } from "./modules/essentialObsidian/ModuleObsidianAPI.ts";
|
import { ModuleObsidianAPI } from "./modules/essentialObsidian/ModuleObsidianAPI.ts";
|
||||||
import { ModuleObsidianEvents } from "./modules/essentialObsidian/ModuleObsidianEvents.ts";
|
import { ModuleObsidianEvents } from "./modules/essentialObsidian/ModuleObsidianEvents.ts";
|
||||||
import { injectModules, type AbstractModule } from "./modules/AbstractModule.ts";
|
import { type AbstractModule } from "./modules/AbstractModule.ts";
|
||||||
import type { ICoreModule } from "./modules/ModuleTypes.ts";
|
|
||||||
import { ModuleObsidianSettingDialogue } from "./modules/features/ModuleObsidianSettingTab.ts";
|
import { ModuleObsidianSettingDialogue } from "./modules/features/ModuleObsidianSettingTab.ts";
|
||||||
import { ModuleObsidianDocumentHistory } from "./modules/features/ModuleObsidianDocumentHistory.ts";
|
import { ModuleObsidianDocumentHistory } from "./modules/features/ModuleObsidianDocumentHistory.ts";
|
||||||
import { ModuleObsidianGlobalHistory } from "./modules/features/ModuleGlobalHistory.ts";
|
import { ModuleObsidianGlobalHistory } from "./modules/features/ModuleGlobalHistory.ts";
|
||||||
@@ -84,13 +67,17 @@ import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain.ts";
|
|||||||
import { ModuleExtraSyncObsidian } from "./modules/extraFeaturesObsidian/ModuleExtraSyncObsidian.ts";
|
import { ModuleExtraSyncObsidian } from "./modules/extraFeaturesObsidian/ModuleExtraSyncObsidian.ts";
|
||||||
import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
|
import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
|
||||||
import { P2PReplicator } from "./features/P2PSync/CmdP2PReplicator.ts";
|
import { P2PReplicator } from "./features/P2PSync/CmdP2PReplicator.ts";
|
||||||
|
import type { LiveSyncManagers } from "./lib/src/managers/LiveSyncManagers.ts";
|
||||||
|
import { ObsidianServiceHub } from "./modules/services/ObsidianServices.ts";
|
||||||
|
import type { InjectableServiceHub } from "./lib/src/services/InjectableServices.ts";
|
||||||
|
|
||||||
|
// function throwShouldBeOverridden(): never {
|
||||||
|
// throw new Error("This function should be overridden by the module.");
|
||||||
|
// }
|
||||||
|
// const InterceptiveAll = Promise.resolve(true);
|
||||||
|
// const InterceptiveEvery = Promise.resolve(true);
|
||||||
|
// const InterceptiveAny = Promise.resolve(undefined);
|
||||||
|
|
||||||
function throwShouldBeOverridden(): never {
|
|
||||||
throw new Error("This function should be overridden by the module.");
|
|
||||||
}
|
|
||||||
const InterceptiveAll = Promise.resolve(true);
|
|
||||||
const InterceptiveEvery = Promise.resolve(true);
|
|
||||||
const InterceptiveAny = Promise.resolve(undefined);
|
|
||||||
/**
|
/**
|
||||||
* All $prefixed functions are hooked by the modules. Be careful to call them directly.
|
* All $prefixed functions are hooked by the modules. Be careful to call them directly.
|
||||||
* Please refer to the module's source code to understand the function.
|
* Please refer to the module's source code to understand the function.
|
||||||
@@ -100,6 +87,13 @@ const InterceptiveAny = Promise.resolve(undefined);
|
|||||||
* $any : Process all modules until the first success.
|
* $any : Process all modules until the first success.
|
||||||
* $ : Other interceptive points. You should manually assign the module
|
* $ : Other interceptive points. You should manually assign the module
|
||||||
* All of above performed on injectModules function.
|
* All of above performed on injectModules function.
|
||||||
|
*
|
||||||
|
* No longer used! See AppLifecycleService in Services.ts.
|
||||||
|
* For a while, just commented out some previously used code. (sorry, some are deleted...)
|
||||||
|
* 'Convention over configuration' was a lie for me. At least, very lack of refactor-ability.
|
||||||
|
*
|
||||||
|
* Still some modules are separated, and connected by `ThroughHole` class.
|
||||||
|
* However, it is not a good design. I am going to manage the modules in a more explicit way.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class ObsidianLiveSyncPlugin
|
export default class ObsidianLiveSyncPlugin
|
||||||
@@ -111,6 +105,18 @@ export default class ObsidianLiveSyncPlugin
|
|||||||
LiveSyncCouchDBReplicatorEnv,
|
LiveSyncCouchDBReplicatorEnv,
|
||||||
HasSettings<ObsidianLiveSyncSettings>
|
HasSettings<ObsidianLiveSyncSettings>
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* The service hub for managing all services.
|
||||||
|
*/
|
||||||
|
_services: InjectableServiceHub = new ObsidianServiceHub(this);
|
||||||
|
get services() {
|
||||||
|
return this._services;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Bind functions to the service hub (for migration purpose).
|
||||||
|
*/
|
||||||
|
// bindFunctions = (this.serviceHub as ObsidianServiceHub).bindFunctions.bind(this.serviceHub);
|
||||||
|
|
||||||
// --> Module System
|
// --> Module System
|
||||||
getAddOn<T extends LiveSyncCommands>(cls: string) {
|
getAddOn<T extends LiveSyncCommands>(cls: string) {
|
||||||
for (const addon of this.addOns) {
|
for (const addon of this.addOns) {
|
||||||
@@ -171,46 +177,23 @@ export default class ObsidianLiveSyncPlugin
|
|||||||
new ModuleDev(this, this),
|
new ModuleDev(this, this),
|
||||||
new ModuleReplicateTest(this, this),
|
new ModuleReplicateTest(this, this),
|
||||||
new ModuleIntegratedTest(this, this),
|
new ModuleIntegratedTest(this, this),
|
||||||
|
new SetupManager(this, this),
|
||||||
] as (IObsidianModule | AbstractModule)[];
|
] as (IObsidianModule | AbstractModule)[];
|
||||||
injected = injectModules(this, [...this.modules, ...this.addOns] as ICoreModule[]);
|
|
||||||
|
getModule<T extends IObsidianModule>(constructor: new (...args: any[]) => T): T {
|
||||||
|
for (const module of this.modules) {
|
||||||
|
if (module.constructor === constructor) return module as T;
|
||||||
|
}
|
||||||
|
throw new Error(`Module ${constructor} not found or not loaded.`);
|
||||||
|
}
|
||||||
|
// injected = injectModules(this, [...this.modules, ...this.addOns] as ICoreModule[]);
|
||||||
// <-- Module System
|
// <-- Module System
|
||||||
|
|
||||||
$$isSuspended(): boolean {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$$setSuspended(value: boolean): void {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$$isDatabaseReady(): boolean {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$$getDeviceAndVaultName(): string {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
$$setDeviceAndVaultName(name: string): void {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$$addLog(message: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key = ""): void {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
$$isReady(): boolean {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
$$markIsReady(): void {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
$$resetIsReady(): void {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Following are plugged by the modules.
|
// Following are plugged by the modules.
|
||||||
|
|
||||||
settings!: ObsidianLiveSyncSettings;
|
settings!: ObsidianLiveSyncSettings;
|
||||||
localDatabase!: LiveSyncLocalDB;
|
localDatabase!: LiveSyncLocalDB;
|
||||||
|
managers!: LiveSyncManagers;
|
||||||
simpleStore!: SimpleStore<CheckPointInfo>;
|
simpleStore!: SimpleStore<CheckPointInfo>;
|
||||||
replicator!: LiveSyncAbstractReplicator;
|
replicator!: LiveSyncAbstractReplicator;
|
||||||
confirm!: Confirm;
|
confirm!: Confirm;
|
||||||
@@ -227,30 +210,6 @@ export default class ObsidianLiveSyncPlugin
|
|||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
$$markFileListPossiblyChanged(): void {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$$customFetchHandler(): ObsHttpHandler {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$$getLastPostFailedBySize(): boolean {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$$isStorageInsensitive(): boolean {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$$shouldCheckCaseInsensitive(): boolean {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$$isUnloaded(): boolean {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
requestCount = reactiveSource(0);
|
requestCount = reactiveSource(0);
|
||||||
responseCount = reactiveSource(0);
|
responseCount = reactiveSource(0);
|
||||||
totalQueued = reactiveSource(0);
|
totalQueued = reactiveSource(0);
|
||||||
@@ -275,101 +234,6 @@ export default class ObsidianLiveSyncPlugin
|
|||||||
syncStatus: "CLOSED" as DatabaseConnectingStatus,
|
syncStatus: "CLOSED" as DatabaseConnectingStatus,
|
||||||
});
|
});
|
||||||
|
|
||||||
$$isReloadingScheduled(): boolean {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
$$getReplicator(): LiveSyncAbstractReplicator {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$$connectRemoteCouchDB(
|
|
||||||
uri: string,
|
|
||||||
auth: CouchDBCredentials,
|
|
||||||
disableRequestURI: boolean,
|
|
||||||
passphrase: string | false,
|
|
||||||
useDynamicIterationCount: boolean,
|
|
||||||
performSetup: boolean,
|
|
||||||
skipInfo: boolean,
|
|
||||||
compression: boolean,
|
|
||||||
customHeaders: Record<string, string>,
|
|
||||||
useRequestAPI: boolean,
|
|
||||||
getPBKDF2Salt: () => Promise<Uint8Array>
|
|
||||||
): Promise<
|
|
||||||
| string
|
|
||||||
| {
|
|
||||||
db: PouchDB.Database<EntryDoc>;
|
|
||||||
info: PouchDB.Core.DatabaseInfo;
|
|
||||||
}
|
|
||||||
> {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$$isMobile(): boolean {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
$$vaultName(): string {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
// --> Path
|
|
||||||
|
|
||||||
$$getActiveFilePath(): FilePathWithPrefix | undefined {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
// <-- Path
|
|
||||||
|
|
||||||
// --> Path conversion
|
|
||||||
$$id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$$path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID> {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
// <!-- Path conversion
|
|
||||||
|
|
||||||
// --> Database
|
|
||||||
$$createPouchDBInstance<T extends object>(
|
|
||||||
name?: string,
|
|
||||||
options?: PouchDB.Configuration.DatabaseConfiguration
|
|
||||||
): PouchDB.Database<T> {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$allOnDBUnload(db: LiveSyncLocalDB): void {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$allOnDBClose(db: LiveSyncLocalDB): void {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// <!-- Database
|
|
||||||
|
|
||||||
$anyNewReplicator(settingOverride: Partial<ObsidianLiveSyncSettings> = {}): Promise<LiveSyncAbstractReplicator> {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
|
||||||
return InterceptiveEvery;
|
|
||||||
}
|
|
||||||
|
|
||||||
$everyOnResetDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
|
||||||
return InterceptiveEvery;
|
|
||||||
}
|
|
||||||
|
|
||||||
// end interfaces
|
|
||||||
|
|
||||||
$$getVaultName(): string {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$$getSimpleStore<T>(kind: string): SimpleStore<T> {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
// trench!: Trench;
|
|
||||||
|
|
||||||
// --> Events
|
// --> Events
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -410,326 +274,404 @@ export default class ObsidianLiveSyncPlugin
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$everyOnLayoutReady(): Promise<boolean> {
|
// $everyOnLayoutReady(): Promise<boolean> {
|
||||||
return InterceptiveEvery;
|
// //TODO: AppLifecycleService.onLayoutReady
|
||||||
}
|
// return InterceptiveEvery;
|
||||||
$everyOnFirstInitialize(): Promise<boolean> {
|
// }
|
||||||
return InterceptiveEvery;
|
// $everyOnFirstInitialize(): Promise<boolean> {
|
||||||
}
|
// //TODO: AppLifecycleService.onFirstInitialize
|
||||||
|
// return InterceptiveEvery;
|
||||||
|
// }
|
||||||
|
|
||||||
// Some Module should call this function to start the plugin.
|
// Some Module should call this function to start the plugin.
|
||||||
$$onLiveSyncReady(): Promise<false | undefined> {
|
// $$onLiveSyncReady(): Promise<false | undefined> {
|
||||||
throwShouldBeOverridden();
|
// //TODO: AppLifecycleService.onLiveSyncReady
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
$$wireUpEvents(): void {
|
// }
|
||||||
throwShouldBeOverridden();
|
// $$wireUpEvents(): void {
|
||||||
}
|
// //TODO: AppLifecycleService.wireUpEvents
|
||||||
$$onLiveSyncLoad(): Promise<void> {
|
// throwShouldBeOverridden();
|
||||||
throwShouldBeOverridden();
|
// }
|
||||||
}
|
// $$onLiveSyncLoad(): Promise<void> {
|
||||||
|
// //TODO: AppLifecycleService.onLoad
|
||||||
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$onLiveSyncUnload(): Promise<void> {
|
// $$onLiveSyncUnload(): Promise<void> {
|
||||||
throwShouldBeOverridden();
|
// //TODO: AppLifecycleService.onAppUnload
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$allScanStat(): Promise<boolean> {
|
// $allScanStat(): Promise<boolean> {
|
||||||
return InterceptiveAll;
|
// //TODO: AppLifecycleService.scanStartupIssues
|
||||||
}
|
// return InterceptiveAll;
|
||||||
$everyOnloadStart(): Promise<boolean> {
|
// }
|
||||||
return InterceptiveEvery;
|
// $everyOnloadStart(): Promise<boolean> {
|
||||||
}
|
// //TODO: AppLifecycleService.onInitialise
|
||||||
|
// return InterceptiveEvery;
|
||||||
|
// }
|
||||||
|
|
||||||
$everyOnloadAfterLoadSettings(): Promise<boolean> {
|
// $everyOnloadAfterLoadSettings(): Promise<boolean> {
|
||||||
return InterceptiveEvery;
|
// //TODO: AppLifecycleService.onApplyStartupLoaded
|
||||||
}
|
// return InterceptiveEvery;
|
||||||
|
// }
|
||||||
|
|
||||||
$everyOnload(): Promise<boolean> {
|
// $everyOnload(): Promise<boolean> {
|
||||||
return InterceptiveEvery;
|
// //TODO: AppLifecycleService.onLoaded
|
||||||
}
|
// return InterceptiveEvery;
|
||||||
|
// }
|
||||||
|
|
||||||
$anyHandlerProcessesFileEvent(item: FileEventItem): Promise<boolean | undefined> {
|
// $anyHandlerProcessesFileEvent(item: FileEventItem): Promise<boolean | undefined> {
|
||||||
return InterceptiveAny;
|
// //TODO: FileProcessingService.processFileEvent
|
||||||
}
|
// return InterceptiveAny;
|
||||||
|
// }
|
||||||
|
|
||||||
$allStartOnUnload(): Promise<boolean> {
|
// $allStartOnUnload(): Promise<boolean> {
|
||||||
return InterceptiveAll;
|
// //TODO: AppLifecycleService.onBeforeUnload
|
||||||
}
|
// return InterceptiveAll;
|
||||||
$allOnUnload(): Promise<boolean> {
|
// }
|
||||||
return InterceptiveAll;
|
// $allOnUnload(): Promise<boolean> {
|
||||||
}
|
// //TODO: AppLifecycleService.onUnload
|
||||||
|
// return InterceptiveAll;
|
||||||
|
// }
|
||||||
|
|
||||||
$$openDatabase(): Promise<boolean> {
|
// $$openDatabase(): Promise<boolean> {
|
||||||
throwShouldBeOverridden();
|
// // DatabaseService.openDatabase
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$realizeSettingSyncMode(): Promise<void> {
|
// $$realizeSettingSyncMode(): Promise<void> {
|
||||||
throwShouldBeOverridden();
|
// // SettingService.realiseSetting
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
$$performRestart() {
|
// }
|
||||||
throwShouldBeOverridden();
|
// $$performRestart() {
|
||||||
}
|
// // AppLifecycleService.performRestart
|
||||||
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$clearUsedPassphrase(): void {
|
// $$clearUsedPassphrase(): void {
|
||||||
throwShouldBeOverridden();
|
// // SettingService.clearUsedPassphrase
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$decryptSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
|
// $$decryptSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
|
||||||
throwShouldBeOverridden();
|
// // SettingService.decryptSettings
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
$$adjustSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
|
// }
|
||||||
throwShouldBeOverridden();
|
// $$adjustSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
|
||||||
}
|
// // SettingService.adjustSettings
|
||||||
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$loadSettings(): Promise<void> {
|
// $$loadSettings(): Promise<void> {
|
||||||
throwShouldBeOverridden();
|
// // SettingService.loadSettings
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$saveDeviceAndVaultName(): void {
|
// $$saveDeviceAndVaultName(): void {
|
||||||
throwShouldBeOverridden();
|
// // SettingService.saveDeviceAndVaultName
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$saveSettingData(): Promise<void> {
|
// $$saveSettingData(): Promise<void> {
|
||||||
throwShouldBeOverridden();
|
// // SettingService.saveSettingData
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$anyProcessOptionalFileEvent(path: FilePath): Promise<boolean | undefined> {
|
// $anyProcessOptionalFileEvent(path: FilePath): Promise<boolean | undefined> {
|
||||||
return InterceptiveAny;
|
// // FileProcessingService.processOptionalFileEvent
|
||||||
}
|
// return InterceptiveAny;
|
||||||
|
// }
|
||||||
|
|
||||||
$everyCommitPendingFileEvent(): Promise<boolean> {
|
// $everyCommitPendingFileEvent(): Promise<boolean> {
|
||||||
return InterceptiveEvery;
|
// // FileProcessingService.commitPendingFileEvent
|
||||||
}
|
// return InterceptiveEvery;
|
||||||
|
// }
|
||||||
|
|
||||||
// ->
|
// ->
|
||||||
$anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise<boolean | undefined | "newer"> {
|
// $anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise<boolean | undefined | "newer"> {
|
||||||
return InterceptiveAny;
|
// return InterceptiveAny;
|
||||||
}
|
// }
|
||||||
|
|
||||||
$$queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise<void> {
|
// $$queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise<void> {
|
||||||
throwShouldBeOverridden();
|
// // ConflictEventManager.queueCheckForConflictIfOpen
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$queueConflictCheck(file: FilePathWithPrefix): Promise<void> {
|
// $$queueConflictCheck(file: FilePathWithPrefix): Promise<void> {
|
||||||
throwShouldBeOverridden();
|
// // ConflictEventManager.queueCheckForConflict
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$waitForAllConflictProcessed(): Promise<boolean> {
|
// $$waitForAllConflictProcessed(): Promise<boolean> {
|
||||||
throwShouldBeOverridden();
|
// // ConflictEventManager.ensureAllConflictProcessed
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
//<-- Conflict Check
|
//<-- Conflict Check
|
||||||
|
|
||||||
$anyProcessOptionalSyncFiles(doc: LoadedEntry): Promise<boolean | undefined> {
|
// $anyProcessOptionalSyncFiles(doc: LoadedEntry): Promise<boolean | undefined> {
|
||||||
return InterceptiveAny;
|
// // ReplicationService.processOptionalSyncFile
|
||||||
}
|
// return InterceptiveAny;
|
||||||
|
// }
|
||||||
|
|
||||||
$anyProcessReplicatedDoc(doc: MetaEntry): Promise<boolean | undefined> {
|
// $anyProcessReplicatedDoc(doc: MetaEntry): Promise<boolean | undefined> {
|
||||||
return InterceptiveAny;
|
// // ReplicationService.processReplicatedDocument
|
||||||
}
|
// return InterceptiveAny;
|
||||||
|
// }
|
||||||
|
|
||||||
//---> Sync
|
//---> Sync
|
||||||
$$parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): void {
|
// $$parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): void {
|
||||||
throwShouldBeOverridden();
|
// // ReplicationService.parseSynchroniseResult
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$anyModuleParsedReplicationResultItem(docs: PouchDB.Core.ExistingDocument<EntryDoc>): Promise<boolean | undefined> {
|
// $anyModuleParsedReplicationResultItem(docs: PouchDB.Core.ExistingDocument<EntryDoc>): Promise<boolean | undefined> {
|
||||||
return InterceptiveAny;
|
// // ReplicationService.processVirtualDocument
|
||||||
}
|
// return InterceptiveAny;
|
||||||
$everyBeforeRealizeSetting(): Promise<boolean> {
|
// }
|
||||||
return InterceptiveEvery;
|
// $everyBeforeRealizeSetting(): Promise<boolean> {
|
||||||
}
|
// // SettingEventManager.beforeRealiseSetting
|
||||||
$everyAfterRealizeSetting(): Promise<boolean> {
|
// return InterceptiveEvery;
|
||||||
return InterceptiveEvery;
|
// }
|
||||||
}
|
// $everyAfterRealizeSetting(): Promise<boolean> {
|
||||||
$everyRealizeSettingSyncMode(): Promise<boolean> {
|
// // SettingEventManager.onSettingRealised
|
||||||
return InterceptiveEvery;
|
// return InterceptiveEvery;
|
||||||
}
|
// }
|
||||||
|
// $everyRealizeSettingSyncMode(): Promise<boolean> {
|
||||||
|
// // SettingEventManager.onRealiseSetting
|
||||||
|
// return InterceptiveEvery;
|
||||||
|
// }
|
||||||
|
|
||||||
$everyBeforeSuspendProcess(): Promise<boolean> {
|
// $everyBeforeSuspendProcess(): Promise<boolean> {
|
||||||
return InterceptiveEvery;
|
// // AppLifecycleService.onSuspending
|
||||||
}
|
// return InterceptiveEvery;
|
||||||
$everyOnResumeProcess(): Promise<boolean> {
|
// }
|
||||||
return InterceptiveEvery;
|
// $everyOnResumeProcess(): Promise<boolean> {
|
||||||
}
|
// // AppLifecycleService.onResuming
|
||||||
$everyAfterResumeProcess(): Promise<boolean> {
|
// return InterceptiveEvery;
|
||||||
return InterceptiveEvery;
|
// }
|
||||||
}
|
// $everyAfterResumeProcess(): Promise<boolean> {
|
||||||
|
// // AppLifecycleService.onResumed
|
||||||
|
// return InterceptiveEvery;
|
||||||
|
// }
|
||||||
|
|
||||||
$$fetchRemotePreferredTweakValues(trialSetting: RemoteDBSettings): Promise<TweakValues | false> {
|
// $$fetchRemotePreferredTweakValues(trialSetting: RemoteDBSettings): Promise<TweakValues | false> {
|
||||||
throwShouldBeOverridden();
|
// //TODO:TweakValueService.fetchRemotePreferred
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
$$checkAndAskResolvingMismatchedTweaks(preferred: Partial<TweakValues>): Promise<[TweakValues | boolean, boolean]> {
|
// }
|
||||||
throwShouldBeOverridden();
|
// $$checkAndAskResolvingMismatchedTweaks(preferred: Partial<TweakValues>): Promise<[TweakValues | boolean, boolean]> {
|
||||||
}
|
// //TODO:TweakValueService.checkAndAskResolvingMismatched
|
||||||
$$askResolvingMismatchedTweaks(preferredSource: TweakValues): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
|
// throwShouldBeOverridden();
|
||||||
throwShouldBeOverridden();
|
// }
|
||||||
}
|
// $$askResolvingMismatchedTweaks(preferredSource: TweakValues): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
|
||||||
|
// //TODO:TweakValueService.askResolvingMismatched
|
||||||
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$checkAndAskUseRemoteConfiguration(
|
// $$checkAndAskUseRemoteConfiguration(
|
||||||
settings: RemoteDBSettings
|
// settings: RemoteDBSettings
|
||||||
): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
|
// ): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
|
||||||
throwShouldBeOverridden();
|
// // TweakValueService.checkAndAskUseRemoteConfiguration
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$askUseRemoteConfiguration(
|
// $$askUseRemoteConfiguration(
|
||||||
trialSetting: RemoteDBSettings,
|
// trialSetting: RemoteDBSettings,
|
||||||
preferred: TweakValues
|
// preferred: TweakValues
|
||||||
): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
|
// ): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
|
||||||
throwShouldBeOverridden();
|
// // TweakValueService.askUseRemoteConfiguration
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
$everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
|
// }
|
||||||
return InterceptiveEvery;
|
// $everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
|
||||||
}
|
// // ReplicationService.beforeReplicate
|
||||||
$$replicate(showMessage: boolean = false): Promise<boolean | void> {
|
// return InterceptiveEvery;
|
||||||
throwShouldBeOverridden();
|
// }
|
||||||
}
|
|
||||||
$$replicateByEvent(showMessage: boolean = false): Promise<boolean | void> {
|
|
||||||
throwShouldBeOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$everyOnDatabaseInitialized(showingNotice: boolean): Promise<boolean> {
|
// $$canReplicate(showMessage: boolean = false): Promise<boolean> {
|
||||||
throwShouldBeOverridden();
|
// // ReplicationService.isReplicationReady
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$initializeDatabase(
|
// $$replicate(showMessage: boolean = false): Promise<boolean | void> {
|
||||||
showingNotice: boolean = false,
|
// // ReplicationService.replicate
|
||||||
reopenDatabase = true,
|
// throwShouldBeOverridden();
|
||||||
ignoreSuspending: boolean = false
|
// }
|
||||||
): Promise<boolean> {
|
// $$replicateByEvent(showMessage: boolean = false): Promise<boolean | void> {
|
||||||
throwShouldBeOverridden();
|
// // ReplicationService.replicateByEvent
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> {
|
// $everyOnDatabaseInitialized(showingNotice: boolean): Promise<boolean> {
|
||||||
return InterceptiveAny;
|
// // DatabaseEventService.onDatabaseInitialised
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$replicateAllToServer(
|
// $$initializeDatabase(
|
||||||
showingNotice: boolean = false,
|
// showingNotice: boolean = false,
|
||||||
sendChunksInBulkDisabled: boolean = false
|
// reopenDatabase = true,
|
||||||
): Promise<boolean> {
|
// ignoreSuspending: boolean = false
|
||||||
throwShouldBeOverridden();
|
// ): Promise<boolean> {
|
||||||
}
|
// // DatabaseEventService.initializeDatabase
|
||||||
$$replicateAllFromServer(showingNotice: boolean = false): Promise<boolean> {
|
// throwShouldBeOverridden();
|
||||||
throwShouldBeOverridden();
|
// }
|
||||||
}
|
|
||||||
|
// $anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> {
|
||||||
|
// // ReplicationService.checkConnectionFailure
|
||||||
|
// return InterceptiveAny;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// $$replicateAllToServer(
|
||||||
|
// showingNotice: boolean = false,
|
||||||
|
// sendChunksInBulkDisabled: boolean = false
|
||||||
|
// ): Promise<boolean> {
|
||||||
|
// // RemoteService.replicateAllToRemote
|
||||||
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
// $$replicateAllFromServer(showingNotice: boolean = false): Promise<boolean> {
|
||||||
|
// // RemoteService.replicateAllFromRemote
|
||||||
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
// Remote Governing
|
// Remote Governing
|
||||||
$$markRemoteLocked(lockByClean: boolean = false): Promise<void> {
|
// $$markRemoteLocked(lockByClean: boolean = false): Promise<void> {
|
||||||
throwShouldBeOverridden();
|
// // RemoteService.markLocked;
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$markRemoteUnlocked(): Promise<void> {
|
// $$markRemoteUnlocked(): Promise<void> {
|
||||||
throwShouldBeOverridden();
|
// // RemoteService.markUnlocked;
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$markRemoteResolved(): Promise<void> {
|
// $$markRemoteResolved(): Promise<void> {
|
||||||
throwShouldBeOverridden();
|
// // RemoteService.markResolved;
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
// <-- Remote Governing
|
// <-- Remote Governing
|
||||||
|
|
||||||
$$isFileSizeExceeded(size: number): boolean {
|
// $$isFileSizeExceeded(size: number): boolean {
|
||||||
throwShouldBeOverridden();
|
// // VaultService.isFileSizeTooLarge
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$performFullScan(showingNotice?: boolean, ignoreSuspending?: boolean): Promise<void> {
|
// $$performFullScan(showingNotice?: boolean, ignoreSuspending?: boolean): Promise<void> {
|
||||||
throwShouldBeOverridden();
|
// // VaultService.scanVault
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$anyResolveConflictByUI(
|
// $anyResolveConflictByUI(
|
||||||
filename: FilePathWithPrefix,
|
// filename: FilePathWithPrefix,
|
||||||
conflictCheckResult: diff_result
|
// conflictCheckResult: diff_result
|
||||||
): Promise<boolean | undefined> {
|
// ): Promise<boolean | undefined> {
|
||||||
return InterceptiveAny;
|
// // ConflictService.resolveConflictByUserInteraction
|
||||||
}
|
// return InterceptiveAny;
|
||||||
$$resolveConflictByDeletingRev(
|
// }
|
||||||
path: FilePathWithPrefix,
|
// $$resolveConflictByDeletingRev(
|
||||||
deleteRevision: string,
|
// path: FilePathWithPrefix,
|
||||||
subTitle = ""
|
// deleteRevision: string,
|
||||||
): Promise<typeof MISSING_OR_ERROR | typeof AUTO_MERGED> {
|
// subTitle = ""
|
||||||
throwShouldBeOverridden();
|
// ): Promise<typeof MISSING_OR_ERROR | typeof AUTO_MERGED> {
|
||||||
}
|
// // ConflictService.resolveByDeletingRevision
|
||||||
$$resolveConflict(filename: FilePathWithPrefix): Promise<void> {
|
// throwShouldBeOverridden();
|
||||||
throwShouldBeOverridden();
|
// }
|
||||||
}
|
// $$resolveConflict(filename: FilePathWithPrefix): Promise<void> {
|
||||||
$anyResolveConflictByNewest(filename: FilePathWithPrefix): Promise<boolean> {
|
// // ConflictService.resolveConflict
|
||||||
throwShouldBeOverridden();
|
// throwShouldBeOverridden();
|
||||||
}
|
// }
|
||||||
|
// $anyResolveConflictByNewest(filename: FilePathWithPrefix): Promise<boolean> {
|
||||||
|
// // ConflictService.resolveByNewest
|
||||||
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$resetLocalDatabase(): Promise<void> {
|
// $$resetLocalDatabase(): Promise<void> {
|
||||||
throwShouldBeOverridden();
|
// // DatabaseService.resetDatabase;
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$tryResetRemoteDatabase(): Promise<void> {
|
// $$tryResetRemoteDatabase(): Promise<void> {
|
||||||
throwShouldBeOverridden();
|
// // RemoteService.tryResetDatabase;
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$tryCreateRemoteDatabase(): Promise<void> {
|
// $$tryCreateRemoteDatabase(): Promise<void> {
|
||||||
throwShouldBeOverridden();
|
// // RemoteService.tryCreateDatabase;
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$isIgnoredByIgnoreFiles(file: string | UXFileInfoStub): Promise<boolean> {
|
// $$isIgnoredByIgnoreFiles(file: string | UXFileInfoStub): Promise<boolean> {
|
||||||
throwShouldBeOverridden();
|
// // VaultService.isIgnoredByIgnoreFiles
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$isTargetFile(file: string | UXFileInfoStub, keepFileCheckList = false): Promise<boolean> {
|
// $$isTargetFile(file: string | UXFileInfoStub, keepFileCheckList = false): Promise<boolean> {
|
||||||
throwShouldBeOverridden();
|
// // VaultService.isTargetFile
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$askReload(message?: string) {
|
// $$askReload(message?: string) {
|
||||||
throwShouldBeOverridden();
|
// // AppLifecycleService.askRestart
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
$$scheduleAppReload() {
|
// }
|
||||||
throwShouldBeOverridden();
|
// $$scheduleAppReload() {
|
||||||
}
|
// // AppLifecycleService.scheduleRestart
|
||||||
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
//--- Setup
|
//--- Setup
|
||||||
$allSuspendAllSync(): Promise<boolean> {
|
// $allSuspendAllSync(): Promise<boolean> {
|
||||||
return InterceptiveAll;
|
// // SettingEventManager.suspendAllSync
|
||||||
}
|
// return InterceptiveAll;
|
||||||
$allSuspendExtraSync(): Promise<boolean> {
|
// }
|
||||||
return InterceptiveAll;
|
// $allSuspendExtraSync(): Promise<boolean> {
|
||||||
}
|
// // SettingEventManager.suspendExtraSync
|
||||||
|
// return InterceptiveAll;
|
||||||
|
// }
|
||||||
|
|
||||||
$allAskUsingOptionalSyncFeature(opt: { enableFetch?: boolean; enableOverwrite?: boolean }): Promise<boolean> {
|
// $allAskUsingOptionalSyncFeature(opt: { enableFetch?: boolean; enableOverwrite?: boolean }): Promise<boolean> {
|
||||||
throwShouldBeOverridden();
|
// // SettingEventManager.suggestOptionalFeatures
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
$anyConfigureOptionalSyncFeature(mode: string): Promise<void> {
|
// }
|
||||||
throwShouldBeOverridden();
|
// $anyConfigureOptionalSyncFeature(mode: string): Promise<void> {
|
||||||
}
|
// // SettingEventManager.enableOptionalFeature
|
||||||
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
$$showView(viewType: string): Promise<void> {
|
// $$showView(viewType: string): Promise<void> {
|
||||||
throwShouldBeOverridden();
|
// // UIManager.showWindow //
|
||||||
}
|
// throwShouldBeOverridden();
|
||||||
|
// }
|
||||||
|
|
||||||
// For Development: Ensure reliability MORE AND MORE. May the this plug-in helps all of us.
|
// For Development: Ensure reliability MORE AND MORE. May the this plug-in helps all of us.
|
||||||
$everyModuleTest(): Promise<boolean> {
|
// $everyModuleTest(): Promise<boolean> {
|
||||||
return InterceptiveEvery;
|
// return InterceptiveEvery;
|
||||||
}
|
// }
|
||||||
$everyModuleTestMultiDevice(): Promise<boolean> {
|
// $everyModuleTestMultiDevice(): Promise<boolean> {
|
||||||
return InterceptiveEvery;
|
// return InterceptiveEvery;
|
||||||
}
|
// }
|
||||||
$$addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void {
|
// $$addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void {
|
||||||
throwShouldBeOverridden();
|
// throwShouldBeOverridden();
|
||||||
}
|
// }
|
||||||
|
|
||||||
_isThisModuleEnabled(): boolean {
|
// _isThisModuleEnabled(): boolean {
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
$anyGetAppId(): Promise<string | undefined> {
|
// $anyGetAppId(): Promise<string | undefined> {
|
||||||
return InterceptiveAny;
|
// // APIService.getAppId
|
||||||
}
|
// return InterceptiveAny;
|
||||||
|
// }
|
||||||
|
|
||||||
// Plug-in's overrideable functions
|
// Plug-in's overrideable functions
|
||||||
onload() {
|
onload() {
|
||||||
void this.$$onLiveSyncLoad();
|
void this.services.appLifecycle.onLoad();
|
||||||
}
|
}
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
await this.$$saveSettingData();
|
await this.services.setting.saveSettingData();
|
||||||
}
|
}
|
||||||
onunload() {
|
onunload() {
|
||||||
return void this.$$onLiveSyncUnload();
|
return void this.services.appLifecycle.onAppUnload();
|
||||||
}
|
}
|
||||||
// <-- Plug-in's overrideable functions
|
// <-- Plug-in's overrideable functions
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,35 @@
|
|||||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
|
||||||
import type { LOG_LEVEL } from "../lib/src/common/types";
|
import type { LOG_LEVEL } from "../lib/src/common/types";
|
||||||
import type { LiveSyncCore } from "../main";
|
import type { LiveSyncCore } from "../main";
|
||||||
import { unique } from "octagonal-wheels/collection";
|
import { __$checkInstanceBinding } from "../lib/src/dev/checks";
|
||||||
import type { IObsidianModule } from "./AbstractObsidianModule.ts";
|
// import { unique } from "octagonal-wheels/collection";
|
||||||
import type {
|
// import type { IObsidianModule } from "./AbstractObsidianModule.ts";
|
||||||
ICoreModuleBase,
|
// import type {
|
||||||
AllInjectableProps,
|
// ICoreModuleBase,
|
||||||
AllExecuteProps,
|
// AllInjectableProps,
|
||||||
EveryExecuteProps,
|
// AllExecuteProps,
|
||||||
AnyExecuteProps,
|
// EveryExecuteProps,
|
||||||
ICoreModule,
|
// AnyExecuteProps,
|
||||||
} from "./ModuleTypes";
|
// ICoreModule,
|
||||||
|
// } from "./ModuleTypes";
|
||||||
|
|
||||||
function isOverridableKey(key: string): key is keyof ICoreModuleBase {
|
// function isOverridableKey(key: string): key is keyof ICoreModuleBase {
|
||||||
return key.startsWith("$");
|
// return key.startsWith("$");
|
||||||
}
|
// }
|
||||||
|
|
||||||
function isInjectableKey(key: string): key is keyof AllInjectableProps {
|
// function isInjectableKey(key: string): key is keyof AllInjectableProps {
|
||||||
return key.startsWith("$$");
|
// return key.startsWith("$$");
|
||||||
}
|
// }
|
||||||
|
|
||||||
function isAllExecuteKey(key: string): key is keyof AllExecuteProps {
|
// function isAllExecuteKey(key: string): key is keyof AllExecuteProps {
|
||||||
return key.startsWith("$all");
|
// return key.startsWith("$all");
|
||||||
}
|
// }
|
||||||
function isEveryExecuteKey(key: string): key is keyof EveryExecuteProps {
|
// function isEveryExecuteKey(key: string): key is keyof EveryExecuteProps {
|
||||||
return key.startsWith("$every");
|
// return key.startsWith("$every");
|
||||||
}
|
// }
|
||||||
function isAnyExecuteKey(key: string): key is keyof AnyExecuteProps {
|
// function isAnyExecuteKey(key: string): key is keyof AnyExecuteProps {
|
||||||
return key.startsWith("$any");
|
// return key.startsWith("$any");
|
||||||
}
|
// }
|
||||||
/**
|
/**
|
||||||
* All $prefixed functions are hooked by the modules. Be careful to call them directly.
|
* All $prefixed functions are hooked by the modules. Be careful to call them directly.
|
||||||
* Please refer to the module's source code to understand the function.
|
* Please refer to the module's source code to understand the function.
|
||||||
@@ -39,100 +40,100 @@ function isAnyExecuteKey(key: string): key is keyof AnyExecuteProps {
|
|||||||
* $ : Other interceptive points. You should manually assign the module
|
* $ : Other interceptive points. You should manually assign the module
|
||||||
* All of above performed on injectModules function.
|
* All of above performed on injectModules function.
|
||||||
*/
|
*/
|
||||||
export function injectModules<T extends ICoreModule>(target: T, modules: ICoreModule[]) {
|
// export function injectModules<T extends ICoreModule>(target: T, modules: ICoreModule[]) {
|
||||||
const allKeys = unique([
|
// const allKeys = unique([
|
||||||
...Object.keys(Object.getOwnPropertyDescriptors(target)),
|
// ...Object.keys(Object.getOwnPropertyDescriptors(target)),
|
||||||
...Object.keys(Object.getOwnPropertyDescriptors(Object.getPrototypeOf(target))),
|
// ...Object.keys(Object.getOwnPropertyDescriptors(Object.getPrototypeOf(target))),
|
||||||
]).filter((e) => e.startsWith("$")) as (keyof ICoreModule)[];
|
// ]).filter((e) => e.startsWith("$")) as (keyof ICoreModule)[];
|
||||||
const moduleMap = new Map<string, IObsidianModule[]>();
|
// const moduleMap = new Map<string, IObsidianModule[]>();
|
||||||
for (const module of modules) {
|
// for (const module of modules) {
|
||||||
for (const key of allKeys) {
|
// for (const key of allKeys) {
|
||||||
if (isOverridableKey(key)) {
|
// if (isOverridableKey(key)) {
|
||||||
if (key in module) {
|
// if (key in module) {
|
||||||
const list = moduleMap.get(key) || [];
|
// const list = moduleMap.get(key) || [];
|
||||||
if (typeof module[key] === "function") {
|
// if (typeof module[key] === "function") {
|
||||||
module[key] = module[key].bind(module) as any;
|
// module[key] = module[key].bind(module) as any;
|
||||||
}
|
// }
|
||||||
list.push(module);
|
// list.push(module);
|
||||||
moduleMap.set(key, list);
|
// moduleMap.set(key, list);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
Logger(`Injecting modules for ${target.constructor.name}`, LOG_LEVEL_VERBOSE);
|
// Logger(`Injecting modules for ${target.constructor.name}`, LOG_LEVEL_VERBOSE);
|
||||||
for (const key of allKeys) {
|
// for (const key of allKeys) {
|
||||||
const modules = moduleMap.get(key) || [];
|
// const modules = moduleMap.get(key) || [];
|
||||||
if (isInjectableKey(key)) {
|
// if (isInjectableKey(key)) {
|
||||||
if (modules.length == 0) {
|
// if (modules.length == 0) {
|
||||||
throw new Error(`No module injected for ${key}. This is a fatal error.`);
|
// throw new Error(`No module injected for ${key}. This is a fatal error.`);
|
||||||
}
|
// }
|
||||||
target[key] = modules[0][key]! as any;
|
// target[key] = modules[0][key]! as any;
|
||||||
Logger(`[${modules[0].constructor.name}]: Injected ${key} `, LOG_LEVEL_VERBOSE);
|
// Logger(`[${modules[0].constructor.name}]: Injected ${key} `, LOG_LEVEL_VERBOSE);
|
||||||
} else if (isAllExecuteKey(key)) {
|
// } else if (isAllExecuteKey(key)) {
|
||||||
const modules = moduleMap.get(key) || [];
|
// const modules = moduleMap.get(key) || [];
|
||||||
target[key] = async (...args: any) => {
|
// target[key] = async (...args: any) => {
|
||||||
for (const module of modules) {
|
// for (const module of modules) {
|
||||||
try {
|
// try {
|
||||||
//@ts-ignore
|
// //@ts-ignore
|
||||||
await module[key]!(...args);
|
// await module[key]!(...args);
|
||||||
} catch (ex) {
|
// } catch (ex) {
|
||||||
Logger(`[${module.constructor.name}]: All handler for ${key} failed`, LOG_LEVEL_VERBOSE);
|
// Logger(`[${module.constructor.name}]: All handler for ${key} failed`, LOG_LEVEL_VERBOSE);
|
||||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
// Logger(ex, LOG_LEVEL_VERBOSE);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return true;
|
// return true;
|
||||||
};
|
// };
|
||||||
for (const module of modules) {
|
// for (const module of modules) {
|
||||||
Logger(`[${module.constructor.name}]: Injected (All) ${key} `, LOG_LEVEL_VERBOSE);
|
// Logger(`[${module.constructor.name}]: Injected (All) ${key} `, LOG_LEVEL_VERBOSE);
|
||||||
}
|
// }
|
||||||
} else if (isEveryExecuteKey(key)) {
|
// } else if (isEveryExecuteKey(key)) {
|
||||||
target[key] = async (...args: any) => {
|
// target[key] = async (...args: any) => {
|
||||||
for (const module of modules) {
|
// for (const module of modules) {
|
||||||
try {
|
// try {
|
||||||
//@ts-ignore:2556
|
// //@ts-ignore:2556
|
||||||
const ret = await module[key]!(...args);
|
// const ret = await module[key]!(...args);
|
||||||
if (ret !== undefined && !ret) {
|
// if (ret !== undefined && !ret) {
|
||||||
// Failed then return that falsy value.
|
// // Failed then return that falsy value.
|
||||||
return ret;
|
// return ret;
|
||||||
}
|
// }
|
||||||
} catch (ex) {
|
// } catch (ex) {
|
||||||
Logger(`[${module.constructor.name}]: Every handler for ${key} failed`);
|
// Logger(`[${module.constructor.name}]: Every handler for ${key} failed`);
|
||||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
// Logger(ex, LOG_LEVEL_VERBOSE);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return true;
|
// return true;
|
||||||
};
|
// };
|
||||||
for (const module of modules) {
|
// for (const module of modules) {
|
||||||
Logger(`[${module.constructor.name}]: Injected (Every) ${key} `, LOG_LEVEL_VERBOSE);
|
// Logger(`[${module.constructor.name}]: Injected (Every) ${key} `, LOG_LEVEL_VERBOSE);
|
||||||
}
|
// }
|
||||||
} else if (isAnyExecuteKey(key)) {
|
// } else if (isAnyExecuteKey(key)) {
|
||||||
//@ts-ignore
|
// //@ts-ignore
|
||||||
target[key] = async (...args: any[]) => {
|
// target[key] = async (...args: any[]) => {
|
||||||
for (const module of modules) {
|
// for (const module of modules) {
|
||||||
try {
|
// try {
|
||||||
//@ts-ignore:2556
|
// //@ts-ignore:2556
|
||||||
const ret = await module[key](...args);
|
// const ret = await module[key](...args);
|
||||||
// If truly value returned, then return that value.
|
// // If truly value returned, then return that value.
|
||||||
if (ret) {
|
// if (ret) {
|
||||||
return ret;
|
// return ret;
|
||||||
}
|
// }
|
||||||
} catch (ex) {
|
// } catch (ex) {
|
||||||
Logger(`[${module.constructor.name}]: Any handler for ${key} failed`);
|
// Logger(`[${module.constructor.name}]: Any handler for ${key} failed`);
|
||||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
// Logger(ex, LOG_LEVEL_VERBOSE);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return false;
|
// return false;
|
||||||
};
|
// };
|
||||||
for (const module of modules) {
|
// for (const module of modules) {
|
||||||
Logger(`[${module.constructor.name}]: Injected (Any) ${key} `, LOG_LEVEL_VERBOSE);
|
// Logger(`[${module.constructor.name}]: Injected (Any) ${key} `, LOG_LEVEL_VERBOSE);
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
Logger(`No injected handler for ${key} `, LOG_LEVEL_VERBOSE);
|
// Logger(`No injected handler for ${key} `, LOG_LEVEL_VERBOSE);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
Logger(`Injected modules for ${target.constructor.name}`, LOG_LEVEL_VERBOSE);
|
// Logger(`Injected modules for ${target.constructor.name}`, LOG_LEVEL_VERBOSE);
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
export abstract class AbstractModule {
|
export abstract class AbstractModule {
|
||||||
_log = (msg: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key?: string) => {
|
_log = (msg: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key?: string) => {
|
||||||
@@ -153,14 +154,18 @@ export abstract class AbstractModule {
|
|||||||
this.core.settings = value;
|
this.core.settings = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services) {
|
||||||
|
// Override if needed.
|
||||||
|
}
|
||||||
constructor(public core: LiveSyncCore) {
|
constructor(public core: LiveSyncCore) {
|
||||||
|
this.onBindFunction(core, core.services);
|
||||||
Logger(`[${this.constructor.name}] Loaded`, LOG_LEVEL_VERBOSE);
|
Logger(`[${this.constructor.name}] Loaded`, LOG_LEVEL_VERBOSE);
|
||||||
|
__$checkInstanceBinding(this);
|
||||||
}
|
}
|
||||||
saveSettings = this.core.saveSettings.bind(this.core);
|
saveSettings = this.core.saveSettings.bind(this.core);
|
||||||
|
|
||||||
// abstract $everyTest(): Promise<boolean>;
|
|
||||||
addTestResult(key: string, value: boolean, summary?: string, message?: string) {
|
addTestResult(key: string, value: boolean, summary?: string, message?: string) {
|
||||||
this.core.$$addTestResult(`${this.constructor.name}`, key, value, summary, message);
|
this.services.test.addTestResult(`${this.constructor.name}`, key, value, summary, message);
|
||||||
}
|
}
|
||||||
testDone(result: boolean = true) {
|
testDone(result: boolean = true) {
|
||||||
return Promise.resolve(result);
|
return Promise.resolve(result);
|
||||||
@@ -185,4 +190,8 @@ export abstract class AbstractModule {
|
|||||||
}
|
}
|
||||||
return this.testDone();
|
return this.testDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get services() {
|
||||||
|
return this.core._services;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,18 +37,18 @@ export abstract class AbstractObsidianModule extends AbstractModule {
|
|||||||
|
|
||||||
saveSettings = this.plugin.saveSettings.bind(this.plugin);
|
saveSettings = this.plugin.saveSettings.bind(this.plugin);
|
||||||
|
|
||||||
_isMainReady() {
|
isMainReady() {
|
||||||
return this.core.$$isReady();
|
return this.services.appLifecycle.isReady();
|
||||||
}
|
}
|
||||||
_isMainSuspended() {
|
isMainSuspended() {
|
||||||
return this.core.$$isSuspended();
|
return this.services.appLifecycle.isSuspended();
|
||||||
}
|
}
|
||||||
_isDatabaseReady() {
|
isDatabaseReady() {
|
||||||
return this.core.$$isDatabaseReady();
|
return this.services.database.isDatabaseReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
//should be overridden
|
//should be overridden
|
||||||
_isThisModuleEnabled() {
|
isThisModuleEnabled() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import type {
|
|||||||
DocumentID,
|
DocumentID,
|
||||||
} from "../../lib/src/common/types";
|
} from "../../lib/src/common/types";
|
||||||
import type { DatabaseFileAccess } from "../interfaces/DatabaseFileAccess";
|
import type { DatabaseFileAccess } from "../interfaces/DatabaseFileAccess";
|
||||||
import { type IObsidianModule } from "../AbstractObsidianModule.ts";
|
|
||||||
import { isPlainText, shouldBeIgnored, stripAllPrefixes } from "../../lib/src/string_and_binary/path";
|
import { isPlainText, shouldBeIgnored, stripAllPrefixes } from "../../lib/src/string_and_binary/path";
|
||||||
import {
|
import {
|
||||||
createBlob,
|
createBlob,
|
||||||
@@ -30,14 +29,15 @@ import {
|
|||||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||||
import { AbstractModule } from "../AbstractModule.ts";
|
import { AbstractModule } from "../AbstractModule.ts";
|
||||||
import { ICHeader } from "../../common/types.ts";
|
import { ICHeader } from "../../common/types.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidianModule, DatabaseFileAccess {
|
export class ModuleDatabaseFileAccess extends AbstractModule implements DatabaseFileAccess {
|
||||||
$everyOnload(): Promise<boolean> {
|
private _everyOnload(): Promise<boolean> {
|
||||||
this.core.databaseFileAccess = this;
|
this.core.databaseFileAccess = this;
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $everyModuleTest(): Promise<boolean> {
|
private async _everyModuleTest(): Promise<boolean> {
|
||||||
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
||||||
const testString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus nec nunc";
|
const testString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus nec nunc";
|
||||||
// Before test, we need to delete completely.
|
// Before test, we need to delete completely.
|
||||||
@@ -75,7 +75,7 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
|
|||||||
|
|
||||||
async checkIsTargetFile(file: UXFileInfoStub | FilePathWithPrefix): Promise<boolean> {
|
async checkIsTargetFile(file: UXFileInfoStub | FilePathWithPrefix): Promise<boolean> {
|
||||||
const path = getStoragePathFromUXFileInfo(file);
|
const path = getStoragePathFromUXFileInfo(file);
|
||||||
if (!(await this.core.$$isTargetFile(path))) {
|
if (!(await this.services.vault.isTargetFile(path))) {
|
||||||
this._log(`File is not target`, LOG_LEVEL_VERBOSE);
|
this._log(`File is not target`, LOG_LEVEL_VERBOSE);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -102,11 +102,11 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createChunks(file: UXFileInfo, force: boolean = false, skipCheck?: boolean): Promise<boolean> {
|
async createChunks(file: UXFileInfo, force: boolean = false, skipCheck?: boolean): Promise<boolean> {
|
||||||
return await this._store(file, force, skipCheck, true);
|
return await this.__store(file, force, skipCheck, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async store(file: UXFileInfo, force: boolean = false, skipCheck?: boolean): Promise<boolean> {
|
async store(file: UXFileInfo, force: boolean = false, skipCheck?: boolean): Promise<boolean> {
|
||||||
return await this._store(file, force, skipCheck, false);
|
return await this.__store(file, force, skipCheck, false);
|
||||||
}
|
}
|
||||||
async storeContent(path: FilePathWithPrefix, content: string): Promise<boolean> {
|
async storeContent(path: FilePathWithPrefix, content: string): Promise<boolean> {
|
||||||
const blob = createTextBlob(content);
|
const blob = createTextBlob(content);
|
||||||
@@ -124,10 +124,10 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
|
|||||||
body: blob,
|
body: blob,
|
||||||
isInternal,
|
isInternal,
|
||||||
};
|
};
|
||||||
return await this._store(dummyUXFileInfo, true, false, false);
|
return await this.__store(dummyUXFileInfo, true, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _store(
|
private async __store(
|
||||||
file: UXFileInfo,
|
file: UXFileInfo,
|
||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
skipCheck?: boolean,
|
skipCheck?: boolean,
|
||||||
@@ -177,7 +177,7 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const idMain = await this.core.$$path2id(fullPath);
|
const idMain = await this.services.path.path2id(fullPath);
|
||||||
|
|
||||||
const id = (idPrefix + idMain) as DocumentID;
|
const id = (idPrefix + idMain) as DocumentID;
|
||||||
const d: SavingEntry = {
|
const d: SavingEntry = {
|
||||||
@@ -345,4 +345,8 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
|
|||||||
eventHub.emitEvent(EVENT_FILE_SAVED);
|
eventHub.emitEvent(EVENT_FILE_SAVED);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
|
||||||
|
services.test.handleTest(this._everyModuleTest.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ import {
|
|||||||
getStoragePathFromUXFileInfo,
|
getStoragePathFromUXFileInfo,
|
||||||
markChangesAreSame,
|
markChangesAreSame,
|
||||||
} from "../../common/utils";
|
} from "../../common/utils";
|
||||||
import { getDocDataAsArray, isDocContentSame, readContent } from "../../lib/src/common/utils";
|
import { getDocDataAsArray, isDocContentSame, readAsBlob, readContent } from "../../lib/src/common/utils";
|
||||||
import { shouldBeIgnored } from "../../lib/src/string_and_binary/path";
|
import { shouldBeIgnored } from "../../lib/src/string_and_binary/path";
|
||||||
import type { ICoreModule } from "../ModuleTypes";
|
|
||||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||||
import { eventHub } from "../../common/events.ts";
|
import { eventHub } from "../../common/events.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
export class ModuleFileHandler extends AbstractModule {
|
||||||
get db() {
|
get db() {
|
||||||
return this.core.databaseFileAccess;
|
return this.core.databaseFileAccess;
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
|||||||
return this.core.storageAccess;
|
return this.core.storageAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
$everyOnloadStart(): Promise<boolean> {
|
_everyOnloadStart(): Promise<boolean> {
|
||||||
this.core.fileHandler = this;
|
this.core.fileHandler = this;
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
|||||||
info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix,
|
info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix,
|
||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
onlyChunks: boolean = false
|
onlyChunks: boolean = false
|
||||||
): Promise<boolean | undefined> {
|
): Promise<boolean> {
|
||||||
const file = typeof info === "string" ? this.storage.getFileStub(info) : info;
|
const file = typeof info === "string" ? this.storage.getFileStub(info) : info;
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
this._log(`File ${info} is not exist on the storage`, LOG_LEVEL_VERBOSE);
|
this._log(`File ${info} is not exist on the storage`, LOG_LEVEL_VERBOSE);
|
||||||
@@ -94,10 +94,14 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
|||||||
let readFile: UXFileInfo | undefined = undefined;
|
let readFile: UXFileInfo | undefined = undefined;
|
||||||
if (!shouldApplied) {
|
if (!shouldApplied) {
|
||||||
readFile = await this.readFileFromStub(file);
|
readFile = await this.readFileFromStub(file);
|
||||||
|
if (!readFile) {
|
||||||
|
this._log(`File ${file.path} is not exist on the storage`, LOG_LEVEL_NOTICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (await isDocContentSame(getDocDataAsArray(entry.data), readFile.body)) {
|
if (await isDocContentSame(getDocDataAsArray(entry.data), readFile.body)) {
|
||||||
// Timestamp is different but the content is same. therefore, two timestamps should be handled as same.
|
// Timestamp is different but the content is same. therefore, two timestamps should be handled as same.
|
||||||
// So, mark the changes are same.
|
// So, mark the changes are same.
|
||||||
markChangesAreSame(file, file.stat.mtime, entry.mtime);
|
markChangesAreSame(readFile, readFile.stat.mtime, entry.mtime);
|
||||||
} else {
|
} else {
|
||||||
shouldApplied = true;
|
shouldApplied = true;
|
||||||
}
|
}
|
||||||
@@ -125,7 +129,7 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteFileFromDB(info: UXFileInfoStub | UXInternalFileInfoStub | FilePath): Promise<boolean | undefined> {
|
async deleteFileFromDB(info: UXFileInfoStub | UXInternalFileInfoStub | FilePath): Promise<boolean> {
|
||||||
const file = typeof info === "string" ? this.storage.getFileStub(info) : info;
|
const file = typeof info === "string" ? this.storage.getFileStub(info) : info;
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
this._log(`File ${info} is not exist on the storage`, LOG_LEVEL_VERBOSE);
|
this._log(`File ${info} is not exist on the storage`, LOG_LEVEL_VERBOSE);
|
||||||
@@ -222,7 +226,7 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
|||||||
// NO OP
|
// NO OP
|
||||||
} else {
|
} else {
|
||||||
// If not, then it should be checked. and will be processed later (i.e., after the conflict is resolved).
|
// If not, then it should be checked. and will be processed later (i.e., after the conflict is resolved).
|
||||||
await this.core.$$queueConflictCheckIfOpen(path);
|
await this.services.conflict.queueCheckForIfOpen(path);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,6 +260,17 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
|||||||
this._log(`File ${path} is not exist on the database`, LOG_LEVEL_VERBOSE);
|
this._log(`File ${path} is not exist on the database`, LOG_LEVEL_VERBOSE);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we want to process size mismatched files -- in case of having files created by some integrations, enable the toggle.
|
||||||
|
if (!this.settings.processSizeMismatchedFiles) {
|
||||||
|
// Check the file is not corrupted
|
||||||
|
// (Zero is a special case, may be created by some APIs and it might be acceptable).
|
||||||
|
if (docRead.size != 0 && docRead.size !== readAsBlob(docRead).size) {
|
||||||
|
this._log(`File ${path} seems to be corrupted! Writing prevented.`, LOG_LEVEL_NOTICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const docData = readContent(docRead);
|
const docData = readContent(docRead);
|
||||||
|
|
||||||
if (existOnStorage && !force) {
|
if (existOnStorage && !force) {
|
||||||
@@ -302,11 +317,11 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $anyHandlerProcessesFileEvent(item: FileEventItem): Promise<boolean | undefined> {
|
private async _anyHandlerProcessesFileEvent(item: FileEventItem): Promise<boolean> {
|
||||||
const eventItem = item.args;
|
const eventItem = item.args;
|
||||||
const type = item.type;
|
const type = item.type;
|
||||||
const path = eventItem.file.path;
|
const path = eventItem.file.path;
|
||||||
if (!(await this.core.$$isTargetFile(path))) {
|
if (!(await this.services.vault.isTargetFile(path))) {
|
||||||
this._log(`File ${path} is not the target file`, LOG_LEVEL_VERBOSE);
|
this._log(`File ${path} is not the target file`, LOG_LEVEL_VERBOSE);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -332,12 +347,16 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async $anyProcessReplicatedDoc(entry: MetaEntry): Promise<boolean | undefined> {
|
async _anyProcessReplicatedDoc(entry: MetaEntry): Promise<boolean> {
|
||||||
return await serialized(entry.path, async () => {
|
return await serialized(entry.path, async () => {
|
||||||
if (!(await this.core.$$isTargetFile(entry.path))) {
|
if (!(await this.services.vault.isTargetFile(entry.path))) {
|
||||||
this._log(`File ${entry.path} is not the target file`, LOG_LEVEL_VERBOSE);
|
this._log(`File ${entry.path} is not the target file`, LOG_LEVEL_VERBOSE);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (this.services.vault.isFileSizeTooLarge(entry.size)) {
|
||||||
|
this._log(`File ${entry.path} is too large (on database) to be processed`, LOG_LEVEL_VERBOSE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (shouldBeIgnored(entry.path)) {
|
if (shouldBeIgnored(entry.path)) {
|
||||||
this._log(`File ${entry.path} should be ignored`, LOG_LEVEL_VERBOSE);
|
this._log(`File ${entry.path} should be ignored`, LOG_LEVEL_VERBOSE);
|
||||||
return false;
|
return false;
|
||||||
@@ -350,8 +369,12 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
|||||||
// Nothing to do and other modules should also nothing to do.
|
// Nothing to do and other modules should also nothing to do.
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
if (targetFile && this.services.vault.isFileSizeTooLarge(targetFile.stat.size)) {
|
||||||
|
this._log(`File ${targetFile.path} is too large (on storage) to be processed`, LOG_LEVEL_VERBOSE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
this._log(
|
this._log(
|
||||||
`Processing ${path} (${entry._id.substring(0, 8)}: ${entry._rev?.substring(0, 5)}) :Started...`,
|
`Processing ${path} (${entry._id.substring(0, 8)} :${entry._rev?.substring(0, 5)}) : Started...`,
|
||||||
LOG_LEVEL_VERBOSE
|
LOG_LEVEL_VERBOSE
|
||||||
);
|
);
|
||||||
// Before writing (or skipped ), merging dialogue should be cancelled.
|
// Before writing (or skipped ), merging dialogue should be cancelled.
|
||||||
@@ -380,7 +403,11 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
|||||||
};
|
};
|
||||||
const total = filesStorageSrc.length;
|
const total = filesStorageSrc.length;
|
||||||
const procAllChunks = filesStorageSrc.map(async (file) => {
|
const procAllChunks = filesStorageSrc.map(async (file) => {
|
||||||
if (!(await this.core.$$isTargetFile(file))) {
|
if (!(await this.services.vault.isTargetFile(file))) {
|
||||||
|
incProcessed();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.services.vault.isFileSizeTooLarge(file.stat.size)) {
|
||||||
incProcessed();
|
incProcessed();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -405,4 +432,9 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
|||||||
"chunkCreation"
|
"chunkCreation"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||||
|
services.fileProcessing.handleProcessFileEvent(this._anyHandlerProcessesFileEvent.bind(this));
|
||||||
|
services.replication.handleProcessSynchroniseResult(this._anyProcessReplicatedDoc.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,45 @@ import { $msg } from "../../lib/src/common/i18n";
|
|||||||
import { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB.ts";
|
import { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB.ts";
|
||||||
import { initializeStores } from "../../common/stores.ts";
|
import { initializeStores } from "../../common/stores.ts";
|
||||||
import { AbstractModule } from "../AbstractModule.ts";
|
import { AbstractModule } from "../AbstractModule.ts";
|
||||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
import { LiveSyncManagers } from "../../lib/src/managers/LiveSyncManagers.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
export class ModuleLocalDatabaseObsidian extends AbstractModule implements ICoreModule {
|
export class ModuleLocalDatabaseObsidian extends AbstractModule {
|
||||||
$everyOnloadStart(): Promise<boolean> {
|
_everyOnloadStart(): Promise<boolean> {
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
async $$openDatabase(): Promise<boolean> {
|
private async _openDatabase(): Promise<boolean> {
|
||||||
if (this.localDatabase != null) {
|
if (this.localDatabase != null) {
|
||||||
await this.localDatabase.close();
|
await this.localDatabase.close();
|
||||||
}
|
}
|
||||||
const vaultName = this.core.$$getVaultName();
|
const vaultName = this.services.vault.getVaultName();
|
||||||
this._log($msg("moduleLocalDatabase.logWaitingForReady"));
|
this._log($msg("moduleLocalDatabase.logWaitingForReady"));
|
||||||
|
const getDB = () => this.core.localDatabase.localDatabase;
|
||||||
|
const getSettings = () => this.core.settings;
|
||||||
|
this.core.managers = new LiveSyncManagers({
|
||||||
|
get database() {
|
||||||
|
return getDB();
|
||||||
|
},
|
||||||
|
getActiveReplicator: () => this.core.replicator,
|
||||||
|
id2path: this.services.path.id2path,
|
||||||
|
// path2id: this.core.$$path2id.bind(this.core),
|
||||||
|
path2id: this.services.path.path2id,
|
||||||
|
get settings() {
|
||||||
|
return getSettings();
|
||||||
|
},
|
||||||
|
});
|
||||||
this.core.localDatabase = new LiveSyncLocalDB(vaultName, this.core);
|
this.core.localDatabase = new LiveSyncLocalDB(vaultName, this.core);
|
||||||
|
|
||||||
initializeStores(vaultName);
|
initializeStores(vaultName);
|
||||||
return await this.localDatabase.initializeDatabase();
|
return await this.localDatabase.initializeDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
$$isDatabaseReady(): boolean {
|
_isDatabaseReady(): boolean {
|
||||||
return this.localDatabase != null && this.localDatabase.isReady;
|
return this.localDatabase != null && this.localDatabase.isReady;
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.database.handleIsDatabaseReady(this._isDatabaseReady.bind(this));
|
||||||
|
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||||
|
services.database.handleOpenDatabase(this._openDatabase.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,41 @@
|
|||||||
import { PeriodicProcessor } from "../../common/utils";
|
import { PeriodicProcessor } from "../../common/utils";
|
||||||
|
import type { LiveSyncCore } from "../../main";
|
||||||
import { AbstractModule } from "../AbstractModule";
|
import { AbstractModule } from "../AbstractModule";
|
||||||
import type { ICoreModule } from "../ModuleTypes";
|
|
||||||
|
|
||||||
export class ModulePeriodicProcess extends AbstractModule implements ICoreModule {
|
export class ModulePeriodicProcess extends AbstractModule {
|
||||||
periodicSyncProcessor = new PeriodicProcessor(this.core, async () => await this.core.$$replicate());
|
periodicSyncProcessor = new PeriodicProcessor(this.core, async () => await this.services.replication.replicate());
|
||||||
|
|
||||||
_disablePeriodic() {
|
disablePeriodic() {
|
||||||
this.periodicSyncProcessor?.disable();
|
this.periodicSyncProcessor?.disable();
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
_resumePeriodic() {
|
resumePeriodic() {
|
||||||
this.periodicSyncProcessor.enable(
|
this.periodicSyncProcessor.enable(
|
||||||
this.settings.periodicReplication ? this.settings.periodicReplicationInterval * 1000 : 0
|
this.settings.periodicReplication ? this.settings.periodicReplicationInterval * 1000 : 0
|
||||||
);
|
);
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
$allOnUnload() {
|
private _allOnUnload() {
|
||||||
return this._disablePeriodic();
|
return this.disablePeriodic();
|
||||||
}
|
}
|
||||||
$everyBeforeRealizeSetting(): Promise<boolean> {
|
private _everyBeforeRealizeSetting(): Promise<boolean> {
|
||||||
return this._disablePeriodic();
|
return this.disablePeriodic();
|
||||||
}
|
}
|
||||||
$everyBeforeSuspendProcess(): Promise<boolean> {
|
private _everyBeforeSuspendProcess(): Promise<boolean> {
|
||||||
return this._disablePeriodic();
|
return this.disablePeriodic();
|
||||||
}
|
}
|
||||||
$everyAfterResumeProcess(): Promise<boolean> {
|
private _everyAfterResumeProcess(): Promise<boolean> {
|
||||||
return this._resumePeriodic();
|
return this.resumePeriodic();
|
||||||
}
|
}
|
||||||
$everyAfterRealizeSetting(): Promise<boolean> {
|
private _everyAfterRealizeSetting(): Promise<boolean> {
|
||||||
return this._resumePeriodic();
|
return this.resumePeriodic();
|
||||||
|
}
|
||||||
|
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleOnUnload(this._allOnUnload.bind(this));
|
||||||
|
services.setting.handleBeforeRealiseSetting(this._everyBeforeRealizeSetting.bind(this));
|
||||||
|
services.setting.handleSettingRealised(this._everyAfterRealizeSetting.bind(this));
|
||||||
|
services.appLifecycle.handleOnSuspending(this._everyBeforeSuspendProcess.bind(this));
|
||||||
|
services.appLifecycle.handleOnResumed(this._everyAfterResumeProcess.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { AbstractModule } from "../AbstractModule";
|
import { AbstractModule } from "../AbstractModule";
|
||||||
import type { ICoreModule } from "../ModuleTypes";
|
|
||||||
import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
|
import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
|
||||||
|
import type { LiveSyncCore } from "../../main";
|
||||||
|
|
||||||
export class ModulePouchDB extends AbstractModule implements ICoreModule {
|
export class ModulePouchDB extends AbstractModule {
|
||||||
$$createPouchDBInstance<T extends object>(
|
_createPouchDBInstance<T extends object>(
|
||||||
name?: string,
|
name?: string,
|
||||||
options?: PouchDB.Configuration.DatabaseConfiguration
|
options?: PouchDB.Configuration.DatabaseConfiguration
|
||||||
): PouchDB.Database<T> {
|
): PouchDB.Database<T> {
|
||||||
@@ -16,4 +16,7 @@ export class ModulePouchDB extends AbstractModule implements ICoreModule {
|
|||||||
}
|
}
|
||||||
return new PouchDB(name, optionPass);
|
return new PouchDB(name, optionPass);
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.database.handleCreatePouchDBInstance(this._createPouchDBInstance.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import {
|
|||||||
} from "../../lib/src/common/types.ts";
|
} from "../../lib/src/common/types.ts";
|
||||||
import { AbstractModule } from "../AbstractModule.ts";
|
import { AbstractModule } from "../AbstractModule.ts";
|
||||||
import type { Rebuilder } from "../interfaces/DatabaseRebuilder.ts";
|
import type { Rebuilder } from "../interfaces/DatabaseRebuilder.ts";
|
||||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
|
||||||
import type { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator.ts";
|
import type { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator.ts";
|
||||||
import { fetchAllUsedChunks } from "@/lib/src/pouchdb/chunks.ts";
|
import { fetchAllUsedChunks } from "@/lib/src/pouchdb/chunks.ts";
|
||||||
import { EVENT_DATABASE_REBUILT, eventHub } from "src/common/events.ts";
|
import { EVENT_DATABASE_REBUILT, eventHub } from "src/common/events.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebuilder {
|
export class ModuleRebuilder extends AbstractModule implements Rebuilder {
|
||||||
$everyOnload(): Promise<boolean> {
|
private _everyOnload(): Promise<boolean> {
|
||||||
this.core.rebuilder = this;
|
this.core.rebuilder = this;
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,14 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async informOptionalFeatures() {
|
||||||
|
await this.core.services.UI.showMarkdownDialog(
|
||||||
|
"All optional features are disabled",
|
||||||
|
`Customisation Sync and Hidden File Sync will all be disabled.
|
||||||
|
Please enable them from the settings screen after setup is complete.`,
|
||||||
|
["OK"]
|
||||||
|
);
|
||||||
|
}
|
||||||
async askUsingOptionalFeature(opt: { enableFetch?: boolean; enableOverwrite?: boolean }) {
|
async askUsingOptionalFeature(opt: { enableFetch?: boolean; enableOverwrite?: boolean }) {
|
||||||
if (
|
if (
|
||||||
(await this.core.confirm.askYesNoDialog(
|
(await this.core.confirm.askYesNoDialog(
|
||||||
@@ -43,47 +51,49 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
|
|||||||
{ title: "Enable extra features", defaultOption: "No", timeout: 15 }
|
{ title: "Enable extra features", defaultOption: "No", timeout: 15 }
|
||||||
)) == "yes"
|
)) == "yes"
|
||||||
) {
|
) {
|
||||||
await this.core.$allAskUsingOptionalSyncFeature(opt);
|
await this.services.setting.suggestOptionalFeatures(opt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async rebuildRemote() {
|
async rebuildRemote() {
|
||||||
await this.core.$allSuspendExtraSync();
|
await this.services.setting.suspendExtraSync();
|
||||||
this.core.settings.isConfigured = true;
|
this.core.settings.isConfigured = true;
|
||||||
|
|
||||||
await this.core.$$realizeSettingSyncMode();
|
await this.services.setting.realiseSetting();
|
||||||
await this.core.$$markRemoteLocked();
|
await this.services.remote.markLocked();
|
||||||
await this.core.$$tryResetRemoteDatabase();
|
await this.services.remote.tryResetDatabase();
|
||||||
await this.core.$$markRemoteLocked();
|
await this.services.remote.markLocked();
|
||||||
await delay(500);
|
await delay(500);
|
||||||
await this.askUsingOptionalFeature({ enableOverwrite: true });
|
// await this.askUsingOptionalFeature({ enableOverwrite: true });
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
await this.core.$$replicateAllToServer(true);
|
await this.services.remote.replicateAllToRemote(true);
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
await this.core.$$replicateAllToServer(true, true);
|
await this.services.remote.replicateAllToRemote(true, true);
|
||||||
|
await this.informOptionalFeatures();
|
||||||
}
|
}
|
||||||
$rebuildRemote(): Promise<void> {
|
$rebuildRemote(): Promise<void> {
|
||||||
return this.rebuildRemote();
|
return this.rebuildRemote();
|
||||||
}
|
}
|
||||||
|
|
||||||
async rebuildEverything() {
|
async rebuildEverything() {
|
||||||
await this.core.$allSuspendExtraSync();
|
await this.services.setting.suspendExtraSync();
|
||||||
await this.askUseNewAdapter();
|
await this.askUseNewAdapter();
|
||||||
this.core.settings.isConfigured = true;
|
this.core.settings.isConfigured = true;
|
||||||
await this.core.$$realizeSettingSyncMode();
|
await this.services.setting.realiseSetting();
|
||||||
await this.resetLocalDatabase();
|
await this.resetLocalDatabase();
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
await this.core.$$initializeDatabase(true, true, true);
|
await this.services.databaseEvents.initialiseDatabase(true, true, true);
|
||||||
await this.core.$$markRemoteLocked();
|
await this.services.remote.markLocked();
|
||||||
await this.core.$$tryResetRemoteDatabase();
|
await this.services.remote.tryResetDatabase();
|
||||||
await this.core.$$markRemoteLocked();
|
await this.services.remote.markLocked();
|
||||||
await delay(500);
|
await delay(500);
|
||||||
// We do not have any other devices' data, so we do not need to ask for overwriting.
|
// We do not have any other devices' data, so we do not need to ask for overwriting.
|
||||||
await this.askUsingOptionalFeature({ enableOverwrite: false });
|
// await this.askUsingOptionalFeature({ enableOverwrite: false });
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
await this.core.$$replicateAllToServer(true);
|
await this.services.remote.replicateAllToRemote(true);
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
await this.core.$$replicateAllToServer(true, true);
|
await this.services.remote.replicateAllToRemote(true, true);
|
||||||
|
await this.informOptionalFeatures();
|
||||||
}
|
}
|
||||||
|
|
||||||
$rebuildEverything(): Promise<void> {
|
$rebuildEverything(): Promise<void> {
|
||||||
@@ -101,7 +111,7 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
|
|||||||
this._log("Could not create red_flag_rebuild.md", LOG_LEVEL_NOTICE);
|
this._log("Could not create red_flag_rebuild.md", LOG_LEVEL_NOTICE);
|
||||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
this._log(ex, LOG_LEVEL_VERBOSE);
|
||||||
}
|
}
|
||||||
this.core.$$performRestart();
|
this.services.appLifecycle.performRestart();
|
||||||
}
|
}
|
||||||
async scheduleFetch(): Promise<void> {
|
async scheduleFetch(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -110,20 +120,20 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
|
|||||||
this._log("Could not create red_flag_fetch.md", LOG_LEVEL_NOTICE);
|
this._log("Could not create red_flag_fetch.md", LOG_LEVEL_NOTICE);
|
||||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
this._log(ex, LOG_LEVEL_VERBOSE);
|
||||||
}
|
}
|
||||||
this.core.$$performRestart();
|
this.services.appLifecycle.performRestart();
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$tryResetRemoteDatabase(): Promise<void> {
|
private async _tryResetRemoteDatabase(): Promise<void> {
|
||||||
await this.core.replicator.tryResetRemoteDatabase(this.settings);
|
await this.core.replicator.tryResetRemoteDatabase(this.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$tryCreateRemoteDatabase(): Promise<void> {
|
private async _tryCreateRemoteDatabase(): Promise<void> {
|
||||||
await this.core.replicator.tryCreateRemoteDatabase(this.settings);
|
await this.core.replicator.tryCreateRemoteDatabase(this.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$resetLocalDatabase(): Promise<void> {
|
private async _resetLocalDatabase(): Promise<boolean> {
|
||||||
this.core.storageAccess.clearTouched();
|
this.core.storageAccess.clearTouched();
|
||||||
await this.localDatabase.resetDatabase();
|
return await this.localDatabase.resetDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
async suspendAllSync() {
|
async suspendAllSync() {
|
||||||
@@ -134,7 +144,7 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
|
|||||||
this.core.settings.syncOnStart = false;
|
this.core.settings.syncOnStart = false;
|
||||||
this.core.settings.syncOnFileOpen = false;
|
this.core.settings.syncOnFileOpen = false;
|
||||||
this.core.settings.syncAfterMerge = false;
|
this.core.settings.syncAfterMerge = false;
|
||||||
await this.core.$allSuspendExtraSync();
|
await this.services.setting.suspendExtraSync();
|
||||||
}
|
}
|
||||||
async suspendReflectingDatabase() {
|
async suspendReflectingDatabase() {
|
||||||
if (this.core.settings.doNotSuspendOnFetching) return;
|
if (this.core.settings.doNotSuspendOnFetching) return;
|
||||||
@@ -153,8 +163,8 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
|
|||||||
this._log(`Database and storage reflection has been resumed!`, LOG_LEVEL_NOTICE);
|
this._log(`Database and storage reflection has been resumed!`, LOG_LEVEL_NOTICE);
|
||||||
this.core.settings.suspendParseReplicationResult = false;
|
this.core.settings.suspendParseReplicationResult = false;
|
||||||
this.core.settings.suspendFileWatching = false;
|
this.core.settings.suspendFileWatching = false;
|
||||||
await this.core.$$performFullScan(true);
|
await this.services.vault.scanVault(true);
|
||||||
await this.core.$everyBeforeReplicate(false); //TODO: Check actual need of this.
|
await this.services.replication.onBeforeReplicate(false); //TODO: Check actual need of this.
|
||||||
await this.core.saveSettings();
|
await this.core.saveSettings();
|
||||||
}
|
}
|
||||||
async askUseNewAdapter() {
|
async askUseNewAdapter() {
|
||||||
@@ -177,36 +187,38 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fetchLocal(makeLocalChunkBeforeSync?: boolean, preventMakeLocalFilesBeforeSync?: boolean) {
|
async fetchLocal(makeLocalChunkBeforeSync?: boolean, preventMakeLocalFilesBeforeSync?: boolean) {
|
||||||
await this.core.$allSuspendExtraSync();
|
await this.services.setting.suspendExtraSync();
|
||||||
await this.askUseNewAdapter();
|
await this.askUseNewAdapter();
|
||||||
this.core.settings.isConfigured = true;
|
this.core.settings.isConfigured = true;
|
||||||
await this.suspendReflectingDatabase();
|
await this.suspendReflectingDatabase();
|
||||||
await this.core.$$realizeSettingSyncMode();
|
await this.services.setting.realiseSetting();
|
||||||
await this.resetLocalDatabase();
|
await this.resetLocalDatabase();
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
await this.core.$$openDatabase();
|
await this.services.database.openDatabase();
|
||||||
// this.core.isReady = true;
|
// this.core.isReady = true;
|
||||||
this.core.$$markIsReady();
|
this.services.appLifecycle.markIsReady();
|
||||||
if (makeLocalChunkBeforeSync) {
|
if (makeLocalChunkBeforeSync) {
|
||||||
await this.core.fileHandler.createAllChunks(true);
|
await this.core.fileHandler.createAllChunks(true);
|
||||||
} else if (!preventMakeLocalFilesBeforeSync) {
|
} else if (!preventMakeLocalFilesBeforeSync) {
|
||||||
await this.core.$$initializeDatabase(true, true, true);
|
await this.services.databaseEvents.initialiseDatabase(true, true, true);
|
||||||
} else {
|
} else {
|
||||||
// Do not create local file entries before sync (Means use remote information)
|
// Do not create local file entries before sync (Means use remote information)
|
||||||
}
|
}
|
||||||
await this.core.$$markRemoteResolved();
|
await this.services.remote.markResolved();
|
||||||
await delay(500);
|
await delay(500);
|
||||||
await this.core.$$replicateAllFromServer(true);
|
await this.services.remote.replicateAllFromRemote(true);
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
await this.core.$$replicateAllFromServer(true);
|
await this.services.remote.replicateAllFromRemote(true);
|
||||||
await this.resumeReflectingDatabase();
|
await this.resumeReflectingDatabase();
|
||||||
await this.askUsingOptionalFeature({ enableFetch: true });
|
await this.informOptionalFeatures();
|
||||||
|
// No longer enable
|
||||||
|
// await this.askUsingOptionalFeature({ enableFetch: true });
|
||||||
}
|
}
|
||||||
async fetchLocalWithRebuild() {
|
async fetchLocalWithRebuild() {
|
||||||
return await this.fetchLocal(true);
|
return await this.fetchLocal(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $allSuspendAllSync(): Promise<boolean> {
|
private async _allSuspendAllSync(): Promise<boolean> {
|
||||||
await this.suspendAllSync();
|
await this.suspendAllSync();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -214,11 +226,11 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
|
|||||||
async resetLocalDatabase() {
|
async resetLocalDatabase() {
|
||||||
if (this.core.settings.isConfigured && this.core.settings.additionalSuffixOfDatabaseName == "") {
|
if (this.core.settings.isConfigured && this.core.settings.additionalSuffixOfDatabaseName == "") {
|
||||||
// Discard the non-suffixed database
|
// Discard the non-suffixed database
|
||||||
await this.core.$$resetLocalDatabase();
|
await this.services.database.resetDatabase();
|
||||||
}
|
}
|
||||||
const suffix = (await this.core.$anyGetAppId()) || "";
|
const suffix = this.services.API.getAppID() || "";
|
||||||
this.core.settings.additionalSuffixOfDatabaseName = suffix;
|
this.core.settings.additionalSuffixOfDatabaseName = suffix;
|
||||||
await this.core.$$resetLocalDatabase();
|
await this.services.database.resetDatabase();
|
||||||
eventHub.emitEvent(EVENT_DATABASE_REBUILT);
|
eventHub.emitEvent(EVENT_DATABASE_REBUILT);
|
||||||
}
|
}
|
||||||
async fetchRemoteChunks() {
|
async fetchRemoteChunks() {
|
||||||
@@ -228,10 +240,10 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
|
|||||||
this.core.settings.remoteType == REMOTE_COUCHDB
|
this.core.settings.remoteType == REMOTE_COUCHDB
|
||||||
) {
|
) {
|
||||||
this._log(`Fetching chunks`, LOG_LEVEL_NOTICE);
|
this._log(`Fetching chunks`, LOG_LEVEL_NOTICE);
|
||||||
const replicator = this.core.$$getReplicator() as LiveSyncCouchDBReplicator;
|
const replicator = this.services.replicator.getActiveReplicator() as LiveSyncCouchDBReplicator;
|
||||||
const remoteDB = await replicator.connectRemoteCouchDBWithSetting(
|
const remoteDB = await replicator.connectRemoteCouchDBWithSetting(
|
||||||
this.settings,
|
this.settings,
|
||||||
this.core.$$isMobile(),
|
this.services.API.isMobile(),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
if (typeof remoteDB == "string") {
|
if (typeof remoteDB == "string") {
|
||||||
@@ -254,8 +266,15 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
|
|||||||
LOG_LEVEL_NOTICE,
|
LOG_LEVEL_NOTICE,
|
||||||
"resolveAllConflictedFilesByNewerOnes"
|
"resolveAllConflictedFilesByNewerOnes"
|
||||||
);
|
);
|
||||||
await this.core.$anyResolveConflictByNewest(file);
|
await this.services.conflict.resolveByNewest(file);
|
||||||
}
|
}
|
||||||
this._log(`Done!`, LOG_LEVEL_NOTICE, "resolveAllConflictedFilesByNewerOnes");
|
this._log(`Done!`, LOG_LEVEL_NOTICE, "resolveAllConflictedFilesByNewerOnes");
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
|
||||||
|
services.database.handleResetDatabase(this._resetLocalDatabase.bind(this));
|
||||||
|
services.remote.handleTryResetDatabase(this._tryResetRemoteDatabase.bind(this));
|
||||||
|
services.remote.handleTryCreateDatabase(this._tryCreateRemoteDatabase.bind(this));
|
||||||
|
services.setting.handleSuspendAllSync(this._allSuspendAllSync.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { fireAndForget, yieldMicrotask } from "octagonal-wheels/promises";
|
import { fireAndForget, yieldMicrotask } from "octagonal-wheels/promises";
|
||||||
import type { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB";
|
import type { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB";
|
||||||
import { AbstractModule } from "../AbstractModule";
|
import { AbstractModule } from "../AbstractModule";
|
||||||
import type { ICoreModule } from "../ModuleTypes";
|
|
||||||
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 } from "octagonal-wheels/common/logger";
|
||||||
import { isLockAcquired, shareRunningResult, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
|
import { isLockAcquired, shareRunningResult, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
|
||||||
import { balanceChunkPurgedDBs } from "@/lib/src/pouchdb/chunks";
|
import { balanceChunkPurgedDBs } from "@/lib/src/pouchdb/chunks";
|
||||||
@@ -17,6 +16,7 @@ import {
|
|||||||
type EntryLeaf,
|
type EntryLeaf,
|
||||||
type LoadedEntry,
|
type LoadedEntry,
|
||||||
type MetaEntry,
|
type MetaEntry,
|
||||||
|
type RemoteType,
|
||||||
} from "../../lib/src/common/types";
|
} from "../../lib/src/common/types";
|
||||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||||
import {
|
import {
|
||||||
@@ -33,16 +33,18 @@ import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveS
|
|||||||
|
|
||||||
import { $msg } from "../../lib/src/common/i18n";
|
import { $msg } from "../../lib/src/common/i18n";
|
||||||
import { clearHandlers } from "../../lib/src/replication/SyncParamsHandler";
|
import { clearHandlers } from "../../lib/src/replication/SyncParamsHandler";
|
||||||
|
import type { LiveSyncCore } from "../../main";
|
||||||
|
|
||||||
const KEY_REPLICATION_ON_EVENT = "replicationOnEvent";
|
const KEY_REPLICATION_ON_EVENT = "replicationOnEvent";
|
||||||
const REPLICATION_ON_EVENT_FORECASTED_TIME = 5000;
|
const REPLICATION_ON_EVENT_FORECASTED_TIME = 5000;
|
||||||
|
|
||||||
export class ModuleReplicator extends AbstractModule implements ICoreModule {
|
export class ModuleReplicator extends AbstractModule {
|
||||||
_replicatorType?: string;
|
_replicatorType?: RemoteType;
|
||||||
$everyOnloadAfterLoadSettings(): Promise<boolean> {
|
|
||||||
|
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
|
||||||
eventHub.onEvent(EVENT_FILE_SAVED, () => {
|
eventHub.onEvent(EVENT_FILE_SAVED, () => {
|
||||||
if (this.settings.syncOnSave && !this.core.$$isSuspended()) {
|
if (this.settings.syncOnSave && !this.core.services.appLifecycle.isSuspended()) {
|
||||||
scheduleTask("perform-replicate-after-save", 250, () => this.core.$$replicateByEvent());
|
scheduleTask("perform-replicate-after-save", 250, () => this.services.replication.replicateByEvent());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
eventHub.onEvent(EVENT_SETTING_SAVED, (setting) => {
|
eventHub.onEvent(EVENT_SETTING_SAVED, (setting) => {
|
||||||
@@ -55,7 +57,7 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setReplicator() {
|
async setReplicator() {
|
||||||
const replicator = await this.core.$anyNewReplicator();
|
const replicator = await this.services.replicator.getNewReplicator();
|
||||||
if (!replicator) {
|
if (!replicator) {
|
||||||
this._log($msg("Replicator.Message.InitialiseFatalError"), LOG_LEVEL_NOTICE);
|
this._log($msg("Replicator.Message.InitialiseFatalError"), LOG_LEVEL_NOTICE);
|
||||||
return false;
|
return false;
|
||||||
@@ -72,25 +74,33 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$$getReplicator(): LiveSyncAbstractReplicator {
|
_getReplicator(): LiveSyncAbstractReplicator {
|
||||||
return this.core.replicator;
|
return this.core.replicator;
|
||||||
}
|
}
|
||||||
|
|
||||||
$everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
_everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
||||||
return this.setReplicator();
|
return this.setReplicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
$everyOnResetDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
_everyOnResetDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
||||||
return this.setReplicator();
|
return this.setReplicator();
|
||||||
}
|
}
|
||||||
async ensureReplicatorPBKDF2Salt(showMessage: boolean = false): Promise<boolean> {
|
async ensureReplicatorPBKDF2Salt(showMessage: boolean = false): Promise<boolean> {
|
||||||
// Checking salt
|
// Checking salt
|
||||||
const replicator = this.core.$$getReplicator();
|
const replicator = this.services.replicator.getActiveReplicator();
|
||||||
|
if (!replicator) {
|
||||||
|
this._log($msg("Replicator.Message.InitialiseFatalError"), LOG_LEVEL_NOTICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return await replicator.ensurePBKDF2Salt(this.settings, showMessage, true);
|
return await replicator.ensurePBKDF2Salt(this.settings, showMessage, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
|
async _everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
|
||||||
// Checking salt
|
// Checking salt
|
||||||
|
if (!this.core.managers.networkManager.isOnline) {
|
||||||
|
this._log("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).
|
// 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))) {
|
if (!(await this.ensureReplicatorPBKDF2Salt(false))) {
|
||||||
Logger("Failed to initialise the encryption key, preventing replication.", LOG_LEVEL_NOTICE);
|
Logger("Failed to initialise the encryption key, preventing replication.", LOG_LEVEL_NOTICE);
|
||||||
@@ -100,7 +110,7 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$replicate(showMessage: boolean = false): Promise<boolean | void> {
|
private async _replicate(showMessage: boolean = false): Promise<boolean | void> {
|
||||||
try {
|
try {
|
||||||
updatePreviousExecutionTime(KEY_REPLICATION_ON_EVENT, REPLICATION_ON_EVENT_FORECASTED_TIME);
|
updatePreviousExecutionTime(KEY_REPLICATION_ON_EVENT, REPLICATION_ON_EVENT_FORECASTED_TIME);
|
||||||
return await this.$$_replicate(showMessage);
|
return await this.$$_replicate(showMessage);
|
||||||
@@ -137,11 +147,11 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
|||||||
await this.core.rebuilder.$performRebuildDB("localOnly");
|
await this.core.rebuilder.$performRebuildDB("localOnly");
|
||||||
}
|
}
|
||||||
if (ret == CHOICE_CLEAN) {
|
if (ret == CHOICE_CLEAN) {
|
||||||
const replicator = this.core.$$getReplicator();
|
const replicator = this.services.replicator.getActiveReplicator();
|
||||||
if (!(replicator instanceof LiveSyncCouchDBReplicator)) return;
|
if (!(replicator instanceof LiveSyncCouchDBReplicator)) return;
|
||||||
const remoteDB = await replicator.connectRemoteCouchDBWithSetting(
|
const remoteDB = await replicator.connectRemoteCouchDBWithSetting(
|
||||||
this.settings,
|
this.settings,
|
||||||
this.core.$$isMobile(),
|
this.services.API.isMobile(),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
if (typeof remoteDB == "string") {
|
if (typeof remoteDB == "string") {
|
||||||
@@ -156,7 +166,7 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
|||||||
await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db);
|
await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db);
|
||||||
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
|
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
|
||||||
this.localDatabase.clearCaches();
|
this.localDatabase.clearCaches();
|
||||||
await this.core.$$getReplicator().markRemoteResolved(this.settings);
|
await this.services.replicator.getActiveReplicator()?.markRemoteResolved(this.settings);
|
||||||
Logger("The local database has been cleaned up.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
|
Logger("The local database has been cleaned up.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
|
||||||
} else {
|
} else {
|
||||||
Logger(
|
Logger(
|
||||||
@@ -167,31 +177,48 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async $$_replicate(showMessage: boolean = false): Promise<boolean | void> {
|
|
||||||
//--?
|
async _canReplicate(showMessage: boolean = false): Promise<boolean> {
|
||||||
if (!this.core.$$isReady()) return;
|
if (!this.services.appLifecycle.isReady()) {
|
||||||
|
Logger(`Not ready`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (isLockAcquired("cleanup")) {
|
if (isLockAcquired("cleanup")) {
|
||||||
Logger($msg("Replicator.Message.Cleaned"), LOG_LEVEL_NOTICE);
|
Logger($msg("Replicator.Message.Cleaned"), LOG_LEVEL_NOTICE);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.settings.versionUpFlash != "") {
|
if (this.settings.versionUpFlash != "") {
|
||||||
Logger($msg("Replicator.Message.VersionUpFlash"), LOG_LEVEL_NOTICE);
|
Logger($msg("Replicator.Message.VersionUpFlash"), LOG_LEVEL_NOTICE);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (!(await this.core.$everyCommitPendingFileEvent())) {
|
|
||||||
|
if (!(await this.services.fileProcessing.commitPendingFileEvents())) {
|
||||||
Logger($msg("Replicator.Message.Pending"), LOG_LEVEL_NOTICE);
|
Logger($msg("Replicator.Message.Pending"), LOG_LEVEL_NOTICE);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!(await this.core.$everyBeforeReplicate(showMessage))) {
|
|
||||||
|
if (!this.core.managers.networkManager.isOnline) {
|
||||||
|
this._log("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);
|
Logger($msg("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async $$_replicate(showMessage: boolean = false): Promise<boolean | void> {
|
||||||
|
const checkBeforeReplicate = await this.services.replication.isReplicationReady(showMessage);
|
||||||
|
if (!checkBeforeReplicate) return false;
|
||||||
|
|
||||||
//<-- Here could be an module.
|
//<-- Here could be an module.
|
||||||
const ret = await this.core.replicator.openReplication(this.settings, false, showMessage, false);
|
const ret = await this.core.replicator.openReplication(this.settings, false, showMessage, false);
|
||||||
if (!ret) {
|
if (!ret) {
|
||||||
if (this.core.replicator.tweakSettingsMismatched && this.core.replicator.preferredTweakValue) {
|
if (this.core.replicator.tweakSettingsMismatched && this.core.replicator.preferredTweakValue) {
|
||||||
await this.core.$$askResolvingMismatchedTweaks(this.core.replicator.preferredTweakValue);
|
await this.services.tweakValue.askResolvingMismatched(this.core.replicator.preferredTweakValue);
|
||||||
} else {
|
} else {
|
||||||
if (this.core.replicator?.remoteLockedAndDeviceNotAccepted) {
|
if (this.core.replicator?.remoteLockedAndDeviceNotAccepted) {
|
||||||
if (this.core.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
|
if (this.core.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
|
||||||
@@ -213,7 +240,7 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
|||||||
if (ret == CHOICE_FETCH) {
|
if (ret == CHOICE_FETCH) {
|
||||||
this._log($msg("Replicator.Dialogue.Locked.Message.Fetch"), LOG_LEVEL_NOTICE);
|
this._log($msg("Replicator.Dialogue.Locked.Message.Fetch"), LOG_LEVEL_NOTICE);
|
||||||
await this.core.rebuilder.scheduleFetch();
|
await this.core.rebuilder.scheduleFetch();
|
||||||
this.core.$$scheduleAppReload();
|
this.services.appLifecycle.scheduleRestart();
|
||||||
return;
|
return;
|
||||||
} else if (ret == CHOICE_UNLOCK) {
|
} else if (ret == CHOICE_UNLOCK) {
|
||||||
await this.core.replicator.markRemoteResolved(this.settings);
|
await this.core.replicator.markRemoteResolved(this.settings);
|
||||||
@@ -227,16 +254,16 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$replicateByEvent(): Promise<boolean | void> {
|
private async _replicateByEvent(): Promise<boolean | void> {
|
||||||
const least = this.settings.syncMinimumInterval;
|
const least = this.settings.syncMinimumInterval;
|
||||||
if (least > 0) {
|
if (least > 0) {
|
||||||
return rateLimitedSharedExecution(KEY_REPLICATION_ON_EVENT, least, async () => {
|
return rateLimitedSharedExecution(KEY_REPLICATION_ON_EVENT, least, async () => {
|
||||||
return await this.$$replicate();
|
return await this.services.replication.replicate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await shareRunningResult(`replication`, () => this.core.$$replicate());
|
return await shareRunningResult(`replication`, () => this.services.replication.replicate());
|
||||||
}
|
}
|
||||||
$$parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): void {
|
_parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): void {
|
||||||
if (this.settings.suspendParseReplicationResult && !this.replicationResultProcessor.isSuspended) {
|
if (this.settings.suspendParseReplicationResult && !this.replicationResultProcessor.isSuspended) {
|
||||||
this.replicationResultProcessor.suspend();
|
this.replicationResultProcessor.suspend();
|
||||||
}
|
}
|
||||||
@@ -313,7 +340,7 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
|||||||
this.localDatabase.onNewLeaf(change as EntryLeaf);
|
this.localDatabase.onNewLeaf(change as EntryLeaf);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (await this.core.$anyModuleParsedReplicationResultItem(change)) return;
|
if (await this.services.replication.processVirtualDocument(change)) return;
|
||||||
// any addon needs this item?
|
// any addon needs this item?
|
||||||
// for (const proc of this.core.addOns) {
|
// for (const proc of this.core.addOns) {
|
||||||
// if (await proc.parseReplicationResultItem(change)) {
|
// if (await proc.parseReplicationResultItem(change)) {
|
||||||
@@ -338,7 +365,7 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
|||||||
}
|
}
|
||||||
if (isAnyNote(change)) {
|
if (isAnyNote(change)) {
|
||||||
const docPath = getPath(change);
|
const docPath = getPath(change);
|
||||||
if (!(await this.core.$$isTargetFile(docPath))) {
|
if (!(await this.services.vault.isTargetFile(docPath))) {
|
||||||
Logger(`Skipped: ${docPath}`, LOG_LEVEL_VERBOSE);
|
Logger(`Skipped: ${docPath}`, LOG_LEVEL_VERBOSE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -346,7 +373,7 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
|||||||
Logger(`Processing scheduled: ${docPath}`, LOG_LEVEL_INFO);
|
Logger(`Processing scheduled: ${docPath}`, LOG_LEVEL_INFO);
|
||||||
}
|
}
|
||||||
const size = change.size;
|
const size = change.size;
|
||||||
if (this.core.$$isFileSizeExceeded(size)) {
|
if (this.services.vault.isFileSizeTooLarge(size)) {
|
||||||
Logger(
|
Logger(
|
||||||
`Processing ${docPath} has been skipped due to file size exceeding the limit`,
|
`Processing ${docPath} has been skipped due to file size exceeding the limit`,
|
||||||
LOG_LEVEL_NOTICE
|
LOG_LEVEL_NOTICE
|
||||||
@@ -390,7 +417,7 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.core.$anyProcessOptionalSyncFiles(dbDoc)) {
|
if (await this.services.replication.processOptionalSynchroniseResult(dbDoc)) {
|
||||||
// Already processed
|
// Already processed
|
||||||
} else if (isValidPath(getPath(doc))) {
|
} else if (isValidPath(getPath(doc))) {
|
||||||
this.storageApplyingProcessor.enqueue(doc as MetaEntry);
|
this.storageApplyingProcessor.enqueue(doc as MetaEntry);
|
||||||
@@ -417,7 +444,7 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
|||||||
storageApplyingProcessor = new QueueProcessor(
|
storageApplyingProcessor = new QueueProcessor(
|
||||||
async (docs: MetaEntry[]) => {
|
async (docs: MetaEntry[]) => {
|
||||||
const entry = docs[0];
|
const entry = docs[0];
|
||||||
await this.core.$anyProcessReplicatedDoc(entry);
|
await this.services.replication.processSynchroniseResult(entry);
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -435,17 +462,17 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
|||||||
})
|
})
|
||||||
.startPipeline();
|
.startPipeline();
|
||||||
|
|
||||||
$everyBeforeSuspendProcess(): Promise<boolean> {
|
_everyBeforeSuspendProcess(): Promise<boolean> {
|
||||||
this.core.replicator.closeReplication();
|
this.core.replicator?.closeReplication();
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$replicateAllToServer(
|
private async _replicateAllToServer(
|
||||||
showingNotice: boolean = false,
|
showingNotice: boolean = false,
|
||||||
sendChunksInBulkDisabled: boolean = false
|
sendChunksInBulkDisabled: boolean = false
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!this.core.$$isReady()) return false;
|
if (!this.services.appLifecycle.isReady()) return false;
|
||||||
if (!(await this.core.$everyBeforeReplicate(showingNotice))) {
|
if (!(await this.services.replication.onBeforeReplicate(showingNotice))) {
|
||||||
Logger($msg("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
|
Logger($msg("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -463,16 +490,31 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
|||||||
}
|
}
|
||||||
const ret = await this.core.replicator.replicateAllToServer(this.settings, showingNotice);
|
const ret = await this.core.replicator.replicateAllToServer(this.settings, showingNotice);
|
||||||
if (ret) return true;
|
if (ret) return true;
|
||||||
const checkResult = await this.core.$anyAfterConnectCheckFailed();
|
const checkResult = await this.services.replication.checkConnectionFailure();
|
||||||
if (checkResult == "CHECKAGAIN") return await this.core.$$replicateAllToServer(showingNotice);
|
if (checkResult == "CHECKAGAIN") return await this.services.remote.replicateAllToRemote(showingNotice);
|
||||||
return !checkResult;
|
return !checkResult;
|
||||||
}
|
}
|
||||||
async $$replicateAllFromServer(showingNotice: boolean = false): Promise<boolean> {
|
async _replicateAllFromServer(showingNotice: boolean = false): Promise<boolean> {
|
||||||
if (!this.core.$$isReady()) return false;
|
if (!this.services.appLifecycle.isReady()) return false;
|
||||||
const ret = await this.core.replicator.replicateAllFromServer(this.settings, showingNotice);
|
const ret = await this.core.replicator.replicateAllFromServer(this.settings, showingNotice);
|
||||||
if (ret) return true;
|
if (ret) return true;
|
||||||
const checkResult = await this.core.$anyAfterConnectCheckFailed();
|
const checkResult = await this.services.replication.checkConnectionFailure();
|
||||||
if (checkResult == "CHECKAGAIN") return await this.core.$$replicateAllFromServer(showingNotice);
|
if (checkResult == "CHECKAGAIN") return await this.services.remote.replicateAllFromRemote(showingNotice);
|
||||||
return !checkResult;
|
return !checkResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.replicator.handleGetActiveReplicator(this._getReplicator.bind(this));
|
||||||
|
services.databaseEvents.handleOnDatabaseInitialisation(this._everyOnInitializeDatabase.bind(this));
|
||||||
|
services.databaseEvents.handleOnResetDatabase(this._everyOnResetDatabase.bind(this));
|
||||||
|
services.appLifecycle.handleOnSettingLoaded(this._everyOnloadAfterLoadSettings.bind(this));
|
||||||
|
services.replication.handleParseSynchroniseResult(this._parseReplicationResult.bind(this));
|
||||||
|
services.appLifecycle.handleOnSuspending(this._everyBeforeSuspendProcess.bind(this));
|
||||||
|
services.replication.handleBeforeReplicate(this._everyBeforeReplicate.bind(this));
|
||||||
|
services.replication.handleIsReplicationReady(this._canReplicate.bind(this));
|
||||||
|
services.replication.handleReplicate(this._replicate.bind(this));
|
||||||
|
services.replication.handleReplicateByEvent(this._replicateByEvent.bind(this));
|
||||||
|
services.remote.handleReplicateAllToRemote(this._replicateAllToServer.bind(this));
|
||||||
|
services.remote.handleReplicateAllFromRemote(this._replicateAllFromServer.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,30 +3,40 @@ import { REMOTE_MINIO, REMOTE_P2P, type RemoteDBSettings } from "../../lib/src/c
|
|||||||
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
|
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
|
||||||
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
|
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
|
||||||
import { AbstractModule } from "../AbstractModule";
|
import { AbstractModule } from "../AbstractModule";
|
||||||
import type { ICoreModule } from "../ModuleTypes";
|
import type { LiveSyncCore } from "../../main";
|
||||||
|
|
||||||
export class ModuleReplicatorCouchDB extends AbstractModule implements ICoreModule {
|
export class ModuleReplicatorCouchDB extends AbstractModule {
|
||||||
$anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
|
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator | false> {
|
||||||
const settings = { ...this.settings, ...settingOverride };
|
const settings = { ...this.settings, ...settingOverride };
|
||||||
// If new remote types were added, add them here. Do not use `REMOTE_COUCHDB` directly for the safety valve.
|
// If new remote types were added, add them here. Do not use `REMOTE_COUCHDB` directly for the safety valve.
|
||||||
if (settings.remoteType == REMOTE_MINIO || settings.remoteType == REMOTE_P2P) {
|
if (settings.remoteType == REMOTE_MINIO || settings.remoteType == REMOTE_P2P) {
|
||||||
return undefined!;
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
return Promise.resolve(new LiveSyncCouchDBReplicator(this.core));
|
return Promise.resolve(new LiveSyncCouchDBReplicator(this.core));
|
||||||
}
|
}
|
||||||
$everyAfterResumeProcess(): Promise<boolean> {
|
_everyAfterResumeProcess(): Promise<boolean> {
|
||||||
|
if (this.services.appLifecycle.isSuspended()) return Promise.resolve(true);
|
||||||
|
if (!this.services.appLifecycle.isReady()) return Promise.resolve(true);
|
||||||
if (this.settings.remoteType != REMOTE_MINIO && this.settings.remoteType != REMOTE_P2P) {
|
if (this.settings.remoteType != REMOTE_MINIO && this.settings.remoteType != REMOTE_P2P) {
|
||||||
// If LiveSync enabled, open replication
|
const LiveSyncEnabled = this.settings.liveSync;
|
||||||
if (this.settings.liveSync) {
|
const continuous = LiveSyncEnabled;
|
||||||
fireAndForget(() => this.core.replicator.openReplication(this.settings, true, false, false));
|
const eventualOnStart = !LiveSyncEnabled && this.settings.syncOnStart;
|
||||||
}
|
// If enabled LiveSync or on start, open replication
|
||||||
// If sync on start enabled, open replication
|
if (LiveSyncEnabled || eventualOnStart) {
|
||||||
if (!this.settings.liveSync && this.settings.syncOnStart) {
|
// And note that we do not open the conflict detection dialogue directly during this process.
|
||||||
// Possibly ok as if only share the result
|
// This should be raised explicitly if needed.
|
||||||
fireAndForget(() => this.core.replicator.openReplication(this.settings, false, false, false));
|
fireAndForget(async () => {
|
||||||
|
const canReplicate = await this.services.replication.isReplicationReady(false);
|
||||||
|
if (!canReplicate) return;
|
||||||
|
void this.core.replicator.openReplication(this.settings, continuous, false, false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.replicator.handleGetNewReplicator(this._anyNewReplicator.bind(this));
|
||||||
|
services.appLifecycle.handleOnResumed(this._everyAfterResumeProcess.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { REMOTE_MINIO, type RemoteDBSettings } from "../../lib/src/common/types";
|
import { REMOTE_MINIO, type RemoteDBSettings } from "../../lib/src/common/types";
|
||||||
import { LiveSyncJournalReplicator } from "../../lib/src/replication/journal/LiveSyncJournalReplicator";
|
import { LiveSyncJournalReplicator } from "../../lib/src/replication/journal/LiveSyncJournalReplicator";
|
||||||
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
|
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
|
||||||
|
import type { LiveSyncCore } from "../../main";
|
||||||
import { AbstractModule } from "../AbstractModule";
|
import { AbstractModule } from "../AbstractModule";
|
||||||
import type { ICoreModule } from "../ModuleTypes";
|
|
||||||
|
|
||||||
export class ModuleReplicatorMinIO extends AbstractModule implements ICoreModule {
|
export class ModuleReplicatorMinIO extends AbstractModule {
|
||||||
$anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
|
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator | false> {
|
||||||
const settings = { ...this.settings, ...settingOverride };
|
const settings = { ...this.settings, ...settingOverride };
|
||||||
if (settings.remoteType == REMOTE_MINIO) {
|
if (settings.remoteType == REMOTE_MINIO) {
|
||||||
return Promise.resolve(new LiveSyncJournalReplicator(this.core));
|
return Promise.resolve(new LiveSyncJournalReplicator(this.core));
|
||||||
}
|
}
|
||||||
return undefined!;
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.replicator.handleGetNewReplicator(this._anyNewReplicator.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { REMOTE_P2P, type RemoteDBSettings } from "../../lib/src/common/types";
|
import { REMOTE_P2P, type RemoteDBSettings } from "../../lib/src/common/types";
|
||||||
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
|
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
|
||||||
import { AbstractModule } from "../AbstractModule";
|
import { AbstractModule } from "../AbstractModule";
|
||||||
import type { ICoreModule } from "../ModuleTypes";
|
|
||||||
import { LiveSyncTrysteroReplicator } from "../../lib/src/replication/trystero/LiveSyncTrysteroReplicator";
|
import { LiveSyncTrysteroReplicator } from "../../lib/src/replication/trystero/LiveSyncTrysteroReplicator";
|
||||||
|
import type { LiveSyncCore } from "../../main";
|
||||||
|
|
||||||
export class ModuleReplicatorP2P extends AbstractModule implements ICoreModule {
|
export class ModuleReplicatorP2P extends AbstractModule {
|
||||||
$anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
|
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator | false> {
|
||||||
const settings = { ...this.settings, ...settingOverride };
|
const settings = { ...this.settings, ...settingOverride };
|
||||||
if (settings.remoteType == REMOTE_P2P) {
|
if (settings.remoteType == REMOTE_P2P) {
|
||||||
return Promise.resolve(new LiveSyncTrysteroReplicator(this.core));
|
return Promise.resolve(new LiveSyncTrysteroReplicator(this.core));
|
||||||
}
|
}
|
||||||
return undefined!;
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
$everyAfterResumeProcess(): Promise<boolean> {
|
_everyAfterResumeProcess(): Promise<boolean> {
|
||||||
if (this.settings.remoteType == REMOTE_P2P) {
|
if (this.settings.remoteType == REMOTE_P2P) {
|
||||||
// // If LiveSync enabled, open replication
|
// // If LiveSync enabled, open replication
|
||||||
// if (this.settings.liveSync) {
|
// if (this.settings.liveSync) {
|
||||||
@@ -27,4 +27,8 @@ export class ModuleReplicatorP2P extends AbstractModule implements ICoreModule {
|
|||||||
|
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.replicator.handleGetNewReplicator(this._anyNewReplicator.bind(this));
|
||||||
|
services.appLifecycle.handleOnResumed(this._everyAfterResumeProcess.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ import {
|
|||||||
} from "../../lib/src/common/types";
|
} from "../../lib/src/common/types";
|
||||||
import { addPrefix, isAcceptedAll } from "../../lib/src/string_and_binary/path";
|
import { addPrefix, isAcceptedAll } from "../../lib/src/string_and_binary/path";
|
||||||
import { AbstractModule } from "../AbstractModule";
|
import { AbstractModule } from "../AbstractModule";
|
||||||
import type { ICoreModule } from "../ModuleTypes";
|
|
||||||
import { EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
|
import { EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
|
||||||
import { isDirty } from "../../lib/src/common/utils";
|
import { isDirty } from "../../lib/src/common/utils";
|
||||||
export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
|
import type { LiveSyncCore } from "../../main";
|
||||||
|
export class ModuleTargetFilter extends AbstractModule {
|
||||||
reloadIgnoreFiles() {
|
reloadIgnoreFiles() {
|
||||||
this.ignoreFiles = this.settings.ignoreFiles.split(",").map((e) => e.trim());
|
this.ignoreFiles = this.settings.ignoreFiles.split(",").map((e) => e.trim());
|
||||||
}
|
}
|
||||||
$everyOnload(): Promise<boolean> {
|
private _everyOnload(): Promise<boolean> {
|
||||||
eventHub.onEvent(EVENT_SETTING_SAVED, (evt: ObsidianLiveSyncSettings) => {
|
eventHub.onEvent(EVENT_SETTING_SAVED, (evt: ObsidianLiveSyncSettings) => {
|
||||||
this.reloadIgnoreFiles();
|
this.reloadIgnoreFiles();
|
||||||
});
|
});
|
||||||
@@ -35,7 +35,7 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
|
|||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$$id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
|
_id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
|
||||||
const tempId = id2path(id, entry);
|
const tempId = id2path(id, entry);
|
||||||
if (stripPrefix && isInternalMetadata(tempId)) {
|
if (stripPrefix && isInternalMetadata(tempId)) {
|
||||||
const out = stripInternalMetadataPrefix(tempId);
|
const out = stripInternalMetadataPrefix(tempId);
|
||||||
@@ -43,7 +43,7 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
|
|||||||
}
|
}
|
||||||
return tempId;
|
return tempId;
|
||||||
}
|
}
|
||||||
async $$path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID> {
|
async _path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID> {
|
||||||
const destPath = addPrefix(filename, prefix ?? "");
|
const destPath = addPrefix(filename, prefix ?? "");
|
||||||
return await path2id(
|
return await path2id(
|
||||||
destPath,
|
destPath,
|
||||||
@@ -52,7 +52,7 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$$isFileSizeExceeded(size: number) {
|
private _isFileSizeExceeded(size: number) {
|
||||||
if (this.settings.syncMaxSizeInMB > 0 && size > 0) {
|
if (this.settings.syncMaxSizeInMB > 0 && size > 0) {
|
||||||
if (this.settings.syncMaxSizeInMB * 1024 * 1024 < size) {
|
if (this.settings.syncMaxSizeInMB * 1024 * 1024 < size) {
|
||||||
return true;
|
return true;
|
||||||
@@ -61,7 +61,7 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$$markFileListPossiblyChanged(): void {
|
_markFileListPossiblyChanged(): void {
|
||||||
this.totalFileEventCount++;
|
this.totalFileEventCount++;
|
||||||
}
|
}
|
||||||
totalFileEventCount = 0;
|
totalFileEventCount = 0;
|
||||||
@@ -72,7 +72,7 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$isTargetFile(file: string | UXFileInfoStub, keepFileCheckList = false) {
|
private async _isTargetFile(file: string | UXFileInfoStub, keepFileCheckList = false) {
|
||||||
const fileCount = useMemo<Record<string, number>>(
|
const fileCount = useMemo<Record<string, number>>(
|
||||||
{
|
{
|
||||||
key: "fileCount", // forceUpdate: !keepFileCheckList,
|
key: "fileCount", // forceUpdate: !keepFileCheckList,
|
||||||
@@ -109,7 +109,7 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
|
|||||||
|
|
||||||
const filepath = getStoragePathFromUXFileInfo(file);
|
const filepath = getStoragePathFromUXFileInfo(file);
|
||||||
const lc = filepath.toLowerCase();
|
const lc = filepath.toLowerCase();
|
||||||
if (this.core.$$shouldCheckCaseInsensitive()) {
|
if (this.services.setting.shouldCheckCaseInsensitively()) {
|
||||||
if (lc in fileCount && fileCount[lc] > 1) {
|
if (lc in fileCount && fileCount[lc] > 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
|
|||||||
// We must reload ignore files due to the its change.
|
// We must reload ignore files due to the its change.
|
||||||
await this.readIgnoreFile(filepath);
|
await this.readIgnoreFile(filepath);
|
||||||
}
|
}
|
||||||
if (await this.core.$$isIgnoredByIgnoreFiles(file)) {
|
if (await this.services.vault.isIgnoredByIgnoreFile(file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
|
|||||||
return await this.readIgnoreFile(path);
|
return await this.readIgnoreFile(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async $$isIgnoredByIgnoreFiles(file: string | UXFileInfoStub): Promise<boolean> {
|
private async _isIgnoredByIgnoreFiles(file: string | UXFileInfoStub): Promise<boolean> {
|
||||||
if (!this.settings.useIgnoreFiles) {
|
if (!this.settings.useIgnoreFiles) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -164,4 +164,14 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.vault.handleMarkFileListPossiblyChanged(this._markFileListPossiblyChanged.bind(this));
|
||||||
|
services.path.handleId2Path(this._id2path.bind(this));
|
||||||
|
services.path.handlePath2Id(this._path2id.bind(this));
|
||||||
|
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
|
||||||
|
services.vault.handleIsFileSizeTooLarge(this._isFileSizeExceeded.bind(this));
|
||||||
|
services.vault.handleIsIgnoredByIgnoreFile(this._isIgnoredByIgnoreFiles.bind(this));
|
||||||
|
services.vault.handleIsTargetFile(this._isTargetFile.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||||
import { AbstractModule } from "../AbstractModule.ts";
|
import { AbstractModule } from "../AbstractModule.ts";
|
||||||
import { sizeToHumanReadable } from "octagonal-wheels/number";
|
import { sizeToHumanReadable } from "octagonal-wheels/number";
|
||||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
|
||||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
export class ModuleCheckRemoteSize extends AbstractModule implements ICoreModule {
|
export class ModuleCheckRemoteSize extends AbstractModule {
|
||||||
async $allScanStat(): Promise<boolean> {
|
async _allScanStat(): Promise<boolean> {
|
||||||
|
if (this.core.managers.networkManager.isOnline === false) {
|
||||||
|
this._log("Network is offline, skipping remote size check.", LOG_LEVEL_INFO);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
this._log($msg("moduleCheckRemoteSize.logCheckingStorageSizes"), LOG_LEVEL_VERBOSE);
|
this._log($msg("moduleCheckRemoteSize.logCheckingStorageSizes"), LOG_LEVEL_VERBOSE);
|
||||||
if (this.settings.notifyThresholdOfRemoteStorageSize < 0) {
|
if (this.settings.notifyThresholdOfRemoteStorageSize < 0) {
|
||||||
const message = $msg("moduleCheckRemoteSize.msgSetDBCapacity");
|
const message = $msg("moduleCheckRemoteSize.msgSetDBCapacity");
|
||||||
@@ -105,4 +109,7 @@ export class ModuleCheckRemoteSize extends AbstractModule implements ICoreModule
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleOnScanningStartupIssues(this._allScanStat.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,35 +2,36 @@ import { AbstractModule } from "../AbstractModule.ts";
|
|||||||
import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "../../lib/src/common/types";
|
import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "../../lib/src/common/types";
|
||||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||||
import { sendValue } from "octagonal-wheels/messagepassing/signal";
|
import { sendValue } from "octagonal-wheels/messagepassing/signal";
|
||||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
export class ModuleConflictChecker extends AbstractModule implements ICoreModule {
|
export class ModuleConflictChecker extends AbstractModule {
|
||||||
async $$queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise<void> {
|
async _queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise<void> {
|
||||||
const path = file;
|
const path = file;
|
||||||
if (this.settings.checkConflictOnlyOnOpen) {
|
if (this.settings.checkConflictOnlyOnOpen) {
|
||||||
const af = this.core.$$getActiveFilePath();
|
const af = this.services.vault.getActiveFilePath();
|
||||||
if (af && af != path) {
|
if (af && af != path) {
|
||||||
this._log(`${file} is conflicted, merging process has been postponed.`, LOG_LEVEL_NOTICE);
|
this._log(`${file} is conflicted, merging process has been postponed.`, LOG_LEVEL_NOTICE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.core.$$queueConflictCheck(path);
|
await this.services.conflict.queueCheckFor(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$queueConflictCheck(file: FilePathWithPrefix): Promise<void> {
|
async _queueConflictCheck(file: FilePathWithPrefix): Promise<void> {
|
||||||
const optionalConflictResult = await this.core.$anyGetOptionalConflictCheckMethod(file);
|
const optionalConflictResult = await this.services.conflict.getOptionalConflictCheckMethod(file);
|
||||||
if (optionalConflictResult == true) {
|
if (optionalConflictResult == true) {
|
||||||
// The conflict has been resolved by another process.
|
// The conflict has been resolved by another process.
|
||||||
return;
|
return;
|
||||||
} else if (optionalConflictResult === "newer") {
|
} else if (optionalConflictResult === "newer") {
|
||||||
// The conflict should be resolved by the newer entry.
|
// The conflict should be resolved by the newer entry.
|
||||||
await this.core.$anyResolveConflictByNewest(file);
|
await this.services.conflict.resolveByNewest(file);
|
||||||
} else {
|
} else {
|
||||||
this.conflictCheckQueue.enqueue(file);
|
this.conflictCheckQueue.enqueue(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$$waitForAllConflictProcessed(): Promise<boolean> {
|
_waitForAllConflictProcessed(): Promise<boolean> {
|
||||||
return this.conflictResolveQueue.waitForAllProcessed();
|
return this.conflictResolveQueue.waitForAllProcessed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ export class ModuleConflictChecker extends AbstractModule implements ICoreModule
|
|||||||
conflictResolveQueue = new QueueProcessor(
|
conflictResolveQueue = new QueueProcessor(
|
||||||
async (filenames: FilePathWithPrefix[]) => {
|
async (filenames: FilePathWithPrefix[]) => {
|
||||||
const filename = filenames[0];
|
const filename = filenames[0];
|
||||||
return await this.core.$$resolveConflict(filename);
|
return await this.services.conflict.resolve(filename);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
suspended: false,
|
suspended: false,
|
||||||
@@ -73,4 +74,9 @@ export class ModuleConflictChecker extends AbstractModule implements ICoreModule
|
|||||||
totalRemainingReactiveSource: this.core.conflictProcessQueueCount,
|
totalRemainingReactiveSource: this.core.conflictProcessQueueCount,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
|
||||||
|
services.conflict.handleQueueCheckForIfOpen(this._queueConflictCheckIfOpen.bind(this));
|
||||||
|
services.conflict.handleQueueCheckFor(this._queueConflictCheck.bind(this));
|
||||||
|
services.conflict.handleEnsureAllProcessed(this._waitForAllConflictProcessed.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ import {
|
|||||||
} from "../../common/utils";
|
} from "../../common/utils";
|
||||||
import diff_match_patch from "diff-match-patch";
|
import diff_match_patch from "diff-match-patch";
|
||||||
import { stripAllPrefixes, isPlainText } from "../../lib/src/string_and_binary/path";
|
import { stripAllPrefixes, isPlainText } from "../../lib/src/string_and_binary/path";
|
||||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
|
||||||
import { eventHub } from "../../common/events.ts";
|
import { eventHub } from "../../common/events.ts";
|
||||||
|
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface LSEvents {
|
interface LSEvents {
|
||||||
@@ -29,8 +30,8 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ModuleConflictResolver extends AbstractModule implements ICoreModule {
|
export class ModuleConflictResolver extends AbstractModule {
|
||||||
async $$resolveConflictByDeletingRev(
|
private async _resolveConflictByDeletingRev(
|
||||||
path: FilePathWithPrefix,
|
path: FilePathWithPrefix,
|
||||||
deleteRevision: string,
|
deleteRevision: string,
|
||||||
subTitle = ""
|
subTitle = ""
|
||||||
@@ -82,7 +83,7 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
|
|||||||
return MISSING_OR_ERROR;
|
return MISSING_OR_ERROR;
|
||||||
}
|
}
|
||||||
// 2. As usual, delete the conflicted revision and if there are no conflicts, write the resolved content to the storage.
|
// 2. As usual, delete the conflicted revision and if there are no conflicts, write the resolved content to the storage.
|
||||||
return await this.core.$$resolveConflictByDeletingRev(path, ret.conflictedRev, "Sensible");
|
return await this.services.conflict.resolveByDeletingRevision(path, ret.conflictedRev, "Sensible");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rightRev, leftLeaf, rightLeaf } = ret;
|
const { rightRev, leftLeaf, rightLeaf } = ret;
|
||||||
@@ -95,7 +96,7 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
|
|||||||
}
|
}
|
||||||
if (rightLeaf == false) {
|
if (rightLeaf == false) {
|
||||||
// Conflicted item could not load, delete this.
|
// Conflicted item could not load, delete this.
|
||||||
return await this.core.$$resolveConflictByDeletingRev(path, rightRev, "MISSING OLD REV");
|
return await this.services.conflict.resolveByDeletingRevision(path, rightRev, "MISSING OLD REV");
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSame = leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted;
|
const isSame = leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted;
|
||||||
@@ -115,7 +116,7 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
|
|||||||
]
|
]
|
||||||
.filter((e) => e.trim())
|
.filter((e) => e.trim())
|
||||||
.join(",");
|
.join(",");
|
||||||
return await this.core.$$resolveConflictByDeletingRev(path, loser.rev, subTitle);
|
return await this.services.conflict.resolveByDeletingRevision(path, loser.rev, subTitle);
|
||||||
}
|
}
|
||||||
// make diff.
|
// make diff.
|
||||||
const dmp = new diff_match_patch();
|
const dmp = new diff_match_patch();
|
||||||
@@ -129,7 +130,7 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$resolveConflict(filename: FilePathWithPrefix): Promise<void> {
|
private async _resolveConflict(filename: FilePathWithPrefix): Promise<void> {
|
||||||
// const filename = filenames[0];
|
// const filename = filenames[0];
|
||||||
return await serialized(`conflict-resolve:${filename}`, async () => {
|
return await serialized(`conflict-resolve:${filename}`, async () => {
|
||||||
const conflictCheckResult = await this.checkConflictAndPerformAutoMerge(filename);
|
const conflictCheckResult = await this.checkConflictAndPerformAutoMerge(filename);
|
||||||
@@ -144,16 +145,16 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
|
|||||||
}
|
}
|
||||||
if (conflictCheckResult === AUTO_MERGED) {
|
if (conflictCheckResult === AUTO_MERGED) {
|
||||||
//auto resolved, but need check again;
|
//auto resolved, but need check again;
|
||||||
if (this.settings.syncAfterMerge && !this.core.$$isSuspended()) {
|
if (this.settings.syncAfterMerge && !this.services.appLifecycle.isSuspended()) {
|
||||||
//Wait for the running replication, if not running replication, run it once.
|
//Wait for the running replication, if not running replication, run it once.
|
||||||
await this.core.$$replicateByEvent();
|
await this.services.replication.replicateByEvent();
|
||||||
}
|
}
|
||||||
this._log("[conflict] Automatically merged, but we have to check it again");
|
this._log("[conflict] Automatically merged, but we have to check it again");
|
||||||
await this.core.$$queueConflictCheck(filename);
|
await this.services.conflict.queueCheckFor(filename);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.settings.showMergeDialogOnlyOnActive) {
|
if (this.settings.showMergeDialogOnlyOnActive) {
|
||||||
const af = this.core.$$getActiveFilePath();
|
const af = this.services.vault.getActiveFilePath();
|
||||||
if (af && af != filename) {
|
if (af && af != filename) {
|
||||||
this._log(
|
this._log(
|
||||||
`[conflict] ${filename} is conflicted. Merging process has been postponed to the file have got opened.`,
|
`[conflict] ${filename} is conflicted. Merging process has been postponed to the file have got opened.`,
|
||||||
@@ -164,11 +165,11 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
|
|||||||
}
|
}
|
||||||
this._log("[conflict] Manual merge required!");
|
this._log("[conflict] Manual merge required!");
|
||||||
eventHub.emitEvent("conflict-cancelled", filename);
|
eventHub.emitEvent("conflict-cancelled", filename);
|
||||||
await this.core.$anyResolveConflictByUI(filename, conflictCheckResult);
|
await this.services.conflict.resolveByUserInteraction(filename, conflictCheckResult);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async $anyResolveConflictByNewest(filename: FilePathWithPrefix): Promise<boolean> {
|
private async _anyResolveConflictByNewest(filename: FilePathWithPrefix): Promise<boolean> {
|
||||||
const currentRev = await this.core.databaseFileAccess.fetchEntryMeta(filename, undefined, true);
|
const currentRev = await this.core.databaseFileAccess.fetchEntryMeta(filename, undefined, true);
|
||||||
if (currentRev == false) {
|
if (currentRev == false) {
|
||||||
this._log(`Could not get current revision of ${filename}`);
|
this._log(`Could not get current revision of ${filename}`);
|
||||||
@@ -206,8 +207,14 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
|
|||||||
this._log(
|
this._log(
|
||||||
`conflict: Deleting the older revision ${mTimeAndRev[i][1]} (${new Date(mTimeAndRev[i][0]).toLocaleString()}) of ${filename}`
|
`conflict: Deleting the older revision ${mTimeAndRev[i][1]} (${new Date(mTimeAndRev[i][0]).toLocaleString()}) of ${filename}`
|
||||||
);
|
);
|
||||||
await this.core.$$resolveConflictByDeletingRev(filename, mTimeAndRev[i][1], "NEWEST");
|
await this.services.conflict.resolveByDeletingRevision(filename, mTimeAndRev[i][1], "NEWEST");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
|
||||||
|
services.conflict.handleResolveByDeletingRevision(this._resolveConflictByDeletingRev.bind(this));
|
||||||
|
services.conflict.handleResolve(this._resolveConflict.bind(this));
|
||||||
|
services.conflict.handleResolveByNewest(this._anyResolveConflictByNewest.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||||
import { normalizePath } from "../../deps.ts";
|
import { normalizePath } from "../../deps.ts";
|
||||||
import {
|
import {
|
||||||
FLAGMD_REDFLAG,
|
FlagFilesHumanReadable,
|
||||||
FLAGMD_REDFLAG2,
|
FlagFilesOriginal,
|
||||||
FLAGMD_REDFLAG2_HR,
|
TweakValuesShouldMatchedTemplate,
|
||||||
FLAGMD_REDFLAG3,
|
|
||||||
FLAGMD_REDFLAG3_HR,
|
|
||||||
type ObsidianLiveSyncSettings,
|
type ObsidianLiveSyncSettings,
|
||||||
} from "../../lib/src/common/types.ts";
|
} from "../../lib/src/common/types.ts";
|
||||||
import { AbstractModule } from "../AbstractModule.ts";
|
import { AbstractModule } from "../AbstractModule.ts";
|
||||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
import { $msg } from "../../lib/src/common/i18n.ts";
|
import { SvelteDialogManager } from "../features/SetupWizard/ObsidianSvelteDialog.ts";
|
||||||
|
import FetchEverything from "../features/SetupWizard/dialogs/FetchEverything.svelte";
|
||||||
|
import RebuildEverything from "../features/SetupWizard/dialogs/RebuildEverything.svelte";
|
||||||
|
import { extractObject } from "octagonal-wheels/object";
|
||||||
|
|
||||||
export class ModuleRedFlag extends AbstractModule implements ICoreModule {
|
export class ModuleRedFlag extends AbstractModule {
|
||||||
async isFlagFileExist(path: string) {
|
async isFlagFileExist(path: string) {
|
||||||
const redflag = await this.core.storageAccess.isExists(normalizePath(path));
|
const redflag = await this.core.storageAccess.isExists(normalizePath(path));
|
||||||
if (redflag) {
|
if (redflag) {
|
||||||
@@ -33,169 +34,292 @@ export class ModuleRedFlag extends AbstractModule implements ICoreModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isRedFlagRaised = async () => await this.isFlagFileExist(FLAGMD_REDFLAG);
|
isSuspendFlagActive = async () => await this.isFlagFileExist(FlagFilesOriginal.SUSPEND_ALL);
|
||||||
isRedFlag2Raised = async () =>
|
isRebuildFlagActive = async () =>
|
||||||
(await this.isFlagFileExist(FLAGMD_REDFLAG2)) || (await this.isFlagFileExist(FLAGMD_REDFLAG2_HR));
|
(await this.isFlagFileExist(FlagFilesOriginal.REBUILD_ALL)) ||
|
||||||
isRedFlag3Raised = async () =>
|
(await this.isFlagFileExist(FlagFilesHumanReadable.REBUILD_ALL));
|
||||||
(await this.isFlagFileExist(FLAGMD_REDFLAG3)) || (await this.isFlagFileExist(FLAGMD_REDFLAG3_HR));
|
isFetchAllFlagActive = async () =>
|
||||||
|
(await this.isFlagFileExist(FlagFilesOriginal.FETCH_ALL)) ||
|
||||||
|
(await this.isFlagFileExist(FlagFilesHumanReadable.FETCH_ALL));
|
||||||
|
|
||||||
async deleteRedFlag2() {
|
async cleanupRebuildFlag() {
|
||||||
await this.deleteFlagFile(FLAGMD_REDFLAG2);
|
await this.deleteFlagFile(FlagFilesOriginal.REBUILD_ALL);
|
||||||
await this.deleteFlagFile(FLAGMD_REDFLAG2_HR);
|
await this.deleteFlagFile(FlagFilesHumanReadable.REBUILD_ALL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteRedFlag3() {
|
async cleanupFetchAllFlag() {
|
||||||
await this.deleteFlagFile(FLAGMD_REDFLAG3);
|
await this.deleteFlagFile(FlagFilesOriginal.FETCH_ALL);
|
||||||
await this.deleteFlagFile(FLAGMD_REDFLAG3_HR);
|
await this.deleteFlagFile(FlagFilesHumanReadable.FETCH_ALL);
|
||||||
}
|
}
|
||||||
async $everyOnLayoutReady(): Promise<boolean> {
|
dialogManager = new SvelteDialogManager(this.core);
|
||||||
try {
|
|
||||||
const isRedFlagRaised = await this.isRedFlagRaised();
|
|
||||||
const isRedFlag2Raised = await this.isRedFlag2Raised();
|
|
||||||
const isRedFlag3Raised = await this.isRedFlag3Raised();
|
|
||||||
|
|
||||||
if (isRedFlagRaised || isRedFlag2Raised || isRedFlag3Raised) {
|
/**
|
||||||
if (isRedFlag2Raised) {
|
* Adjust setting to remote if needed.
|
||||||
if (
|
* @param extra result of dialogues that may contain preventFetchingConfig flag (e.g, from FetchEverything or RebuildEverything)
|
||||||
(await this.core.confirm.askYesNoDialog(
|
* @param config current configuration to retrieve remote preferred config
|
||||||
"Rebuild everything has been scheduled! Are you sure to rebuild everything?",
|
*/
|
||||||
{ defaultOption: "Yes", timeout: 0 }
|
async adjustSettingToRemoteIfNeeded(extra: { preventFetchingConfig: boolean }, config: ObsidianLiveSyncSettings) {
|
||||||
)) !== "yes"
|
if (extra && extra.preventFetchingConfig) {
|
||||||
) {
|
return;
|
||||||
await this.deleteRedFlag2();
|
}
|
||||||
await this.core.$$performRestart();
|
|
||||||
return false;
|
// Remote configuration fetched and applied.
|
||||||
|
if (await this.adjustSettingToRemote(config)) {
|
||||||
|
config = this.core.settings;
|
||||||
|
} else {
|
||||||
|
this._log("Remote configuration not applied.", LOG_LEVEL_NOTICE);
|
||||||
|
}
|
||||||
|
console.debug(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust setting to remote configuration.
|
||||||
|
* @param config current configuration to retrieve remote preferred config
|
||||||
|
* @returns updated configuration if applied, otherwise null.
|
||||||
|
*/
|
||||||
|
async adjustSettingToRemote(config: ObsidianLiveSyncSettings) {
|
||||||
|
// Fetch remote configuration unless prevented.
|
||||||
|
const SKIP_FETCH = "Skip and proceed";
|
||||||
|
const RETRY_FETCH = "Retry (recommended)";
|
||||||
|
let canProceed = false;
|
||||||
|
do {
|
||||||
|
const remoteTweaks = await this.services.tweakValue.fetchRemotePreferred(config);
|
||||||
|
if (!remoteTweaks) {
|
||||||
|
const choice = await this.core.confirm.askSelectStringDialogue(
|
||||||
|
"Could not fetch remote configuration. What do you want to do?",
|
||||||
|
[SKIP_FETCH, RETRY_FETCH] as const,
|
||||||
|
{
|
||||||
|
defaultAction: RETRY_FETCH,
|
||||||
|
timeout: 0,
|
||||||
|
title: "Fetch Remote Configuration Failed",
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
if (choice === SKIP_FETCH) {
|
||||||
|
canProceed = true;
|
||||||
}
|
}
|
||||||
if (isRedFlag3Raised) {
|
} else {
|
||||||
if (
|
const necessary = extractObject(TweakValuesShouldMatchedTemplate, remoteTweaks);
|
||||||
(await this.core.confirm.askYesNoDialog("Fetch again has been scheduled! Are you sure?", {
|
// Check if any necessary tweak value is different from current config.
|
||||||
defaultOption: "Yes",
|
const differentItems = Object.entries(necessary).filter(([key, value]) => {
|
||||||
timeout: 0,
|
return (config as any)[key] !== value;
|
||||||
})) !== "yes"
|
});
|
||||||
) {
|
if (differentItems.length === 0) {
|
||||||
await this.deleteRedFlag3();
|
|
||||||
await this.core.$$performRestart();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.settings.batchSave = false;
|
|
||||||
await this.core.$allSuspendAllSync();
|
|
||||||
await this.core.$allSuspendExtraSync();
|
|
||||||
this.settings.suspendFileWatching = true;
|
|
||||||
await this.saveSettings();
|
|
||||||
if (isRedFlag2Raised) {
|
|
||||||
this._log(
|
this._log(
|
||||||
`${FLAGMD_REDFLAG2} or ${FLAGMD_REDFLAG2_HR} has been detected! Self-hosted LiveSync suspends all sync and rebuild everything.`,
|
"Remote configuration matches local configuration. No changes applied.",
|
||||||
LOG_LEVEL_NOTICE
|
LOG_LEVEL_NOTICE
|
||||||
);
|
);
|
||||||
await this.core.rebuilder.$rebuildEverything();
|
|
||||||
await this.deleteRedFlag2();
|
|
||||||
if (
|
|
||||||
(await this.core.confirm.askYesNoDialog(
|
|
||||||
"Do you want to resume file and database processing, and restart obsidian now?",
|
|
||||||
{ defaultOption: "Yes", timeout: 15 }
|
|
||||||
)) == "yes"
|
|
||||||
) {
|
|
||||||
this.settings.suspendFileWatching = false;
|
|
||||||
await this.saveSettings();
|
|
||||||
this.core.$$performRestart();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (isRedFlag3Raised) {
|
|
||||||
this._log(
|
|
||||||
`${FLAGMD_REDFLAG3} or ${FLAGMD_REDFLAG3_HR} has been detected! Self-hosted LiveSync will discard the local database and fetch everything from the remote once again.`,
|
|
||||||
LOG_LEVEL_NOTICE
|
|
||||||
);
|
|
||||||
const method1 = $msg("RedFlag.Fetch.Method.FetchSafer");
|
|
||||||
const method2 = $msg("RedFlag.Fetch.Method.FetchSmoother");
|
|
||||||
const method3 = $msg("RedFlag.Fetch.Method.FetchTraditional");
|
|
||||||
|
|
||||||
const methods = [method1, method2, method3] as const;
|
|
||||||
const chunkMode = await this.core.confirm.askSelectStringDialogue(
|
|
||||||
$msg("RedFlag.Fetch.Method.Desc"),
|
|
||||||
methods,
|
|
||||||
{
|
|
||||||
defaultAction: method1,
|
|
||||||
timeout: 0,
|
|
||||||
title: $msg("RedFlag.Fetch.Method.Title"),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let makeLocalChunkBeforeSync = false;
|
|
||||||
let makeLocalFilesBeforeSync = false;
|
|
||||||
if (chunkMode === method1) {
|
|
||||||
makeLocalFilesBeforeSync = true;
|
|
||||||
} else if (chunkMode === method2) {
|
|
||||||
makeLocalChunkBeforeSync = true;
|
|
||||||
} else if (chunkMode === method3) {
|
|
||||||
// Do nothing.
|
|
||||||
} else {
|
|
||||||
this._log("Cancelled the fetch operation", LOG_LEVEL_NOTICE);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const optionFetchRemoteConf = $msg("RedFlag.FetchRemoteConfig.Buttons.Fetch");
|
|
||||||
const optionCancel = $msg("RedFlag.FetchRemoteConfig.Buttons.Cancel");
|
|
||||||
const fetchRemote = await this.core.confirm.askSelectStringDialogue(
|
|
||||||
$msg("RedFlag.FetchRemoteConfig.Message"),
|
|
||||||
[optionFetchRemoteConf, optionCancel],
|
|
||||||
{
|
|
||||||
defaultAction: optionFetchRemoteConf,
|
|
||||||
timeout: 0,
|
|
||||||
title: $msg("RedFlag.FetchRemoteConfig.Title"),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (fetchRemote === optionFetchRemoteConf) {
|
|
||||||
this._log("Fetching remote configuration", LOG_LEVEL_NOTICE);
|
|
||||||
const newSettings = JSON.parse(JSON.stringify(this.core.settings)) as ObsidianLiveSyncSettings;
|
|
||||||
const remoteConfig = await this.core.$$fetchRemotePreferredTweakValues(newSettings);
|
|
||||||
if (remoteConfig) {
|
|
||||||
this._log("Remote configuration found.", LOG_LEVEL_NOTICE);
|
|
||||||
const mergedSettings = {
|
|
||||||
...this.core.settings,
|
|
||||||
...remoteConfig,
|
|
||||||
} satisfies ObsidianLiveSyncSettings;
|
|
||||||
this._log("Remote configuration applied.", LOG_LEVEL_NOTICE);
|
|
||||||
this.core.settings = mergedSettings;
|
|
||||||
} else {
|
|
||||||
this._log("Remote configuration not applied.", LOG_LEVEL_NOTICE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.core.rebuilder.$fetchLocal(makeLocalChunkBeforeSync, !makeLocalFilesBeforeSync);
|
|
||||||
|
|
||||||
await this.deleteRedFlag3();
|
|
||||||
if (this.settings.suspendFileWatching) {
|
|
||||||
if (
|
|
||||||
(await this.core.confirm.askYesNoDialog(
|
|
||||||
"Do you want to resume file and database processing, and restart obsidian now?",
|
|
||||||
{ defaultOption: "Yes", timeout: 15 }
|
|
||||||
)) == "yes"
|
|
||||||
) {
|
|
||||||
this.settings.suspendFileWatching = false;
|
|
||||||
await this.saveSettings();
|
|
||||||
this.core.$$performRestart();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._log(
|
|
||||||
"Your content of files will be synchronised gradually. Please wait for the completion.",
|
|
||||||
LOG_LEVEL_NOTICE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Case of FLAGMD_REDFLAG.
|
await this.core.confirm.askSelectStringDialogue(
|
||||||
this.settings.writeLogToTheFile = true;
|
"Your settings differed slightly from the server's. The plug-in has supplemented the incompatible parts with the server settings!",
|
||||||
// await this.plugin.openDatabase();
|
["OK"] as const,
|
||||||
const warningMessage =
|
{
|
||||||
"The red flag is raised! The whole initialize steps are skipped, and any file changes are not captured.";
|
defaultAction: "OK",
|
||||||
this._log(warningMessage, LOG_LEVEL_NOTICE);
|
timeout: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
...Object.fromEntries(differentItems),
|
||||||
|
} satisfies ObsidianLiveSyncSettings;
|
||||||
|
this.core.settings = config;
|
||||||
|
await this.core.services.setting.saveSettingData();
|
||||||
|
this._log("Remote configuration applied.", LOG_LEVEL_NOTICE);
|
||||||
|
canProceed = true;
|
||||||
|
return this.core.settings;
|
||||||
}
|
}
|
||||||
|
} while (!canProceed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process vault initialisation with suspending file watching and sync.
|
||||||
|
* @param proc process to be executed during initialisation, should return true if can be continued, false if app is unable to continue the process.
|
||||||
|
* @param keepSuspending whether to keep suspending file watching after the process.
|
||||||
|
* @returns result of the process, or false if error occurs.
|
||||||
|
*/
|
||||||
|
async processVaultInitialisation(proc: () => Promise<boolean>, keepSuspending = false) {
|
||||||
|
try {
|
||||||
|
// Disable batch saving and file watching during initialisation.
|
||||||
|
this.settings.batchSave = false;
|
||||||
|
await this.services.setting.suspendAllSync();
|
||||||
|
await this.services.setting.suspendExtraSync();
|
||||||
|
this.settings.suspendFileWatching = true;
|
||||||
|
await this.saveSettings();
|
||||||
|
try {
|
||||||
|
const result = await proc();
|
||||||
|
return result;
|
||||||
|
} catch (ex) {
|
||||||
|
this._log("Error during vault initialisation process.", LOG_LEVEL_NOTICE);
|
||||||
|
this._log(ex, LOG_LEVEL_VERBOSE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
this._log("Error during vault initialisation.", LOG_LEVEL_NOTICE);
|
||||||
|
this._log(ex, LOG_LEVEL_VERBOSE);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (!keepSuspending) {
|
||||||
|
// Re-enable file watching after initialisation.
|
||||||
|
this.settings.suspendFileWatching = false;
|
||||||
|
await this.saveSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the rebuild everything scheduled operation.
|
||||||
|
* @returns true if can be continued, false if app restart is needed.
|
||||||
|
*/
|
||||||
|
async onRebuildEverythingScheduled() {
|
||||||
|
const method = await this.dialogManager.openWithExplicitCancel(RebuildEverything);
|
||||||
|
if (method === "cancelled") {
|
||||||
|
// Clean up the flag file and restart the app.
|
||||||
|
this._log("Rebuild everything cancelled by user.", LOG_LEVEL_NOTICE);
|
||||||
|
await this.cleanupRebuildFlag();
|
||||||
|
this.services.appLifecycle.performRestart();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { extra } = method;
|
||||||
|
await this.adjustSettingToRemoteIfNeeded(extra, this.settings);
|
||||||
|
return await this.processVaultInitialisation(async () => {
|
||||||
|
await this.core.rebuilder.$rebuildEverything();
|
||||||
|
await this.cleanupRebuildFlag();
|
||||||
|
this._log("Rebuild everything operation completed.", LOG_LEVEL_NOTICE);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Handle the fetch all scheduled operation.
|
||||||
|
* @returns true if can be continued, false if app restart is needed.
|
||||||
|
*/
|
||||||
|
async onFetchAllScheduled() {
|
||||||
|
const method = await this.dialogManager.openWithExplicitCancel(FetchEverything);
|
||||||
|
if (method === "cancelled") {
|
||||||
|
this._log("Fetch everything cancelled by user.", LOG_LEVEL_NOTICE);
|
||||||
|
// Clean up the flag file and restart the app.
|
||||||
|
await this.cleanupFetchAllFlag();
|
||||||
|
this.services.appLifecycle.performRestart();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { vault, extra } = method;
|
||||||
|
|
||||||
|
const mapVaultStateToAction = {
|
||||||
|
identical: {
|
||||||
|
// If both are identical, no need to make local files/chunks before sync,
|
||||||
|
// Just for the efficiency, chunks should be made before sync.
|
||||||
|
makeLocalChunkBeforeSync: true,
|
||||||
|
makeLocalFilesBeforeSync: false,
|
||||||
|
},
|
||||||
|
independent: {
|
||||||
|
// If both are independent, nothing needs to be made before sync.
|
||||||
|
// Respect the remote state.
|
||||||
|
makeLocalChunkBeforeSync: false,
|
||||||
|
makeLocalFilesBeforeSync: false,
|
||||||
|
},
|
||||||
|
unbalanced: {
|
||||||
|
// If both are unbalanced, local files should be made before sync to avoid data loss.
|
||||||
|
// Then, chunks should be made before sync for the efficiency, but also the metadata made and should be detected as conflicting.
|
||||||
|
makeLocalChunkBeforeSync: false,
|
||||||
|
makeLocalFilesBeforeSync: true,
|
||||||
|
},
|
||||||
|
cancelled: {
|
||||||
|
// Cancelled case, not actually used.
|
||||||
|
makeLocalChunkBeforeSync: false,
|
||||||
|
makeLocalFilesBeforeSync: false,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
return await this.processVaultInitialisation(async () => {
|
||||||
|
await this.adjustSettingToRemoteIfNeeded(extra, this.settings);
|
||||||
|
// Okay, proceed to fetch everything.
|
||||||
|
const { makeLocalChunkBeforeSync, makeLocalFilesBeforeSync } = mapVaultStateToAction[vault];
|
||||||
|
this._log(
|
||||||
|
`Fetching everything with settings: makeLocalChunkBeforeSync=${makeLocalChunkBeforeSync}, makeLocalFilesBeforeSync=${makeLocalFilesBeforeSync}`,
|
||||||
|
LOG_LEVEL_INFO
|
||||||
|
);
|
||||||
|
await this.core.rebuilder.$fetchLocal(makeLocalChunkBeforeSync, !makeLocalFilesBeforeSync);
|
||||||
|
await this.cleanupFetchAllFlag();
|
||||||
|
this._log("Fetch everything operation completed. Vault files will be gradually synced.", LOG_LEVEL_NOTICE);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSuspendAllScheduled() {
|
||||||
|
this._log("SCRAM is detected. All operations are suspended.", LOG_LEVEL_NOTICE);
|
||||||
|
return await this.processVaultInitialisation(async () => {
|
||||||
|
this._log(
|
||||||
|
"All operations are suspended as per SCRAM.\nLogs will be written to the file. This might be a performance impact.",
|
||||||
|
LOG_LEVEL_NOTICE
|
||||||
|
);
|
||||||
|
this.settings.writeLogToTheFile = true;
|
||||||
|
await this.core.services.setting.saveSettingData();
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyAndUnlockSuspension() {
|
||||||
|
if (!this.settings.suspendFileWatching) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(await this.core.confirm.askYesNoDialog(
|
||||||
|
"Do you want to resume file and database processing, and restart obsidian now?",
|
||||||
|
{ defaultOption: "Yes", timeout: 15 }
|
||||||
|
)) != "yes"
|
||||||
|
) {
|
||||||
|
// TODO: Confirm actually proceed to next process.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
this.settings.suspendFileWatching = false;
|
||||||
|
await this.saveSettings();
|
||||||
|
this.services.appLifecycle.performRestart();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processFlagFilesOnStartup(): Promise<boolean> {
|
||||||
|
const isFlagSuspensionActive = await this.isSuspendFlagActive();
|
||||||
|
const isFlagRebuildActive = await this.isRebuildFlagActive();
|
||||||
|
const isFlagFetchAllActive = await this.isFetchAllFlagActive();
|
||||||
|
// TODO: Address the case when both flags are active (very unlikely though).
|
||||||
|
// if(isFlagFetchAllActive && isFlagRebuildActive) {
|
||||||
|
// const message = "Rebuild everything and Fetch everything flags are both detected.";
|
||||||
|
// await this.core.confirm.askSelectStringDialogue(
|
||||||
|
// "Both Rebuild Everything and Fetch Everything flags are detected. Please remove one of them and restart the app.",
|
||||||
|
// ["OK"] as const,)
|
||||||
|
if (isFlagFetchAllActive) {
|
||||||
|
const res = await this.onFetchAllScheduled();
|
||||||
|
if (res) {
|
||||||
|
return await this.verifyAndUnlockSuspension();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isFlagRebuildActive) {
|
||||||
|
const res = await this.onRebuildEverythingScheduled();
|
||||||
|
if (res) {
|
||||||
|
return await this.verifyAndUnlockSuspension();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isFlagSuspensionActive) {
|
||||||
|
const res = await this.onSuspendAllScheduled();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _everyOnLayoutReady(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const flagProcessResult = await this.processFlagFilesOnStartup();
|
||||||
|
return flagProcessResult;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
this._log("Something went wrong on FlagFile Handling", LOG_LEVEL_NOTICE);
|
this._log("Something went wrong on FlagFile Handling", LOG_LEVEL_NOTICE);
|
||||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
this._log(ex, LOG_LEVEL_VERBOSE);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
super.onBindFunction(core, services);
|
||||||
|
services.appLifecycle.handleLayoutReady(this._everyOnLayoutReady.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
|
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
import { AbstractModule } from "../AbstractModule.ts";
|
import { AbstractModule } from "../AbstractModule.ts";
|
||||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
|
||||||
|
|
||||||
export class ModuleRemoteGovernor extends AbstractModule implements ICoreModule {
|
export class ModuleRemoteGovernor extends AbstractModule {
|
||||||
async $$markRemoteLocked(lockByClean: boolean = false): Promise<void> {
|
private async _markRemoteLocked(lockByClean: boolean = false): Promise<void> {
|
||||||
return await this.core.replicator.markRemoteLocked(this.settings, true, lockByClean);
|
return await this.core.replicator.markRemoteLocked(this.settings, true, lockByClean);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$markRemoteUnlocked(): Promise<void> {
|
private async _markRemoteUnlocked(): Promise<void> {
|
||||||
return await this.core.replicator.markRemoteLocked(this.settings, false, false);
|
return await this.core.replicator.markRemoteLocked(this.settings, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$markRemoteResolved(): Promise<void> {
|
private async _markRemoteResolved(): Promise<void> {
|
||||||
return await this.core.replicator.markRemoteResolved(this.settings);
|
return await this.core.replicator.markRemoteResolved(this.settings);
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
|
||||||
|
services.remote.handleMarkLocked(this._markRemoteLocked.bind(this));
|
||||||
|
services.remote.handleMarkUnlocked(this._markRemoteUnlocked.bind(this));
|
||||||
|
services.remote.handleMarkResolved(this._markRemoteResolved.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,21 +11,22 @@ import {
|
|||||||
} from "../../lib/src/common/types.ts";
|
} from "../../lib/src/common/types.ts";
|
||||||
import { escapeMarkdownValue } from "../../lib/src/common/utils.ts";
|
import { escapeMarkdownValue } from "../../lib/src/common/utils.ts";
|
||||||
import { AbstractModule } from "../AbstractModule.ts";
|
import { AbstractModule } from "../AbstractModule.ts";
|
||||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
|
||||||
import { $msg } from "../../lib/src/common/i18n.ts";
|
import { $msg } from "../../lib/src/common/i18n.ts";
|
||||||
|
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
export class ModuleResolvingMismatchedTweaks extends AbstractModule implements ICoreModule {
|
export class ModuleResolvingMismatchedTweaks extends AbstractModule {
|
||||||
async $anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> {
|
async _anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> {
|
||||||
if (!this.core.replicator.tweakSettingsMismatched && !this.core.replicator.preferredTweakValue) return false;
|
if (!this.core.replicator.tweakSettingsMismatched && !this.core.replicator.preferredTweakValue) return false;
|
||||||
const preferred = this.core.replicator.preferredTweakValue;
|
const preferred = this.core.replicator.preferredTweakValue;
|
||||||
if (!preferred) return false;
|
if (!preferred) return false;
|
||||||
const ret = await this.core.$$askResolvingMismatchedTweaks(preferred);
|
const ret = await this.services.tweakValue.askResolvingMismatched(preferred);
|
||||||
if (ret == "OK") return false;
|
if (ret == "OK") return false;
|
||||||
if (ret == "CHECKAGAIN") return "CHECKAGAIN";
|
if (ret == "CHECKAGAIN") return "CHECKAGAIN";
|
||||||
if (ret == "IGNORE") return true;
|
if (ret == "IGNORE") return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$checkAndAskResolvingMismatchedTweaks(
|
async _checkAndAskResolvingMismatchedTweaks(
|
||||||
preferred: Partial<TweakValues>
|
preferred: Partial<TweakValues>
|
||||||
): Promise<[TweakValues | boolean, boolean]> {
|
): Promise<[TweakValues | boolean, boolean]> {
|
||||||
const mine = extractObject(TweakValuesShouldMatchedTemplate, this.settings);
|
const mine = extractObject(TweakValuesShouldMatchedTemplate, this.settings);
|
||||||
@@ -127,7 +128,7 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule implements I
|
|||||||
return CHOICES[retKey];
|
return CHOICES[retKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
|
async _askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
|
||||||
if (!this.core.replicator.tweakSettingsMismatched) {
|
if (!this.core.replicator.tweakSettingsMismatched) {
|
||||||
return "OK";
|
return "OK";
|
||||||
}
|
}
|
||||||
@@ -137,7 +138,7 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule implements I
|
|||||||
}
|
}
|
||||||
const preferred = extractObject(TweakValuesShouldMatchedTemplate, tweaks);
|
const preferred = extractObject(TweakValuesShouldMatchedTemplate, tweaks);
|
||||||
|
|
||||||
const [conf, rebuildRequired] = await this.core.$$checkAndAskResolvingMismatchedTweaks(preferred);
|
const [conf, rebuildRequired] = await this.services.tweakValue.checkAndAskResolvingMismatched(preferred);
|
||||||
if (!conf) return "IGNORE";
|
if (!conf) return "IGNORE";
|
||||||
|
|
||||||
if (conf === true) {
|
if (conf === true) {
|
||||||
@@ -154,7 +155,7 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule implements I
|
|||||||
if (conf) {
|
if (conf) {
|
||||||
this.settings = { ...this.settings, ...conf };
|
this.settings = { ...this.settings, ...conf };
|
||||||
await this.core.replicator.setPreferredRemoteTweakSettings(this.settings);
|
await this.core.replicator.setPreferredRemoteTweakSettings(this.settings);
|
||||||
await this.core.$$saveSettingData();
|
await this.services.setting.saveSettingData();
|
||||||
if (rebuildRequired) {
|
if (rebuildRequired) {
|
||||||
await this.core.rebuilder.$fetchLocal();
|
await this.core.rebuilder.$fetchLocal();
|
||||||
}
|
}
|
||||||
@@ -164,8 +165,12 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule implements I
|
|||||||
return "IGNORE";
|
return "IGNORE";
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$fetchRemotePreferredTweakValues(trialSetting: RemoteDBSettings): Promise<TweakValues | false> {
|
async _fetchRemotePreferredTweakValues(trialSetting: RemoteDBSettings): Promise<TweakValues | false> {
|
||||||
const replicator = await this.core.$anyNewReplicator();
|
const replicator = await this.services.replicator.getNewReplicator(trialSetting);
|
||||||
|
if (!replicator) {
|
||||||
|
this._log("The remote type is not supported for fetching preferred tweak values.", LOG_LEVEL_NOTICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (await replicator.tryConnectRemote(trialSetting)) {
|
if (await replicator.tryConnectRemote(trialSetting)) {
|
||||||
const preferred = await replicator.getRemotePreferredTweakValues(trialSetting);
|
const preferred = await replicator.getRemotePreferredTweakValues(trialSetting);
|
||||||
if (preferred) {
|
if (preferred) {
|
||||||
@@ -178,17 +183,17 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule implements I
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$checkAndAskUseRemoteConfiguration(
|
async _checkAndAskUseRemoteConfiguration(
|
||||||
trialSetting: RemoteDBSettings
|
trialSetting: RemoteDBSettings
|
||||||
): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
|
): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
|
||||||
const preferred = await this.core.$$fetchRemotePreferredTweakValues(trialSetting);
|
const preferred = await this.services.tweakValue.fetchRemotePreferred(trialSetting);
|
||||||
if (preferred) {
|
if (preferred) {
|
||||||
return await this.$$askUseRemoteConfiguration(trialSetting, preferred);
|
return await this.services.tweakValue.askUseRemoteConfiguration(trialSetting, preferred);
|
||||||
}
|
}
|
||||||
return { result: false, requireFetch: false };
|
return { result: false, requireFetch: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$askUseRemoteConfiguration(
|
async _askUseRemoteConfiguration(
|
||||||
trialSetting: RemoteDBSettings,
|
trialSetting: RemoteDBSettings,
|
||||||
preferred: TweakValues
|
preferred: TweakValues
|
||||||
): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
|
): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
|
||||||
@@ -278,4 +283,13 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule implements I
|
|||||||
}
|
}
|
||||||
return { result: false, requireFetch: false };
|
return { result: false, requireFetch: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
|
||||||
|
services.tweakValue.handleFetchRemotePreferred(this._fetchRemotePreferredTweakValues.bind(this));
|
||||||
|
services.tweakValue.handleCheckAndAskResolvingMismatched(this._checkAndAskResolvingMismatchedTweaks.bind(this));
|
||||||
|
services.tweakValue.handleAskResolvingMismatched(this._askResolvingMismatchedTweaks.bind(this));
|
||||||
|
services.tweakValue.handleCheckAndAskUseRemoteConfiguration(this._checkAndAskUseRemoteConfiguration.bind(this));
|
||||||
|
services.tweakValue.handleAskUseRemoteConfiguration(this._askUseRemoteConfiguration.bind(this));
|
||||||
|
services.replication.handleCheckConnectionFailure(this._anyAfterConnectCheckFailed.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TFile, TFolder, type ListedFiles } from "obsidian";
|
import { TFile, TFolder, type ListedFiles } from "obsidian";
|
||||||
import { SerializedFileAccess } from "./storageLib/SerializedFileAccess";
|
import { SerializedFileAccess } from "./storageLib/SerializedFileAccess";
|
||||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
import { LOG_LEVEL_INFO, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_INFO, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||||
import type {
|
import type {
|
||||||
FilePath,
|
FilePath,
|
||||||
@@ -15,43 +15,72 @@ import { TFileToUXFileInfoStub, TFolderToUXFileInfoStub } from "./storageLib/uti
|
|||||||
import { StorageEventManagerObsidian, type StorageEventManager } from "./storageLib/StorageEventManager";
|
import { StorageEventManagerObsidian, type StorageEventManager } from "./storageLib/StorageEventManager";
|
||||||
import type { StorageAccess } from "../interfaces/StorageAccess";
|
import type { StorageAccess } from "../interfaces/StorageAccess";
|
||||||
import { createBlob, type CustomRegExp } from "../../lib/src/common/utils";
|
import { createBlob, type CustomRegExp } from "../../lib/src/common/utils";
|
||||||
|
import { serialized } from "octagonal-wheels/concurrency/lock_v2";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
import type ObsidianLiveSyncPlugin from "../../main.ts";
|
||||||
|
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
||||||
|
|
||||||
export class ModuleFileAccessObsidian extends AbstractObsidianModule implements IObsidianModule, StorageAccess {
|
const fileLockPrefix = "file-lock:";
|
||||||
|
|
||||||
|
export class ModuleFileAccessObsidian extends AbstractObsidianModule implements StorageAccess {
|
||||||
|
processingFiles: Set<FilePathWithPrefix> = new Set();
|
||||||
|
processWriteFile<T>(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise<T>): Promise<T> {
|
||||||
|
const path = typeof file === "string" ? file : file.path;
|
||||||
|
return serialized(`${fileLockPrefix}${path}`, async () => {
|
||||||
|
try {
|
||||||
|
this.processingFiles.add(path);
|
||||||
|
return await proc();
|
||||||
|
} finally {
|
||||||
|
this.processingFiles.delete(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
processReadFile<T>(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise<T>): Promise<T> {
|
||||||
|
const path = typeof file === "string" ? file : file.path;
|
||||||
|
return serialized(`${fileLockPrefix}${path}`, async () => {
|
||||||
|
try {
|
||||||
|
this.processingFiles.add(path);
|
||||||
|
return await proc();
|
||||||
|
} finally {
|
||||||
|
this.processingFiles.delete(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
isFileProcessing(file: UXFileInfoStub | FilePathWithPrefix): boolean {
|
||||||
|
const path = typeof file === "string" ? file : file.path;
|
||||||
|
return this.processingFiles.has(path);
|
||||||
|
}
|
||||||
vaultAccess!: SerializedFileAccess;
|
vaultAccess!: SerializedFileAccess;
|
||||||
vaultManager: StorageEventManager = new StorageEventManagerObsidian(this.plugin, this.core);
|
vaultManager: StorageEventManager = new StorageEventManagerObsidian(this.plugin, this.core, this);
|
||||||
$everyOnload(): Promise<boolean> {
|
private _everyOnload(): Promise<boolean> {
|
||||||
this.core.storageAccess = this;
|
this.core.storageAccess = this;
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
$everyOnFirstInitialize(): Promise<boolean> {
|
_everyOnFirstInitialize(): Promise<boolean> {
|
||||||
this.vaultManager.beginWatch();
|
this.vaultManager.beginWatch();
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
$allOnUnload(): Promise<boolean> {
|
|
||||||
// this.vaultManager.
|
|
||||||
return Promise.resolve(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// $$flushFileEventQueue(): void {
|
// $$flushFileEventQueue(): void {
|
||||||
// this.vaultManager.flushQueue();
|
// this.vaultManager.flushQueue();
|
||||||
// }
|
// }
|
||||||
|
|
||||||
$everyCommitPendingFileEvent(): Promise<boolean> {
|
_everyCommitPendingFileEvent(): Promise<boolean> {
|
||||||
this.vaultManager.flushQueue();
|
this.vaultManager.flushQueue();
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$everyOnloadStart(): Promise<boolean> {
|
_everyOnloadStart(): Promise<boolean> {
|
||||||
this.vaultAccess = new SerializedFileAccess(this.app, this.plugin);
|
this.vaultAccess = new SerializedFileAccess(this.app, this.plugin, this);
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$$isStorageInsensitive(): boolean {
|
_isStorageInsensitive(): boolean {
|
||||||
return this.vaultAccess.isStorageInsensitive();
|
return this.vaultAccess.isStorageInsensitive();
|
||||||
}
|
}
|
||||||
|
|
||||||
$$shouldCheckCaseInsensitive(): boolean {
|
_shouldCheckCaseInsensitive(): boolean {
|
||||||
if (this.$$isStorageInsensitive()) return false;
|
if (this.services.vault.isStorageInsensitive()) return false;
|
||||||
return !this.settings.handleFilenameCaseSensitive;
|
return !this.settings.handleFilenameCaseSensitive;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +222,7 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readStubContent(stub: UXFileInfoStub): Promise<UXFileInfo | false> {
|
async readStubContent(stub: UXFileInfoStub): Promise<UXFileInfo | false> {
|
||||||
const file = this.vaultAccess.getAbstractFileByPath(stub.path);
|
const file = this.vaultAccess.getAbstractFileByPath(stub.path);
|
||||||
if (!(file instanceof TFile)) {
|
if (!(file instanceof TFile)) {
|
||||||
@@ -202,6 +232,7 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
|
|||||||
const data = await this.vaultAccess.vaultReadAuto(file);
|
const data = await this.vaultAccess.vaultReadAuto(file);
|
||||||
return {
|
return {
|
||||||
...stub,
|
...stub,
|
||||||
|
...TFileToUXFileInfoStub(file),
|
||||||
body: createBlob(data),
|
body: createBlob(data),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -245,7 +276,7 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
|
|||||||
if (excludeFilter && excludeFilter.some((ee) => ee.test(file))) {
|
if (excludeFilter && excludeFilter.some((ee) => ee.test(file))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (await this.plugin.$$isIgnoredByIgnoreFiles(file)) continue;
|
if (await this.services.vault.isIgnoredByIgnoreFile(file)) continue;
|
||||||
files.push(file);
|
files.push(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +289,7 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
|
|||||||
if (excludeFilter && excludeFilter.some((e) => e.test(v))) {
|
if (excludeFilter && excludeFilter.some((e) => e.test(v))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (await this.plugin.$$isIgnoredByIgnoreFiles(v)) {
|
if (await this.services.vault.isIgnoredByIgnoreFile(v)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// OK, deep dive!
|
// OK, deep dive!
|
||||||
@@ -314,9 +345,9 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
async _deleteVaultItem(file: TFile | TFolder) {
|
async __deleteVaultItem(file: TFile | TFolder) {
|
||||||
if (file instanceof TFile) {
|
if (file instanceof TFile) {
|
||||||
if (!(await this.core.$$isTargetFile(file.path))) return;
|
if (!(await this.services.vault.isTargetFile(file.path))) return;
|
||||||
}
|
}
|
||||||
const dir = file.parent;
|
const dir = file.parent;
|
||||||
if (this.settings.trashInsteadDelete) {
|
if (this.settings.trashInsteadDelete) {
|
||||||
@@ -332,7 +363,7 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
|
|||||||
this._log(
|
this._log(
|
||||||
`All files under the parent directory (${dir.path}) have been deleted, so delete this one.`
|
`All files under the parent directory (${dir.path}) have been deleted, so delete this one.`
|
||||||
);
|
);
|
||||||
await this._deleteVaultItem(dir);
|
await this.__deleteVaultItem(dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,7 +374,19 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
|
|||||||
const file = this.vaultAccess.getAbstractFileByPath(path);
|
const file = this.vaultAccess.getAbstractFileByPath(path);
|
||||||
if (file === null) return;
|
if (file === null) return;
|
||||||
if (file instanceof TFile || file instanceof TFolder) {
|
if (file instanceof TFile || file instanceof TFolder) {
|
||||||
return await this._deleteVaultItem(file);
|
return await this.__deleteVaultItem(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) {
|
||||||
|
super(plugin, core);
|
||||||
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
|
||||||
|
services.vault.handleIsStorageInsensitive(this._isStorageInsensitive.bind(this));
|
||||||
|
services.setting.handleShouldCheckCaseInsensitively(this._shouldCheckCaseInsensitive.bind(this));
|
||||||
|
services.appLifecycle.handleFirstInitialise(this._everyOnFirstInitialize.bind(this));
|
||||||
|
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||||
|
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
|
||||||
|
services.fileProcessing.handleCommitPendingFileEvents(this._everyCommitPendingFileEvent.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// ModuleInputUIObsidian.ts
|
// ModuleInputUIObsidian.ts
|
||||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
||||||
import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject } from "../../common/utils.ts";
|
import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject } from "../../common/utils.ts";
|
||||||
import {
|
import {
|
||||||
@@ -13,12 +13,13 @@ import { Notice } from "../../deps.ts";
|
|||||||
import type { Confirm } from "../../lib/src/interfaces/Confirm.ts";
|
import type { Confirm } from "../../lib/src/interfaces/Confirm.ts";
|
||||||
import { setConfirmInstance } from "../../lib/src/PlatformAPIs/obsidian/Confirm.ts";
|
import { setConfirmInstance } from "../../lib/src/PlatformAPIs/obsidian/Confirm.ts";
|
||||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
// This module cannot be a common module because it depends on Obsidian's API.
|
// This module cannot be a common module because it depends on Obsidian's API.
|
||||||
// However, we have to make compatible one for other platform.
|
// However, we have to make compatible one for other platform.
|
||||||
|
|
||||||
export class ModuleInputUIObsidian extends AbstractObsidianModule implements IObsidianModule, Confirm {
|
export class ModuleInputUIObsidian extends AbstractObsidianModule implements Confirm {
|
||||||
$everyOnload(): Promise<boolean> {
|
private _everyOnload(): Promise<boolean> {
|
||||||
this.core.confirm = this;
|
this.core.confirm = this;
|
||||||
setConfirmInstance(this);
|
setConfirmInstance(this);
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
@@ -110,4 +111,8 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb
|
|||||||
): Promise<(typeof buttons)[number] | false> {
|
): Promise<(typeof buttons)[number] | false> {
|
||||||
return confirmWithMessage(this.plugin, title, contentMd, buttons, defaultAction, timeout);
|
return confirmWithMessage(this.plugin, title, contentMd, buttons, defaultAction, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import { type App, TFile, type DataWriteOptions, TFolder, TAbstractFile } from "../../../deps.ts";
|
import { type App, TFile, type DataWriteOptions, TFolder, TAbstractFile } from "../../../deps.ts";
|
||||||
import { serialized } from "../../../lib/src/concurrency/lock.ts";
|
|
||||||
import { Logger } from "../../../lib/src/common/logger.ts";
|
import { Logger } from "../../../lib/src/common/logger.ts";
|
||||||
import { isPlainText } from "../../../lib/src/string_and_binary/path.ts";
|
import { isPlainText } from "../../../lib/src/string_and_binary/path.ts";
|
||||||
import type { FilePath, HasSettings, UXFileInfoStub } from "../../../lib/src/common/types.ts";
|
import type { FilePath, HasSettings, UXFileInfoStub } from "../../../lib/src/common/types.ts";
|
||||||
import { createBinaryBlob, isDocContentSame } from "../../../lib/src/common/utils.ts";
|
import { createBinaryBlob, isDocContentSame } from "../../../lib/src/common/utils.ts";
|
||||||
import type { InternalFileInfo } from "../../../common/types.ts";
|
import type { InternalFileInfo } from "../../../common/types.ts";
|
||||||
import { markChangesAreSame } from "../../../common/utils.ts";
|
import { markChangesAreSame } from "../../../common/utils.ts";
|
||||||
import { type UXFileInfo } from "../../../lib/src/common/types.ts";
|
import type { StorageAccess } from "../../interfaces/StorageAccess.ts";
|
||||||
|
function toArrayBuffer(arr: Uint8Array<ArrayBuffer> | ArrayBuffer | DataView<ArrayBuffer>): ArrayBuffer {
|
||||||
function getFileLockKey(file: TFile | TFolder | string | UXFileInfo) {
|
|
||||||
return `fl:${typeof file == "string" ? file : file.path}`;
|
|
||||||
}
|
|
||||||
function toArrayBuffer(arr: Uint8Array | ArrayBuffer | DataView): ArrayBufferLike {
|
|
||||||
if (arr instanceof Uint8Array) {
|
if (arr instanceof Uint8Array) {
|
||||||
return arr.buffer;
|
return arr.buffer;
|
||||||
}
|
}
|
||||||
@@ -21,94 +16,97 @@ function toArrayBuffer(arr: Uint8Array | ArrayBuffer | DataView): ArrayBufferLik
|
|||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// function isFile(file: TFile | TFolder | string | UXFileInfo): boolean {
|
|
||||||
// file instanceof TFile;
|
|
||||||
// }
|
|
||||||
|
|
||||||
async function processReadFile<T>(file: TFile | TFolder | string | UXFileInfo, proc: () => Promise<T>) {
|
|
||||||
const ret = await serialized(getFileLockKey(file), () => proc());
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
async function processWriteFile<T>(file: TFile | TFolder | string | UXFileInfo, proc: () => Promise<T>) {
|
|
||||||
const ret = await serialized(getFileLockKey(file), () => proc());
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SerializedFileAccess {
|
export class SerializedFileAccess {
|
||||||
app: App;
|
app: App;
|
||||||
plugin: HasSettings<{ handleFilenameCaseSensitive: boolean }>;
|
plugin: HasSettings<{ handleFilenameCaseSensitive: boolean }>;
|
||||||
constructor(app: App, plugin: (typeof this)["plugin"]) {
|
storageAccess: StorageAccess;
|
||||||
|
constructor(app: App, plugin: SerializedFileAccess["plugin"], storageAccess: StorageAccess) {
|
||||||
this.app = app;
|
this.app = app;
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
|
this.storageAccess = storageAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryAdapterStat(file: TFile | string) {
|
async tryAdapterStat(file: TFile | string) {
|
||||||
const path = file instanceof TFile ? file.path : file;
|
const path = file instanceof TFile ? file.path : file;
|
||||||
return await processReadFile(file, async () => {
|
return await this.storageAccess.processReadFile(path as FilePath, async () => {
|
||||||
if (!(await this.app.vault.adapter.exists(path))) return null;
|
if (!(await this.app.vault.adapter.exists(path))) return null;
|
||||||
return this.app.vault.adapter.stat(path);
|
return this.app.vault.adapter.stat(path);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async adapterStat(file: TFile | string) {
|
async adapterStat(file: TFile | string) {
|
||||||
const path = file instanceof TFile ? file.path : file;
|
const path = file instanceof TFile ? file.path : file;
|
||||||
return await processReadFile(file, () => this.app.vault.adapter.stat(path));
|
return await this.storageAccess.processReadFile(path as FilePath, () => this.app.vault.adapter.stat(path));
|
||||||
}
|
}
|
||||||
async adapterExists(file: TFile | string) {
|
async adapterExists(file: TFile | string) {
|
||||||
const path = file instanceof TFile ? file.path : file;
|
const path = file instanceof TFile ? file.path : file;
|
||||||
return await processReadFile(file, () => this.app.vault.adapter.exists(path));
|
return await this.storageAccess.processReadFile(path as FilePath, () => this.app.vault.adapter.exists(path));
|
||||||
}
|
}
|
||||||
async adapterRemove(file: TFile | string) {
|
async adapterRemove(file: TFile | string) {
|
||||||
const path = file instanceof TFile ? file.path : file;
|
const path = file instanceof TFile ? file.path : file;
|
||||||
return await processReadFile(file, () => this.app.vault.adapter.remove(path));
|
return await this.storageAccess.processReadFile(path as FilePath, () => this.app.vault.adapter.remove(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
async adapterRead(file: TFile | string) {
|
async adapterRead(file: TFile | string) {
|
||||||
const path = file instanceof TFile ? file.path : file;
|
const path = file instanceof TFile ? file.path : file;
|
||||||
return await processReadFile(file, () => this.app.vault.adapter.read(path));
|
return await this.storageAccess.processReadFile(path as FilePath, () => this.app.vault.adapter.read(path));
|
||||||
}
|
}
|
||||||
async adapterReadBinary(file: TFile | string) {
|
async adapterReadBinary(file: TFile | string) {
|
||||||
const path = file instanceof TFile ? file.path : file;
|
const path = file instanceof TFile ? file.path : file;
|
||||||
return await processReadFile(file, () => this.app.vault.adapter.readBinary(path));
|
return await this.storageAccess.processReadFile(path as FilePath, () =>
|
||||||
|
this.app.vault.adapter.readBinary(path)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async adapterReadAuto(file: TFile | string) {
|
async adapterReadAuto(file: TFile | string) {
|
||||||
const path = file instanceof TFile ? file.path : file;
|
const path = file instanceof TFile ? file.path : file;
|
||||||
if (isPlainText(path)) return await processReadFile(file, () => this.app.vault.adapter.read(path));
|
if (isPlainText(path)) {
|
||||||
return await processReadFile(file, () => this.app.vault.adapter.readBinary(path));
|
return await this.storageAccess.processReadFile(path as FilePath, () => this.app.vault.adapter.read(path));
|
||||||
|
}
|
||||||
|
return await this.storageAccess.processReadFile(path as FilePath, () =>
|
||||||
|
this.app.vault.adapter.readBinary(path)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async adapterWrite(file: TFile | string, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions) {
|
async adapterWrite(
|
||||||
|
file: TFile | string,
|
||||||
|
data: string | ArrayBuffer | Uint8Array<ArrayBuffer>,
|
||||||
|
options?: DataWriteOptions
|
||||||
|
) {
|
||||||
const path = file instanceof TFile ? file.path : file;
|
const path = file instanceof TFile ? file.path : file;
|
||||||
if (typeof data === "string") {
|
if (typeof data === "string") {
|
||||||
return await processWriteFile(file, () => this.app.vault.adapter.write(path, data, options));
|
return await this.storageAccess.processWriteFile(path as FilePath, () =>
|
||||||
|
this.app.vault.adapter.write(path, data, options)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return await processWriteFile(file, () =>
|
return await this.storageAccess.processWriteFile(path as FilePath, () =>
|
||||||
this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), options)
|
this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), options)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async vaultCacheRead(file: TFile) {
|
async vaultCacheRead(file: TFile) {
|
||||||
return await processReadFile(file, () => this.app.vault.cachedRead(file));
|
return await this.storageAccess.processReadFile(file.path as FilePath, () => this.app.vault.cachedRead(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
async vaultRead(file: TFile) {
|
async vaultRead(file: TFile) {
|
||||||
return await processReadFile(file, () => this.app.vault.read(file));
|
return await this.storageAccess.processReadFile(file.path as FilePath, () => this.app.vault.read(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
async vaultReadBinary(file: TFile) {
|
async vaultReadBinary(file: TFile) {
|
||||||
return await processReadFile(file, () => this.app.vault.readBinary(file));
|
return await this.storageAccess.processReadFile(file.path as FilePath, () => this.app.vault.readBinary(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
async vaultReadAuto(file: TFile) {
|
async vaultReadAuto(file: TFile) {
|
||||||
const path = file.path;
|
const path = file.path;
|
||||||
if (isPlainText(path)) return await processReadFile(file, () => this.app.vault.read(file));
|
if (isPlainText(path)) {
|
||||||
return await processReadFile(file, () => this.app.vault.readBinary(file));
|
return await this.storageAccess.processReadFile(path as FilePath, () => this.app.vault.read(file));
|
||||||
|
}
|
||||||
|
return await this.storageAccess.processReadFile(path as FilePath, () => this.app.vault.readBinary(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
async vaultModify(file: TFile, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions) {
|
async vaultModify(file: TFile, data: string | ArrayBuffer | Uint8Array<ArrayBuffer>, options?: DataWriteOptions) {
|
||||||
if (typeof data === "string") {
|
if (typeof data === "string") {
|
||||||
return await processWriteFile(file, async () => {
|
return await this.storageAccess.processWriteFile(file.path as FilePath, async () => {
|
||||||
const oldData = await this.app.vault.read(file);
|
const oldData = await this.app.vault.read(file);
|
||||||
if (data === oldData) {
|
if (data === oldData) {
|
||||||
if (options && options.mtime) markChangesAreSame(file.path, file.stat.mtime, options.mtime);
|
if (options && options.mtime) markChangesAreSame(file.path, file.stat.mtime, options.mtime);
|
||||||
@@ -118,7 +116,7 @@ export class SerializedFileAccess {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return await processWriteFile(file, async () => {
|
return await this.storageAccess.processWriteFile(file.path as FilePath, async () => {
|
||||||
const oldData = await this.app.vault.readBinary(file);
|
const oldData = await this.app.vault.readBinary(file);
|
||||||
if (await isDocContentSame(createBinaryBlob(oldData), createBinaryBlob(data))) {
|
if (await isDocContentSame(createBinaryBlob(oldData), createBinaryBlob(data))) {
|
||||||
if (options && options.mtime) markChangesAreSame(file.path, file.stat.mtime, options.mtime);
|
if (options && options.mtime) markChangesAreSame(file.path, file.stat.mtime, options.mtime);
|
||||||
@@ -131,13 +129,17 @@ export class SerializedFileAccess {
|
|||||||
}
|
}
|
||||||
async vaultCreate(
|
async vaultCreate(
|
||||||
path: string,
|
path: string,
|
||||||
data: string | ArrayBuffer | Uint8Array,
|
data: string | ArrayBuffer | Uint8Array<ArrayBuffer>,
|
||||||
options?: DataWriteOptions
|
options?: DataWriteOptions
|
||||||
): Promise<TFile> {
|
): Promise<TFile> {
|
||||||
if (typeof data === "string") {
|
if (typeof data === "string") {
|
||||||
return await processWriteFile(path, () => this.app.vault.create(path, data, options));
|
return await this.storageAccess.processWriteFile(path as FilePath, () =>
|
||||||
|
this.app.vault.create(path, data, options)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return await processWriteFile(path, () => this.app.vault.createBinary(path, toArrayBuffer(data), options));
|
return await this.storageAccess.processWriteFile(path as FilePath, () =>
|
||||||
|
this.app.vault.createBinary(path, toArrayBuffer(data), options)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,10 +152,14 @@ export class SerializedFileAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(file: TFile | TFolder, force = false) {
|
async delete(file: TFile | TFolder, force = false) {
|
||||||
return await processWriteFile(file, () => this.app.vault.delete(file, force));
|
return await this.storageAccess.processWriteFile(file.path as FilePath, () =>
|
||||||
|
this.app.vault.delete(file, force)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
async trash(file: TFile | TFolder, force = false) {
|
async trash(file: TFile | TFolder, force = false) {
|
||||||
return await processWriteFile(file, () => this.app.vault.trash(file, force));
|
return await this.storageAccess.processWriteFile(file.path as FilePath, () =>
|
||||||
|
this.app.vault.trash(file, force)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isStorageInsensitive(): boolean {
|
isStorageInsensitive(): boolean {
|
||||||
|
|||||||
@@ -7,24 +7,26 @@ import {
|
|||||||
LOG_LEVEL_INFO,
|
LOG_LEVEL_INFO,
|
||||||
LOG_LEVEL_NOTICE,
|
LOG_LEVEL_NOTICE,
|
||||||
LOG_LEVEL_VERBOSE,
|
LOG_LEVEL_VERBOSE,
|
||||||
|
type FileEventType,
|
||||||
type FilePath,
|
type FilePath,
|
||||||
type FilePathWithPrefix,
|
type FilePathWithPrefix,
|
||||||
type UXFileInfoStub,
|
type UXFileInfoStub,
|
||||||
type UXInternalFileInfoStub,
|
type UXInternalFileInfoStub,
|
||||||
} from "../../../lib/src/common/types.ts";
|
} from "../../../lib/src/common/types.ts";
|
||||||
import { delay, fireAndForget, getFileRegExp } from "../../../lib/src/common/utils.ts";
|
import { delay, fireAndForget, getFileRegExp } from "../../../lib/src/common/utils.ts";
|
||||||
import { type FileEventItem, type FileEventType } from "../../../common/types.ts";
|
import { type FileEventItem } from "../../../common/types.ts";
|
||||||
import { serialized, skipIfDuplicated } from "../../../lib/src/concurrency/lock.ts";
|
import { serialized, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
|
||||||
import {
|
import {
|
||||||
finishAllWaitingForTimeout,
|
finishAllWaitingForTimeout,
|
||||||
finishWaitingForTimeout,
|
finishWaitingForTimeout,
|
||||||
isWaitingForTimeout,
|
isWaitingForTimeout,
|
||||||
waitForTimeout,
|
waitForTimeout,
|
||||||
} from "../../../lib/src/concurrency/task.ts";
|
} from "octagonal-wheels/concurrency/task";
|
||||||
import { Semaphore } from "../../../lib/src/concurrency/semaphore.ts";
|
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||||
import type { LiveSyncCore } from "../../../main.ts";
|
import type { LiveSyncCore } from "../../../main.ts";
|
||||||
import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "./utilObsidian.ts";
|
import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "./utilObsidian.ts";
|
||||||
import ObsidianLiveSyncPlugin from "../../../main.ts";
|
import ObsidianLiveSyncPlugin from "../../../main.ts";
|
||||||
|
import type { StorageAccess } from "../../interfaces/StorageAccess.ts";
|
||||||
// import { InternalFileToUXFileInfo } from "../platforms/obsidian.ts";
|
// import { InternalFileToUXFileInfo } from "../platforms/obsidian.ts";
|
||||||
|
|
||||||
export type FileEvent = {
|
export type FileEvent = {
|
||||||
@@ -46,6 +48,10 @@ export abstract class StorageEventManager {
|
|||||||
export class StorageEventManagerObsidian extends StorageEventManager {
|
export class StorageEventManagerObsidian extends StorageEventManager {
|
||||||
plugin: ObsidianLiveSyncPlugin;
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
core: LiveSyncCore;
|
core: LiveSyncCore;
|
||||||
|
storageAccess: StorageAccess;
|
||||||
|
get services() {
|
||||||
|
return this.core.services;
|
||||||
|
}
|
||||||
|
|
||||||
get shouldBatchSave() {
|
get shouldBatchSave() {
|
||||||
return this.core.settings?.batchSave && this.core.settings?.liveSync != true;
|
return this.core.settings?.batchSave && this.core.settings?.liveSync != true;
|
||||||
@@ -56,8 +62,9 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
get batchSaveMaximumDelay(): number {
|
get batchSaveMaximumDelay(): number {
|
||||||
return this.core.settings?.batchSaveMaximumDelay ?? DEFAULT_SETTINGS.batchSaveMaximumDelay;
|
return this.core.settings?.batchSaveMaximumDelay ?? DEFAULT_SETTINGS.batchSaveMaximumDelay;
|
||||||
}
|
}
|
||||||
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) {
|
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, storageAccess: StorageAccess) {
|
||||||
super();
|
super();
|
||||||
|
this.storageAccess = storageAccess;
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.core = core;
|
this.core = core;
|
||||||
}
|
}
|
||||||
@@ -88,6 +95,10 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
}
|
}
|
||||||
const file = info?.file as TFile;
|
const file = info?.file as TFile;
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
|
||||||
|
// Logger(`Editor change skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.isWaiting(file.path as FilePath)) {
|
if (!this.isWaiting(file.path as FilePath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -102,22 +113,35 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
|
|
||||||
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
||||||
if (file instanceof TFolder) return;
|
if (file instanceof TFolder) return;
|
||||||
|
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
|
||||||
|
// Logger(`File create skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const fileInfo = TFileToUXFileInfoStub(file);
|
const fileInfo = TFileToUXFileInfoStub(file);
|
||||||
void this.appendQueue([{ type: "CREATE", file: fileInfo }], ctx);
|
void this.appendQueue([{ type: "CREATE", file: fileInfo }], ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
watchVaultChange(file: TAbstractFile, ctx?: any) {
|
watchVaultChange(file: TAbstractFile, ctx?: any) {
|
||||||
if (file instanceof TFolder) return;
|
if (file instanceof TFolder) return;
|
||||||
|
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
|
||||||
|
// Logger(`File change skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const fileInfo = TFileToUXFileInfoStub(file);
|
const fileInfo = TFileToUXFileInfoStub(file);
|
||||||
void this.appendQueue([{ type: "CHANGED", file: fileInfo }], ctx);
|
void this.appendQueue([{ type: "CHANGED", file: fileInfo }], ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
watchVaultDelete(file: TAbstractFile, ctx?: any) {
|
watchVaultDelete(file: TAbstractFile, ctx?: any) {
|
||||||
if (file instanceof TFolder) return;
|
if (file instanceof TFolder) return;
|
||||||
|
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
|
||||||
|
// Logger(`File delete skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const fileInfo = TFileToUXFileInfoStub(file, true);
|
const fileInfo = TFileToUXFileInfoStub(file, true);
|
||||||
void this.appendQueue([{ type: "DELETE", file: fileInfo }], ctx);
|
void this.appendQueue([{ type: "DELETE", file: fileInfo }], ctx);
|
||||||
}
|
}
|
||||||
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
|
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
|
||||||
|
// vault Rename will not be raised for self-events (Self-hosted LiveSync will not handle 'rename').
|
||||||
if (file instanceof TFile) {
|
if (file instanceof TFile) {
|
||||||
const fileInfo = TFileToUXFileInfoStub(file);
|
const fileInfo = TFileToUXFileInfoStub(file);
|
||||||
void this.appendQueue(
|
void this.appendQueue(
|
||||||
@@ -145,13 +169,17 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
}
|
}
|
||||||
// Watch raw events (Internal API)
|
// Watch raw events (Internal API)
|
||||||
watchVaultRawEvents(path: FilePath) {
|
watchVaultRawEvents(path: FilePath) {
|
||||||
|
if (this.storageAccess.isFileProcessing(path)) {
|
||||||
|
// Logger(`Raw file event skipped because the file is being processed: ${path}`, LOG_LEVEL_VERBOSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Only for internal files.
|
// Only for internal files.
|
||||||
if (!this.plugin.settings) return;
|
if (!this.plugin.settings) return;
|
||||||
// if (this.plugin.settings.useIgnoreFiles && this.plugin.ignoreFiles.some(e => path.endsWith(e.trim()))) {
|
// if (this.plugin.settings.useIgnoreFiles && this.plugin.ignoreFiles.some(e => path.endsWith(e.trim()))) {
|
||||||
if (this.plugin.settings.useIgnoreFiles) {
|
if (this.plugin.settings.useIgnoreFiles) {
|
||||||
// If it is one of ignore files, refresh the cached one.
|
// If it is one of ignore files, refresh the cached one.
|
||||||
// (Calling$$isTargetFile will refresh the cache)
|
// (Calling$$isTargetFile will refresh the cache)
|
||||||
void this.plugin.$$isTargetFile(path).then(() => this._watchVaultRawEvents(path));
|
void this.services.vault.isTargetFile(path).then(() => this._watchVaultRawEvents(path));
|
||||||
} else {
|
} else {
|
||||||
this._watchVaultRawEvents(path);
|
this._watchVaultRawEvents(path);
|
||||||
}
|
}
|
||||||
@@ -185,7 +213,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
async appendQueue(params: FileEvent[], ctx?: any) {
|
async appendQueue(params: FileEvent[], ctx?: any) {
|
||||||
if (!this.core.settings.isConfigured) return;
|
if (!this.core.settings.isConfigured) return;
|
||||||
if (this.core.settings.suspendFileWatching) return;
|
if (this.core.settings.suspendFileWatching) return;
|
||||||
this.core.$$markFileListPossiblyChanged();
|
this.core.services.vault.markFileListPossiblyChanged();
|
||||||
// Flag up to be reload
|
// Flag up to be reload
|
||||||
const processFiles = new Set<FilePath>();
|
const processFiles = new Set<FilePath>();
|
||||||
for (const param of params) {
|
for (const param of params) {
|
||||||
@@ -198,7 +226,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
const oldPath = param.oldPath;
|
const oldPath = param.oldPath;
|
||||||
if (type !== "INTERNAL") {
|
if (type !== "INTERNAL") {
|
||||||
const size = (file as UXFileInfoStub).stat.size;
|
const size = (file as UXFileInfoStub).stat.size;
|
||||||
if (this.core.$$isFileSizeExceeded(size) && (type == "CREATE" || type == "CHANGED")) {
|
if (this.services.vault.isFileSizeTooLarge(size) && (type == "CREATE" || type == "CHANGED")) {
|
||||||
Logger(
|
Logger(
|
||||||
`The storage file has been changed but exceeds the maximum size. Skipping: ${param.file.path}`,
|
`The storage file has been changed but exceeds the maximum size. Skipping: ${param.file.path}`,
|
||||||
LOG_LEVEL_NOTICE
|
LOG_LEVEL_NOTICE
|
||||||
@@ -207,7 +235,10 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (file instanceof TFolder) continue;
|
if (file instanceof TFolder) continue;
|
||||||
if (!(await this.core.$$isTargetFile(file.path))) continue;
|
// TODO: Confirm why only the TFolder skipping
|
||||||
|
// Possibly following line is needed...
|
||||||
|
// if (file?.isFolder) continue;
|
||||||
|
if (!(await this.services.vault.isTargetFile(file.path))) continue;
|
||||||
|
|
||||||
// Stop cache using to prevent the corruption;
|
// Stop cache using to prevent the corruption;
|
||||||
// let cache: null | string | ArrayBuffer;
|
// let cache: null | string | ArrayBuffer;
|
||||||
@@ -264,7 +295,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
concurrentProcessing = Semaphore(5);
|
concurrentProcessing = Semaphore(5);
|
||||||
waitedSince = new Map<FilePath | FilePathWithPrefix, number>();
|
waitedSince = new Map<FilePath | FilePathWithPrefix, number>();
|
||||||
async startStandingBy(filename: FilePath) {
|
async startStandingBy(filename: FilePath) {
|
||||||
// If waited, cancel previous waiting.
|
// If waited, no need to start again (looping inside the function)
|
||||||
await skipIfDuplicated(`storage-event-manager-${filename}`, async () => {
|
await skipIfDuplicated(`storage-event-manager-${filename}`, async () => {
|
||||||
Logger(`Processing ${filename}: Starting`, LOG_LEVEL_DEBUG);
|
Logger(`Processing ${filename}: Starting`, LOG_LEVEL_DEBUG);
|
||||||
const release = await this.concurrentProcessing.acquire();
|
const release = await this.concurrentProcessing.acquire();
|
||||||
@@ -284,6 +315,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
// continue;
|
// continue;
|
||||||
// }
|
// }
|
||||||
const type = target.type;
|
const type = target.type;
|
||||||
|
// If already cancelled by other operation, skip this.
|
||||||
if (target.cancelled) {
|
if (target.cancelled) {
|
||||||
Logger(`Processing ${filename}: Cancelled (scheduled): ${operationType}`, LOG_LEVEL_DEBUG);
|
Logger(`Processing ${filename}: Cancelled (scheduled): ${operationType}`, LOG_LEVEL_DEBUG);
|
||||||
this.cancelStandingBy(target);
|
this.cancelStandingBy(target);
|
||||||
@@ -384,12 +416,12 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
const lockKey = `handleFile:${file.path}`;
|
const lockKey = `handleFile:${file.path}`;
|
||||||
return await serialized(lockKey, async () => {
|
return await serialized(lockKey, async () => {
|
||||||
if (queue.type == "INTERNAL" || file.isInternal) {
|
if (queue.type == "INTERNAL" || file.isInternal) {
|
||||||
await this.core.$anyProcessOptionalFileEvent(file.path as unknown as FilePath);
|
await this.core.services.fileProcessing.processOptionalFileEvent(file.path as unknown as FilePath);
|
||||||
} else {
|
} else {
|
||||||
const key = `file-last-proc-${queue.type}-${file.path}`;
|
const key = `file-last-proc-${queue.type}-${file.path}`;
|
||||||
const last = Number((await this.core.kvDB.get(key)) || 0);
|
const last = Number((await this.core.kvDB.get(key)) || 0);
|
||||||
if (queue.type == "DELETE") {
|
if (queue.type == "DELETE") {
|
||||||
await this.core.$anyHandlerProcessesFileEvent(queue);
|
await this.core.services.fileProcessing.processFileEvent(queue);
|
||||||
} else {
|
} else {
|
||||||
if (file.stat.mtime == last) {
|
if (file.stat.mtime == last) {
|
||||||
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL_VERBOSE);
|
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||||
@@ -397,7 +429,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
// this.cancelRelativeEvent(queue);
|
// this.cancelRelativeEvent(queue);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!(await this.core.$anyHandlerProcessesFileEvent(queue))) {
|
if (!(await this.core.services.fileProcessing.processFileEvent(queue))) {
|
||||||
Logger(
|
Logger(
|
||||||
`STORAGE -> DB: Handler failed, cancel the relative operations: ${file.path}`,
|
`STORAGE -> DB: Handler failed, cancel the relative operations: ${file.path}`,
|
||||||
LOG_LEVEL_INFO
|
LOG_LEVEL_INFO
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ import {
|
|||||||
import { isAnyNote } from "../../lib/src/common/utils.ts";
|
import { isAnyNote } from "../../lib/src/common/utils.ts";
|
||||||
import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
|
import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
|
||||||
import { AbstractModule } from "../AbstractModule.ts";
|
import { AbstractModule } from "../AbstractModule.ts";
|
||||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
|
||||||
import { withConcurrency } from "octagonal-wheels/iterable/map";
|
import { withConcurrency } from "octagonal-wheels/iterable/map";
|
||||||
export class ModuleInitializerFile extends AbstractModule implements ICoreModule {
|
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
||||||
async $$performFullScan(showingNotice?: boolean, ignoreSuspending: boolean = false): Promise<void> {
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
export class ModuleInitializerFile extends AbstractModule {
|
||||||
|
private async _performFullScan(showingNotice?: boolean, ignoreSuspending: boolean = false): Promise<boolean> {
|
||||||
this._log("Opening the key-value database", LOG_LEVEL_VERBOSE);
|
this._log("Opening the key-value database", LOG_LEVEL_VERBOSE);
|
||||||
const isInitialized = (await this.core.kvDB.get<boolean>("initialized")) || false;
|
const isInitialized = (await this.core.kvDB.get<boolean>("initialized")) || false;
|
||||||
// synchronize all files between database and storage.
|
// synchronize all files between database and storage.
|
||||||
@@ -32,7 +33,7 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
|
|||||||
"syncAll"
|
"syncAll"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ignoreSuspending && this.settings.suspendFileWatching) {
|
if (!ignoreSuspending && this.settings.suspendFileWatching) {
|
||||||
if (showingNotice) {
|
if (showingNotice) {
|
||||||
@@ -42,7 +43,7 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
|
|||||||
"syncAll"
|
"syncAll"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showingNotice) {
|
if (showingNotice) {
|
||||||
@@ -59,7 +60,7 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
|
|||||||
const _filesStorage = [] as typeof filesStorageSrc;
|
const _filesStorage = [] as typeof filesStorageSrc;
|
||||||
|
|
||||||
for (const f of filesStorageSrc) {
|
for (const f of filesStorageSrc) {
|
||||||
if (await this.core.$$isTargetFile(f.path, f != filesStorageSrc[0])) {
|
if (await this.services.vault.isTargetFile(f.path, f != filesStorageSrc[0])) {
|
||||||
_filesStorage.push(f);
|
_filesStorage.push(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +104,7 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
|
|||||||
);
|
);
|
||||||
const path = getPath(doc);
|
const path = getPath(doc);
|
||||||
|
|
||||||
if (isValidPath(path) && (await this.core.$$isTargetFile(path, true))) {
|
if (isValidPath(path) && (await this.services.vault.isTargetFile(path, true))) {
|
||||||
if (!isMetaEntry(doc)) {
|
if (!isMetaEntry(doc)) {
|
||||||
this._log(`Invalid entry: ${path}`, LOG_LEVEL_INFO);
|
this._log(`Invalid entry: ${path}`, LOG_LEVEL_INFO);
|
||||||
continue;
|
continue;
|
||||||
@@ -133,7 +134,6 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
|
|||||||
this._log(`Total files in the database: ${databaseFileNames.length}`, LOG_LEVEL_VERBOSE, "syncAll");
|
this._log(`Total files in the database: ${databaseFileNames.length}`, LOG_LEVEL_VERBOSE, "syncAll");
|
||||||
this._log(`Total files in the storage: ${storageFileNames.length}`, LOG_LEVEL_VERBOSE, "syncAll");
|
this._log(`Total files in the storage: ${storageFileNames.length}`, LOG_LEVEL_VERBOSE, "syncAll");
|
||||||
this._log(`Total files: ${allFiles.length}`, LOG_LEVEL_VERBOSE, "syncAll");
|
this._log(`Total files: ${allFiles.length}`, LOG_LEVEL_VERBOSE, "syncAll");
|
||||||
|
|
||||||
const filesExistOnlyInStorage = allFiles.filter((e) => !databaseFileNameCI2CS[e]);
|
const filesExistOnlyInStorage = allFiles.filter((e) => !databaseFileNameCI2CS[e]);
|
||||||
const filesExistOnlyInDatabase = allFiles.filter((e) => !storageFileNameCI2CS[e]);
|
const filesExistOnlyInDatabase = allFiles.filter((e) => !storageFileNameCI2CS[e]);
|
||||||
const filesExistBoth = allFiles.filter((e) => databaseFileNameCI2CS[e] && storageFileNameCI2CS[e]);
|
const filesExistBoth = allFiles.filter((e) => databaseFileNameCI2CS[e] && storageFileNameCI2CS[e]);
|
||||||
@@ -192,7 +192,7 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
|
|||||||
runAll("UPDATE DATABASE", filesExistOnlyInStorage, async (e) => {
|
runAll("UPDATE DATABASE", filesExistOnlyInStorage, async (e) => {
|
||||||
// Exists in storage but not in database.
|
// Exists in storage but not in database.
|
||||||
const file = storageFileNameMap[storageFileNameCI2CS[e]];
|
const file = storageFileNameMap[storageFileNameCI2CS[e]];
|
||||||
if (!this.core.$$isFileSizeExceeded(file.stat.size)) {
|
if (!this.services.vault.isFileSizeTooLarge(file.stat.size)) {
|
||||||
const path = file.path;
|
const path = file.path;
|
||||||
await this.core.fileHandler.storeFileToDB(file);
|
await this.core.fileHandler.storeFileToDB(file);
|
||||||
// fireAndForget(() => this.checkAndApplySettingFromMarkdown(path, true));
|
// fireAndForget(() => this.checkAndApplySettingFromMarkdown(path, true));
|
||||||
@@ -208,7 +208,7 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
|
|||||||
// Exists in database but not in storage.
|
// Exists in database but not in storage.
|
||||||
const path = getPath(w) ?? e;
|
const path = getPath(w) ?? e;
|
||||||
if (w && !(w.deleted || w._deleted)) {
|
if (w && !(w.deleted || w._deleted)) {
|
||||||
if (!this.core.$$isFileSizeExceeded(w.size)) {
|
if (!this.services.vault.isFileSizeTooLarge(w.size)) {
|
||||||
// Prevent applying the conflicted state to the storage.
|
// Prevent applying the conflicted state to the storage.
|
||||||
if (w._conflicts?.length ?? 0 > 0) {
|
if (w._conflicts?.length ?? 0 > 0) {
|
||||||
this._log(`UPDATE STORAGE: ${path} has conflicts. skipped (x)`, LOG_LEVEL_INFO);
|
this._log(`UPDATE STORAGE: ${path} has conflicts. skipped (x)`, LOG_LEVEL_INFO);
|
||||||
@@ -250,7 +250,10 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
|
|||||||
this._log(`SYNC DATABASE AND STORAGE: ${file.path} has conflicts. skipped`, LOG_LEVEL_INFO);
|
this._log(`SYNC DATABASE AND STORAGE: ${file.path} has conflicts. skipped`, LOG_LEVEL_INFO);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.core.$$isFileSizeExceeded(file.stat.size) && !this.core.$$isFileSizeExceeded(doc.size)) {
|
if (
|
||||||
|
!this.services.vault.isFileSizeTooLarge(file.stat.size) &&
|
||||||
|
!this.services.vault.isFileSizeTooLarge(doc.size)
|
||||||
|
) {
|
||||||
await this.syncFileBetweenDBandStorage(file, doc);
|
await this.syncFileBetweenDBandStorage(file, doc);
|
||||||
} else {
|
} else {
|
||||||
this._log(
|
this._log(
|
||||||
@@ -271,6 +274,7 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
|
|||||||
if (showingNotice) {
|
if (showingNotice) {
|
||||||
this._log("Initialize done!", LOG_LEVEL_NOTICE, "syncAll");
|
this._log("Initialize done!", LOG_LEVEL_NOTICE, "syncAll");
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncFileBetweenDBandStorage(file: UXFileInfoStub, doc: MetaEntry) {
|
async syncFileBetweenDBandStorage(file: UXFileInfoStub, doc: MetaEntry) {
|
||||||
@@ -289,7 +293,7 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
|
|||||||
const compareResult = compareFileFreshness(file, doc);
|
const compareResult = compareFileFreshness(file, doc);
|
||||||
switch (compareResult) {
|
switch (compareResult) {
|
||||||
case BASE_IS_NEW:
|
case BASE_IS_NEW:
|
||||||
if (!this.core.$$isFileSizeExceeded(file.stat.size)) {
|
if (!this.services.vault.isFileSizeTooLarge(file.stat.size)) {
|
||||||
this._log("STORAGE -> DB :" + file.path);
|
this._log("STORAGE -> DB :" + file.path);
|
||||||
await this.core.fileHandler.storeFileToDB(file);
|
await this.core.fileHandler.storeFileToDB(file);
|
||||||
} else {
|
} else {
|
||||||
@@ -300,7 +304,7 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TARGET_IS_NEW:
|
case TARGET_IS_NEW:
|
||||||
if (!this.core.$$isFileSizeExceeded(doc.size)) {
|
if (!this.services.vault.isFileSizeTooLarge(doc.size)) {
|
||||||
this._log("STORAGE <- DB :" + file.path);
|
this._log("STORAGE <- DB :" + file.path);
|
||||||
if (await this.core.fileHandler.dbToStorage(doc, stripAllPrefixes(file.path), true)) {
|
if (await this.core.fileHandler.dbToStorage(doc, stripAllPrefixes(file.path), true)) {
|
||||||
eventHub.emitEvent("event-file-changed", {
|
eventHub.emitEvent("event-file-changed", {
|
||||||
@@ -365,27 +369,31 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
|
|||||||
this._log(`Checking expired file history done`);
|
this._log(`Checking expired file history done`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$initializeDatabase(
|
private async _initializeDatabase(
|
||||||
showingNotice: boolean = false,
|
showingNotice: boolean = false,
|
||||||
reopenDatabase = true,
|
reopenDatabase = true,
|
||||||
ignoreSuspending: boolean = false
|
ignoreSuspending: boolean = false
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
this.core.$$resetIsReady();
|
this.services.appLifecycle.resetIsReady();
|
||||||
if (!reopenDatabase || (await this.core.$$openDatabase())) {
|
if (!reopenDatabase || (await this.services.database.openDatabase())) {
|
||||||
if (this.localDatabase.isReady) {
|
if (this.localDatabase.isReady) {
|
||||||
await this.core.$$performFullScan(showingNotice, ignoreSuspending);
|
await this.services.vault.scanVault(showingNotice, ignoreSuspending);
|
||||||
}
|
}
|
||||||
if (!(await this.core.$everyOnDatabaseInitialized(showingNotice))) {
|
if (!(await this.services.databaseEvents.onDatabaseInitialised(showingNotice))) {
|
||||||
this._log(`Initializing database has been failed on some module`, LOG_LEVEL_NOTICE);
|
this._log(`Initializing database has been failed on some module!`, LOG_LEVEL_NOTICE);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.core.$$markIsReady();
|
this.services.appLifecycle.markIsReady();
|
||||||
// run queued event once.
|
// run queued event once.
|
||||||
await this.core.$everyCommitPendingFileEvent();
|
await this.services.fileProcessing.commitPendingFileEvents();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
this.core.$$resetIsReady();
|
this.services.appLifecycle.resetIsReady();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
|
||||||
|
services.databaseEvents.handleInitialiseDatabase(this._initializeDatabase.bind(this));
|
||||||
|
services.vault.handleScanVault(this._performFullScan.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { OpenKeyValueDatabase } from "../../common/KeyValueDB.ts";
|
|||||||
import type { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB.ts";
|
import type { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB.ts";
|
||||||
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||||
import { AbstractModule } from "../AbstractModule.ts";
|
import { AbstractModule } from "../AbstractModule.ts";
|
||||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
export class ModuleKeyValueDB extends AbstractModule implements ICoreModule {
|
export class ModuleKeyValueDB extends AbstractModule {
|
||||||
tryCloseKvDB() {
|
tryCloseKvDB() {
|
||||||
try {
|
try {
|
||||||
this.core.kvDB?.close();
|
this.core.kvDB?.close();
|
||||||
@@ -22,7 +22,7 @@ export class ModuleKeyValueDB extends AbstractModule implements ICoreModule {
|
|||||||
this.tryCloseKvDB();
|
this.tryCloseKvDB();
|
||||||
await delay(10);
|
await delay(10);
|
||||||
await yieldMicrotask();
|
await yieldMicrotask();
|
||||||
this.core.kvDB = await OpenKeyValueDatabase(this.core.$$getVaultName() + "-livesync-kv");
|
this.core.kvDB = await OpenKeyValueDatabase(this.services.vault.getVaultName() + "-livesync-kv");
|
||||||
await yieldMicrotask();
|
await yieldMicrotask();
|
||||||
await delay(100);
|
await delay(100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -33,21 +33,23 @@ export class ModuleKeyValueDB extends AbstractModule implements ICoreModule {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
$allOnDBUnload(db: LiveSyncLocalDB): void {
|
_onDBUnload(db: LiveSyncLocalDB) {
|
||||||
if (this.core.kvDB) this.core.kvDB.close();
|
if (this.core.kvDB) this.core.kvDB.close();
|
||||||
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
$allOnDBClose(db: LiveSyncLocalDB): void {
|
_onDBClose(db: LiveSyncLocalDB) {
|
||||||
if (this.core.kvDB) this.core.kvDB.close();
|
if (this.core.kvDB) this.core.kvDB.close();
|
||||||
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $everyOnloadAfterLoadSettings(): Promise<boolean> {
|
private async _everyOnloadAfterLoadSettings(): Promise<boolean> {
|
||||||
if (!(await this.openKeyValueDB())) {
|
if (!(await this.openKeyValueDB())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.core.simpleStore = this.core.$$getSimpleStore<any>("os");
|
this.core.simpleStore = this.services.database.openSimpleStore<any>("os");
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
$$getSimpleStore<T>(kind: string) {
|
_getSimpleStore<T>(kind: string) {
|
||||||
const prefix = `${kind}-`;
|
const prefix = `${kind}-`;
|
||||||
return {
|
return {
|
||||||
get: async (key: string): Promise<T> => {
|
get: async (key: string): Promise<T> => {
|
||||||
@@ -75,18 +77,18 @@ export class ModuleKeyValueDB extends AbstractModule implements ICoreModule {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
$everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
_everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
||||||
return this.openKeyValueDB();
|
return this.openKeyValueDB();
|
||||||
}
|
}
|
||||||
|
|
||||||
async $everyOnResetDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
async _everyOnResetDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const kvDBKey = "queued-files";
|
const kvDBKey = "queued-files";
|
||||||
await this.core.kvDB.del(kvDBKey);
|
await this.core.kvDB.del(kvDBKey);
|
||||||
// localStorage.removeItem(lsKey);
|
// localStorage.removeItem(lsKey);
|
||||||
await this.core.kvDB.destroy();
|
await this.core.kvDB.destroy();
|
||||||
await yieldMicrotask();
|
await yieldMicrotask();
|
||||||
this.core.kvDB = await OpenKeyValueDatabase(this.core.$$getVaultName() + "-livesync-kv");
|
this.core.kvDB = await OpenKeyValueDatabase(this.services.vault.getVaultName() + "-livesync-kv");
|
||||||
await delay(100);
|
await delay(100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.core.kvDB = undefined!;
|
this.core.kvDB = undefined!;
|
||||||
@@ -96,4 +98,12 @@ export class ModuleKeyValueDB extends AbstractModule implements ICoreModule {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.databaseEvents.handleOnUnloadDatabase(this._onDBUnload.bind(this));
|
||||||
|
services.databaseEvents.handleOnCloseDatabase(this._onDBClose.bind(this));
|
||||||
|
services.databaseEvents.handleOnDatabaseInitialisation(this._everyOnInitializeDatabase.bind(this));
|
||||||
|
services.databaseEvents.handleOnResetDatabase(this._everyOnResetDatabase.bind(this));
|
||||||
|
services.database.handleOpenSimpleStore(this._getSimpleStore.bind(this));
|
||||||
|
services.appLifecycle.handleOnSettingLoaded(this._everyOnloadAfterLoadSettings.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,32 @@
|
|||||||
import { LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, Logger } from "../../lib/src/common/logger.ts";
|
||||||
import {
|
import {
|
||||||
EVENT_REQUEST_OPEN_P2P,
|
EVENT_REQUEST_OPEN_P2P,
|
||||||
EVENT_REQUEST_OPEN_SETTING_WIZARD,
|
EVENT_REQUEST_OPEN_SETTING_WIZARD,
|
||||||
EVENT_REQUEST_OPEN_SETTINGS,
|
EVENT_REQUEST_OPEN_SETTINGS,
|
||||||
EVENT_REQUEST_OPEN_SETUP_URI,
|
|
||||||
EVENT_REQUEST_RUN_DOCTOR,
|
EVENT_REQUEST_RUN_DOCTOR,
|
||||||
|
EVENT_REQUEST_RUN_FIX_INCOMPLETE,
|
||||||
eventHub,
|
eventHub,
|
||||||
} from "../../common/events.ts";
|
} from "../../common/events.ts";
|
||||||
import { AbstractModule } from "../AbstractModule.ts";
|
import { AbstractModule } from "../AbstractModule.ts";
|
||||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
|
||||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||||
import { performDoctorConsultation, RebuildOptions } from "../../lib/src/common/configForDoc.ts";
|
import { performDoctorConsultation, RebuildOptions } from "../../lib/src/common/configForDoc.ts";
|
||||||
|
import { getPath, isValidPath } from "../../common/utils.ts";
|
||||||
|
import { isMetaEntry } from "../../lib/src/common/types.ts";
|
||||||
|
import { isDeletedEntry, isDocContentSame, isLoadedEntry, readAsBlob } from "../../lib/src/common/utils.ts";
|
||||||
|
import { countCompromisedChunks } from "../../lib/src/pouchdb/negotiation.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
import { SetupManager } from "../features/SetupManager.ts";
|
||||||
|
|
||||||
export class ModuleMigration extends AbstractModule implements ICoreModule {
|
type ErrorInfo = {
|
||||||
|
path: string;
|
||||||
|
recordedSize: number;
|
||||||
|
actualSize: number;
|
||||||
|
storageSize: number;
|
||||||
|
contentMatched: boolean;
|
||||||
|
isConflicted?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ModuleMigration extends AbstractModule {
|
||||||
async migrateUsingDoctor(skipRebuild: boolean = false, activateReason = "updated", forceRescan = false) {
|
async migrateUsingDoctor(skipRebuild: boolean = false, activateReason = "updated", forceRescan = false) {
|
||||||
const { shouldRebuild, shouldRebuildLocal, isModified, settings } = await performDoctorConsultation(
|
const { shouldRebuild, shouldRebuildLocal, isModified, settings } = await performDoctorConsultation(
|
||||||
this.core,
|
this.core,
|
||||||
@@ -31,12 +45,15 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
|
|||||||
if (!skipRebuild) {
|
if (!skipRebuild) {
|
||||||
if (shouldRebuild) {
|
if (shouldRebuild) {
|
||||||
await this.core.rebuilder.scheduleRebuild();
|
await this.core.rebuilder.scheduleRebuild();
|
||||||
await this.core.$$performRestart();
|
this.services.appLifecycle.performRestart();
|
||||||
|
return false;
|
||||||
} else if (shouldRebuildLocal) {
|
} else if (shouldRebuildLocal) {
|
||||||
await this.core.rebuilder.scheduleFetch();
|
await this.core.rebuilder.scheduleFetch();
|
||||||
await this.core.$$performRestart();
|
this.services.appLifecycle.performRestart();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async migrateDisableBulkSend() {
|
async migrateDisableBulkSend() {
|
||||||
@@ -49,6 +66,9 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialMessage() {
|
async initialMessage() {
|
||||||
|
const manager = this.core.getModule(SetupManager);
|
||||||
|
return await manager.startOnBoarding();
|
||||||
|
/*
|
||||||
const message = $msg("moduleMigration.msgInitialSetup", {
|
const message = $msg("moduleMigration.msgInitialSetup", {
|
||||||
URI_DOC: $msg("moduleMigration.docUri"),
|
URI_DOC: $msg("moduleMigration.docUri"),
|
||||||
});
|
});
|
||||||
@@ -66,6 +86,7 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
async askAgainForSetupURI() {
|
async askAgainForSetupURI() {
|
||||||
@@ -96,30 +117,245 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $everyOnFirstInitialize(): Promise<boolean> {
|
async hasIncompleteDocs(force: boolean = false): Promise<boolean> {
|
||||||
|
const incompleteDocsChecked = (await this.core.kvDB.get<boolean>("checkIncompleteDocs")) || false;
|
||||||
|
if (incompleteDocsChecked && !force) {
|
||||||
|
this._log("Incomplete docs check already done, skipping.", LOG_LEVEL_VERBOSE);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._log("Checking for incomplete documents...", LOG_LEVEL_NOTICE, "check-incomplete");
|
||||||
|
|
||||||
|
const errorFiles = [] as ErrorInfo[];
|
||||||
|
for await (const metaDoc of this.localDatabase.findAllNormalDocs({ conflicts: true })) {
|
||||||
|
const path = getPath(metaDoc);
|
||||||
|
|
||||||
|
if (!isValidPath(path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!(await this.services.vault.isTargetFile(path, true))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isMetaEntry(metaDoc)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = await this.localDatabase.getDBEntryFromMeta(metaDoc);
|
||||||
|
if (!doc || !isLoadedEntry(doc)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isDeletedEntry(doc)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const isConflicted = metaDoc?._conflicts && metaDoc._conflicts.length > 0;
|
||||||
|
|
||||||
|
let storageFileContent;
|
||||||
|
try {
|
||||||
|
storageFileContent = await this.core.storageAccess.readHiddenFileBinary(path);
|
||||||
|
} catch (e) {
|
||||||
|
Logger(`Failed to read file ${path}: Possibly unprocessed or missing`);
|
||||||
|
Logger(e, LOG_LEVEL_VERBOSE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// const storageFileBlob = createBlob(storageFileContent);
|
||||||
|
const sizeOnStorage = storageFileContent.byteLength;
|
||||||
|
const recordedSize = doc.size;
|
||||||
|
const docBlob = readAsBlob(doc);
|
||||||
|
const actualSize = docBlob.size;
|
||||||
|
if (
|
||||||
|
recordedSize !== actualSize ||
|
||||||
|
sizeOnStorage !== actualSize ||
|
||||||
|
sizeOnStorage !== recordedSize ||
|
||||||
|
isConflicted
|
||||||
|
) {
|
||||||
|
const contentMatched = await isDocContentSame(doc.data, storageFileContent);
|
||||||
|
errorFiles.push({
|
||||||
|
path,
|
||||||
|
recordedSize,
|
||||||
|
actualSize,
|
||||||
|
storageSize: sizeOnStorage,
|
||||||
|
contentMatched,
|
||||||
|
isConflicted,
|
||||||
|
});
|
||||||
|
Logger(
|
||||||
|
`Size mismatch for ${path}: ${recordedSize} (DB Recorded) , ${actualSize} (DB Stored) , ${sizeOnStorage} (Storage Stored), ${contentMatched ? "Content Matched" : "Content Mismatched"} ${isConflicted ? "Conflicted" : "Not Conflicted"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errorFiles.length == 0) {
|
||||||
|
Logger("No size mismatches found", LOG_LEVEL_NOTICE);
|
||||||
|
await this.core.kvDB.set("checkIncompleteDocs", true);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
Logger(`Found ${errorFiles.length} size mismatches`, LOG_LEVEL_NOTICE);
|
||||||
|
// We have to repair them following rules and situations:
|
||||||
|
// A. DB Recorded != DB Stored
|
||||||
|
// A.1. DB Recorded == Storage Stored
|
||||||
|
// Possibly recoverable from storage. Just overwrite the DB content with storage content.
|
||||||
|
// A.2. Neither
|
||||||
|
// Probably it cannot be resolved on this device. Even if the storage content is larger than DB Recorded, it possibly corrupted.
|
||||||
|
// We do not fix it automatically. Leave it as is. Possibly other device can do this.
|
||||||
|
// B. DB Recorded == DB Stored , < Storage Stored
|
||||||
|
// Very fragile, if DB Recorded size is less than Storage Stored size, we possibly repair the content (The issue was `unexpectedly shortened file`).
|
||||||
|
// We do not fix it automatically, but it will be automatically overwritten in other process.
|
||||||
|
// C. DB Recorded == DB Stored , > Storage Stored
|
||||||
|
// Probably restored by the user by resolving A or B on other device, We should overwrite the storage
|
||||||
|
// Also do not fix it automatically. It should be overwritten by replication.
|
||||||
|
const recoverable = errorFiles.filter((e) => {
|
||||||
|
return e.recordedSize === e.storageSize && !e.isConflicted;
|
||||||
|
});
|
||||||
|
const unrecoverable = errorFiles.filter((e) => {
|
||||||
|
return e.recordedSize !== e.storageSize || e.isConflicted;
|
||||||
|
});
|
||||||
|
const fileInfo = (e: (typeof errorFiles)[0]) => {
|
||||||
|
return `${e.path} (M: ${e.recordedSize}, A: ${e.actualSize}, S: ${e.storageSize}) ${e.isConflicted ? "(Conflicted)" : ""}`;
|
||||||
|
};
|
||||||
|
const messageUnrecoverable =
|
||||||
|
unrecoverable.length > 0
|
||||||
|
? $msg("moduleMigration.fix0256.messageUnrecoverable", {
|
||||||
|
filesNotRecoverable: unrecoverable.map((e) => `- ${fileInfo(e)}`).join("\n"),
|
||||||
|
})
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const message = $msg("moduleMigration.fix0256.message", {
|
||||||
|
files: recoverable.map((e) => `- ${fileInfo(e)}`).join("\n"),
|
||||||
|
messageUnrecoverable,
|
||||||
|
});
|
||||||
|
const CHECK_IT_LATER = $msg("moduleMigration.fix0256.buttons.checkItLater");
|
||||||
|
const FIX = $msg("moduleMigration.fix0256.buttons.fix");
|
||||||
|
const DISMISS = $msg("moduleMigration.fix0256.buttons.DismissForever");
|
||||||
|
const ret = await this.core.confirm.askSelectStringDialogue(message, [CHECK_IT_LATER, FIX, DISMISS], {
|
||||||
|
title: $msg("moduleMigration.fix0256.title"),
|
||||||
|
defaultAction: CHECK_IT_LATER,
|
||||||
|
});
|
||||||
|
if (ret == FIX) {
|
||||||
|
for (const file of recoverable) {
|
||||||
|
// Overwrite the database with the files on the storage
|
||||||
|
const stubFile = this.core.storageAccess.getFileStub(file.path);
|
||||||
|
if (stubFile == null) {
|
||||||
|
Logger(`Could not find stub file for ${file.path}`, LOG_LEVEL_NOTICE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stubFile.stat.mtime = Date.now();
|
||||||
|
const result = await this.core.fileHandler.storeFileToDB(stubFile, true, false);
|
||||||
|
if (result) {
|
||||||
|
Logger(`Successfully restored ${file.path} from storage`);
|
||||||
|
} else {
|
||||||
|
Logger(`Failed to restore ${file.path} from storage`, LOG_LEVEL_NOTICE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (ret === DISMISS) {
|
||||||
|
// User chose to dismiss the issue
|
||||||
|
await this.core.kvDB.set("checkIncompleteDocs", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasCompromisedChunks(): Promise<boolean> {
|
||||||
|
Logger(`Checking for compromised chunks...`, LOG_LEVEL_VERBOSE);
|
||||||
|
if (!this.settings.encrypt) {
|
||||||
|
// If not encrypted, we do not need to check for compromised chunks.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Check local database for compromised chunks
|
||||||
|
const localCompromised = await countCompromisedChunks(this.localDatabase.localDatabase);
|
||||||
|
const remote = this.services.replicator.getActiveReplicator();
|
||||||
|
const remoteCompromised = this.core.managers.networkManager.isOnline
|
||||||
|
? await remote?.countCompromisedChunks()
|
||||||
|
: 0;
|
||||||
|
if (localCompromised === false) {
|
||||||
|
Logger(`Failed to count compromised chunks in local database`, LOG_LEVEL_NOTICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (remoteCompromised === false) {
|
||||||
|
Logger(`Failed to count compromised chunks in remote database`, LOG_LEVEL_NOTICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (remoteCompromised === 0 && localCompromised === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Logger(
|
||||||
|
`Found compromised chunks : ${localCompromised} in local, ${remoteCompromised} in remote`,
|
||||||
|
LOG_LEVEL_NOTICE
|
||||||
|
);
|
||||||
|
const title = $msg("moduleMigration.insecureChunkExist.title");
|
||||||
|
const msg = $msg("moduleMigration.insecureChunkExist.message");
|
||||||
|
const REBUILD = $msg("moduleMigration.insecureChunkExist.buttons.rebuild");
|
||||||
|
const FETCH = $msg("moduleMigration.insecureChunkExist.buttons.fetch");
|
||||||
|
const DISMISS = $msg("moduleMigration.insecureChunkExist.buttons.later");
|
||||||
|
const buttons = [REBUILD, FETCH, DISMISS];
|
||||||
|
if (remoteCompromised != 0) {
|
||||||
|
buttons.splice(buttons.indexOf(FETCH), 1);
|
||||||
|
}
|
||||||
|
const result = await this.core.confirm.askSelectStringDialogue(msg, buttons, {
|
||||||
|
title,
|
||||||
|
defaultAction: DISMISS,
|
||||||
|
timeout: 0,
|
||||||
|
});
|
||||||
|
if (result === REBUILD) {
|
||||||
|
// Rebuild the database
|
||||||
|
await this.core.rebuilder.scheduleRebuild();
|
||||||
|
this.services.appLifecycle.performRestart();
|
||||||
|
return false;
|
||||||
|
} else if (result === FETCH) {
|
||||||
|
// Fetch the latest data from remote
|
||||||
|
await this.core.rebuilder.scheduleFetch();
|
||||||
|
this.services.appLifecycle.performRestart();
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
// User chose to dismiss the issue
|
||||||
|
this._log($msg("moduleMigration.insecureChunkExist.laterMessage"), LOG_LEVEL_NOTICE);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _everyOnFirstInitialize(): Promise<boolean> {
|
||||||
if (!this.localDatabase.isReady) {
|
if (!this.localDatabase.isReady) {
|
||||||
this._log($msg("moduleMigration.logLocalDatabaseNotReady"), LOG_LEVEL_NOTICE);
|
this._log($msg("moduleMigration.logLocalDatabaseNotReady"), LOG_LEVEL_NOTICE);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.settings.isConfigured) {
|
if (this.settings.isConfigured) {
|
||||||
await this.migrateUsingDoctor(false);
|
if (!(await this.hasCompromisedChunks())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!(await this.hasIncompleteDocs())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!(await this.migrateUsingDoctor(false))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// await this.migrationCheck();
|
// await this.migrationCheck();
|
||||||
await this.migrateDisableBulkSend();
|
await this.migrateDisableBulkSend();
|
||||||
}
|
}
|
||||||
if (!this.settings.isConfigured) {
|
if (!this.settings.isConfigured) {
|
||||||
// Case sensitivity
|
// if (!(await this.initialMessage()) || !(await this.askAgainForSetupURI())) {
|
||||||
if (!(await this.initialMessage()) || !(await this.askAgainForSetupURI())) {
|
// this._log($msg("moduleMigration.logSetupCancelled"), LOG_LEVEL_NOTICE);
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
if (!(await this.initialMessage())) {
|
||||||
this._log($msg("moduleMigration.logSetupCancelled"), LOG_LEVEL_NOTICE);
|
this._log($msg("moduleMigration.logSetupCancelled"), LOG_LEVEL_NOTICE);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await this.migrateUsingDoctor(true);
|
if (!(await this.migrateUsingDoctor(true))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
$everyOnLayoutReady(): Promise<boolean> {
|
_everyOnLayoutReady(): Promise<boolean> {
|
||||||
eventHub.onEvent(EVENT_REQUEST_RUN_DOCTOR, async (reason) => {
|
eventHub.onEvent(EVENT_REQUEST_RUN_DOCTOR, async (reason) => {
|
||||||
await this.migrateUsingDoctor(false, reason, true);
|
await this.migrateUsingDoctor(false, reason, true);
|
||||||
});
|
});
|
||||||
|
eventHub.onEvent(EVENT_REQUEST_RUN_FIX_INCOMPLETE, async () => {
|
||||||
|
await this.hasIncompleteDocs(true);
|
||||||
|
});
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
super.onBindFunction(core, services);
|
||||||
|
services.appLifecycle.handleLayoutReady(this._everyOnLayoutReady.bind(this));
|
||||||
|
services.appLifecycle.handleFirstInitialise(this._everyOnFirstInitialize.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
import { LOG_LEVEL_DEBUG, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_DEBUG, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||||
import { Notice, requestUrl, type RequestUrlParam, type RequestUrlResponse } from "../../deps.ts";
|
import { Notice, requestUrl, type RequestUrlParam, type RequestUrlResponse } from "../../deps.ts";
|
||||||
import { type CouchDBCredentials, type EntryDoc, type FilePathWithPrefix } from "../../lib/src/common/types.ts";
|
import { type CouchDBCredentials, type EntryDoc, type FilePath } from "../../lib/src/common/types.ts";
|
||||||
import { getPathFromTFile } from "../../common/utils.ts";
|
import { getPathFromTFile } from "../../common/utils.ts";
|
||||||
import { isCloudantURI, isValidRemoteCouchDBURI } from "../../lib/src/pouchdb/utils_couchdb.ts";
|
import { isCloudantURI, isValidRemoteCouchDBURI } from "../../lib/src/pouchdb/utils_couchdb.ts";
|
||||||
import { replicationFilter } from "@/lib/src/pouchdb/compress.ts";
|
import { replicationFilter } from "@/lib/src/pouchdb/compress.ts";
|
||||||
@@ -11,6 +11,7 @@ import { setNoticeClass } from "../../lib/src/mock_and_interop/wrapper.ts";
|
|||||||
import { ObsHttpHandler } from "./APILib/ObsHttpHandler.ts";
|
import { ObsHttpHandler } from "./APILib/ObsHttpHandler.ts";
|
||||||
import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.ts";
|
import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.ts";
|
||||||
import { AuthorizationHeaderGenerator } from "../../lib/src/replication/httplib.ts";
|
import { AuthorizationHeaderGenerator } from "../../lib/src/replication/httplib.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
setNoticeClass(Notice);
|
setNoticeClass(Notice);
|
||||||
|
|
||||||
@@ -19,21 +20,21 @@ async function fetchByAPI(request: RequestUrlParam, errorAsResult = false): Prom
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidianModule {
|
export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||||
_customHandler!: ObsHttpHandler;
|
_customHandler!: ObsHttpHandler;
|
||||||
|
|
||||||
_authHeader = new AuthorizationHeaderGenerator();
|
_authHeader = new AuthorizationHeaderGenerator();
|
||||||
|
|
||||||
last_successful_post = false;
|
last_successful_post = false;
|
||||||
$$customFetchHandler(): ObsHttpHandler {
|
_customFetchHandler(): ObsHttpHandler {
|
||||||
if (!this._customHandler) this._customHandler = new ObsHttpHandler(undefined, undefined);
|
if (!this._customHandler) this._customHandler = new ObsHttpHandler(undefined, undefined);
|
||||||
return this._customHandler;
|
return this._customHandler;
|
||||||
}
|
}
|
||||||
$$getLastPostFailedBySize(): boolean {
|
_getLastPostFailedBySize(): boolean {
|
||||||
return !this.last_successful_post;
|
return !this.last_successful_post;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _fetchByAPI(url: string, authHeader: string, opts?: RequestInit): Promise<Response> {
|
async __fetchByAPI(url: string, authHeader: string, opts?: RequestInit): Promise<Response> {
|
||||||
const body = opts?.body as string;
|
const body = opts?.body as string;
|
||||||
|
|
||||||
const transformedHeaders = { ...(opts?.headers as Record<string, string>) };
|
const transformedHeaders = { ...(opts?.headers as Record<string, string>) };
|
||||||
@@ -68,7 +69,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
|
|||||||
const body = opts?.body as string;
|
const body = opts?.body as string;
|
||||||
const size = body ? ` (${body.length})` : "";
|
const size = body ? ` (${body.length})` : "";
|
||||||
try {
|
try {
|
||||||
const r = await this._fetchByAPI(url, authHeader, opts);
|
const r = await this.__fetchByAPI(url, authHeader, opts);
|
||||||
this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
|
this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
|
||||||
if (method == "POST" || method == "PUT") {
|
if (method == "POST" || method == "PUT") {
|
||||||
this.last_successful_post = r.status - (r.status % 100) == 200;
|
this.last_successful_post = r.status - (r.status % 100) == 200;
|
||||||
@@ -90,7 +91,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$connectRemoteCouchDB(
|
async _connectRemoteCouchDB(
|
||||||
uri: string,
|
uri: string,
|
||||||
auth: CouchDBCredentials,
|
auth: CouchDBCredentials,
|
||||||
disableRequestURI: boolean,
|
disableRequestURI: boolean,
|
||||||
@@ -101,11 +102,14 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
|
|||||||
compression: boolean,
|
compression: boolean,
|
||||||
customHeaders: Record<string, string>,
|
customHeaders: Record<string, string>,
|
||||||
useRequestAPI: boolean,
|
useRequestAPI: boolean,
|
||||||
getPBKDF2Salt: () => Promise<Uint8Array>
|
getPBKDF2Salt: () => Promise<Uint8Array<ArrayBuffer>>
|
||||||
): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> {
|
): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> {
|
||||||
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
|
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
|
||||||
if (uri.toLowerCase() != uri) return "Remote URI and database name could not contain capital letters.";
|
if (uri.toLowerCase() != uri) return "Remote URI and database name could not contain capital letters.";
|
||||||
if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces.";
|
if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces.";
|
||||||
|
if (!this.core.managers.networkManager.isOnline) {
|
||||||
|
return "Network is offline";
|
||||||
|
}
|
||||||
// let authHeader = await this._authHeader.getAuthorizationHeader(auth);
|
// let authHeader = await this._authHeader.getAuthorizationHeader(auth);
|
||||||
|
|
||||||
const conf: PouchDB.HttpAdapter.HttpAdapterConfiguration = {
|
const conf: PouchDB.HttpAdapter.HttpAdapterConfiguration = {
|
||||||
@@ -145,7 +149,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
|
|||||||
try {
|
try {
|
||||||
this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
|
this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
|
||||||
const response: Response = await (useRequestAPI
|
const response: Response = await (useRequestAPI
|
||||||
? this._fetchByAPI(url.toString(), authHeader, { ...opts, headers })
|
? this.__fetchByAPI(url.toString(), authHeader, { ...opts, headers })
|
||||||
: fetch(url, { ...opts, headers }));
|
: fetch(url, { ...opts, headers }));
|
||||||
if (method == "POST" || method == "PUT") {
|
if (method == "POST" || method == "PUT") {
|
||||||
this.last_successful_post = response.ok;
|
this.last_successful_post = response.ok;
|
||||||
@@ -249,21 +253,21 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$$isMobile(): boolean {
|
_isMobile(): boolean {
|
||||||
//@ts-ignore : internal API
|
//@ts-ignore : internal API
|
||||||
return this.app.isMobile;
|
return this.app.isMobile;
|
||||||
}
|
}
|
||||||
|
|
||||||
$$vaultName(): string {
|
_vaultName(): string {
|
||||||
return this.app.vault.getName();
|
return this.app.vault.getName();
|
||||||
}
|
}
|
||||||
$$getVaultName(): string {
|
_getVaultName(): string {
|
||||||
return (
|
return (
|
||||||
this.core.$$vaultName() +
|
this.services.vault.vaultName() +
|
||||||
(this.settings?.additionalSuffixOfDatabaseName ? "-" + this.settings.additionalSuffixOfDatabaseName : "")
|
(this.settings?.additionalSuffixOfDatabaseName ? "-" + this.settings.additionalSuffixOfDatabaseName : "")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
$$getActiveFilePath(): FilePathWithPrefix | undefined {
|
_getActiveFilePath(): FilePath | undefined {
|
||||||
const file = this.app.workspace.getActiveFile();
|
const file = this.app.workspace.getActiveFile();
|
||||||
if (file) {
|
if (file) {
|
||||||
return getPathFromTFile(file);
|
return getPathFromTFile(file);
|
||||||
@@ -271,7 +275,18 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
$anyGetAppId(): Promise<string | undefined> {
|
_anyGetAppId(): string {
|
||||||
return Promise.resolve(`${"appId" in this.app ? this.app.appId : ""}`);
|
return `${"appId" in this.app ? this.app.appId : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services) {
|
||||||
|
services.API.handleGetCustomFetchHandler(this._customFetchHandler.bind(this));
|
||||||
|
services.API.handleIsLastPostFailedDueToPayloadSize(this._getLastPostFailedBySize.bind(this));
|
||||||
|
services.remote.handleConnect(this._connectRemoteCouchDB.bind(this));
|
||||||
|
services.API.handleIsMobile(this._isMobile.bind(this));
|
||||||
|
services.vault.handleGetVaultName(this._getVaultName.bind(this));
|
||||||
|
services.vault.handleVaultName(this._vaultName.bind(this));
|
||||||
|
services.vault.handleGetActiveFilePath(this._getActiveFilePath.bind(this));
|
||||||
|
services.API.handleGetAppID(this._anyGetAppId.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
import { EVENT_FILE_RENAMED, EVENT_LEAF_ACTIVE_CHANGED, eventHub } from "../../common/events.js";
|
import { EVENT_FILE_RENAMED, EVENT_LEAF_ACTIVE_CHANGED, eventHub } from "../../common/events.js";
|
||||||
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||||
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
||||||
@@ -12,9 +12,10 @@ import {
|
|||||||
hiddenFilesEventCount,
|
hiddenFilesEventCount,
|
||||||
hiddenFilesProcessingCount,
|
hiddenFilesProcessingCount,
|
||||||
} from "../../lib/src/mock_and_interop/stores.ts";
|
} from "../../lib/src/mock_and_interop/stores.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
export class ModuleObsidianEvents extends AbstractObsidianModule implements IObsidianModule {
|
export class ModuleObsidianEvents extends AbstractObsidianModule {
|
||||||
$everyOnloadStart(): Promise<boolean> {
|
_everyOnloadStart(): Promise<boolean> {
|
||||||
// this.registerEvent(this.app.workspace.on("editor-change", ));
|
// this.registerEvent(this.app.workspace.on("editor-change", ));
|
||||||
this.plugin.registerEvent(
|
this.plugin.registerEvent(
|
||||||
this.app.vault.on("rename", (file, oldPath) => {
|
this.app.vault.on("rename", (file, oldPath) => {
|
||||||
@@ -30,11 +31,11 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
|
|||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$$performRestart(): void {
|
private _performRestart(): void {
|
||||||
this._performAppReload();
|
this.__performAppReload();
|
||||||
}
|
}
|
||||||
|
|
||||||
_performAppReload() {
|
__performAppReload() {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
this.app.commands.executeCommandById("app:reload");
|
this.app.commands.executeCommandById("app:reload");
|
||||||
}
|
}
|
||||||
@@ -49,14 +50,14 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
|
|||||||
this.initialCallback = save;
|
this.initialCallback = save;
|
||||||
saveCommandDefinition.callback = () => {
|
saveCommandDefinition.callback = () => {
|
||||||
scheduleTask("syncOnEditorSave", 250, () => {
|
scheduleTask("syncOnEditorSave", 250, () => {
|
||||||
if (this.core.$$isUnloaded()) {
|
if (this.services.appLifecycle.hasUnloaded()) {
|
||||||
this._log("Unload and remove the handler.", LOG_LEVEL_VERBOSE);
|
this._log("Unload and remove the handler.", LOG_LEVEL_VERBOSE);
|
||||||
saveCommandDefinition.callback = this.initialCallback;
|
saveCommandDefinition.callback = this.initialCallback;
|
||||||
this.initialCallback = undefined;
|
this.initialCallback = undefined;
|
||||||
} else {
|
} else {
|
||||||
if (this.settings.syncOnEditorSave) {
|
if (this.settings.syncOnEditorSave) {
|
||||||
this._log("Sync on Editor Save.", LOG_LEVEL_VERBOSE);
|
this._log("Sync on Editor Save.", LOG_LEVEL_VERBOSE);
|
||||||
fireAndForget(() => this.core.$$replicateByEvent());
|
fireAndForget(() => this.services.replication.replicateByEvent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -106,14 +107,14 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
|
|||||||
// TODO:FIXME AT V0.17.31, this logic has been disabled.
|
// TODO:FIXME AT V0.17.31, this logic has been disabled.
|
||||||
if (navigator.onLine && this.localDatabase.needScanning) {
|
if (navigator.onLine && this.localDatabase.needScanning) {
|
||||||
this.localDatabase.needScanning = false;
|
this.localDatabase.needScanning = false;
|
||||||
await this.core.$$performFullScan();
|
await this.services.vault.scanVault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async watchWindowVisibilityAsync() {
|
async watchWindowVisibilityAsync() {
|
||||||
if (this.settings.suspendFileWatching) return;
|
if (this.settings.suspendFileWatching) return;
|
||||||
if (!this.settings.isConfigured) return;
|
if (!this.settings.isConfigured) return;
|
||||||
if (!this.core.$$isReady()) return;
|
if (!this.services.appLifecycle.isReady()) return;
|
||||||
|
|
||||||
if (this.isLastHidden && !this.hasFocus) {
|
if (this.isLastHidden && !this.hasFocus) {
|
||||||
// NO OP while non-focused after made hidden;
|
// NO OP while non-focused after made hidden;
|
||||||
@@ -126,22 +127,22 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
|
|||||||
}
|
}
|
||||||
this.isLastHidden = isHidden;
|
this.isLastHidden = isHidden;
|
||||||
|
|
||||||
await this.core.$everyCommitPendingFileEvent();
|
await this.services.fileProcessing.commitPendingFileEvents();
|
||||||
|
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
await this.core.$everyBeforeSuspendProcess();
|
await this.services.appLifecycle.onSuspending();
|
||||||
} else {
|
} else {
|
||||||
// suspend all temporary.
|
// suspend all temporary.
|
||||||
if (this.core.$$isSuspended()) return;
|
if (this.services.appLifecycle.isSuspended()) return;
|
||||||
if (!this.hasFocus) return;
|
if (!this.hasFocus) return;
|
||||||
await this.core.$everyOnResumeProcess();
|
await this.services.appLifecycle.onResuming();
|
||||||
await this.core.$everyAfterResumeProcess();
|
await this.services.appLifecycle.onResumed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
watchWorkspaceOpen(file: TFile | null) {
|
watchWorkspaceOpen(file: TFile | null) {
|
||||||
if (this.settings.suspendFileWatching) return;
|
if (this.settings.suspendFileWatching) return;
|
||||||
if (!this.settings.isConfigured) return;
|
if (!this.settings.isConfigured) return;
|
||||||
if (!this.core.$$isReady()) return;
|
if (!this.services.appLifecycle.isReady()) return;
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
scheduleTask("watch-workspace-open", 500, () => fireAndForget(() => this.watchWorkspaceOpenAsync(file)));
|
scheduleTask("watch-workspace-open", 500, () => fireAndForget(() => this.watchWorkspaceOpenAsync(file)));
|
||||||
}
|
}
|
||||||
@@ -149,25 +150,25 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
|
|||||||
async watchWorkspaceOpenAsync(file: TFile) {
|
async watchWorkspaceOpenAsync(file: TFile) {
|
||||||
if (this.settings.suspendFileWatching) return;
|
if (this.settings.suspendFileWatching) return;
|
||||||
if (!this.settings.isConfigured) return;
|
if (!this.settings.isConfigured) return;
|
||||||
if (!this.core.$$isReady()) return;
|
if (!this.services.appLifecycle.isReady()) return;
|
||||||
await this.core.$everyCommitPendingFileEvent();
|
await this.services.fileProcessing.commitPendingFileEvents();
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.settings.syncOnFileOpen && !this.core.$$isSuspended()) {
|
if (this.settings.syncOnFileOpen && !this.services.appLifecycle.isSuspended()) {
|
||||||
await this.core.$$replicateByEvent();
|
await this.services.replication.replicateByEvent();
|
||||||
}
|
}
|
||||||
await this.core.$$queueConflictCheckIfOpen(file.path as FilePathWithPrefix);
|
await this.services.conflict.queueCheckForIfOpen(file.path as FilePathWithPrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
$everyOnLayoutReady(): Promise<boolean> {
|
_everyOnLayoutReady(): Promise<boolean> {
|
||||||
this.swapSaveCommand();
|
this.swapSaveCommand();
|
||||||
this.registerWatchEvents();
|
this.registerWatchEvents();
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$$askReload(message?: string) {
|
private _askReload(message?: string) {
|
||||||
if (this.core.$$isReloadingScheduled()) {
|
if (this.services.appLifecycle.isReloadingScheduled()) {
|
||||||
this._log(`Reloading is already scheduled`, LOG_LEVEL_VERBOSE);
|
this._log(`Reloading is already scheduled`, LOG_LEVEL_VERBOSE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -181,13 +182,13 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
|
|||||||
{ defaultAction: RETRY_LATER }
|
{ defaultAction: RETRY_LATER }
|
||||||
);
|
);
|
||||||
if (ret == RESTART_NOW) {
|
if (ret == RESTART_NOW) {
|
||||||
this._performAppReload();
|
this.__performAppReload();
|
||||||
} else if (ret == RESTART_AFTER_STABLE) {
|
} else if (ret == RESTART_AFTER_STABLE) {
|
||||||
this.core.$$scheduleAppReload();
|
this.services.appLifecycle.scheduleRestart();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
$$scheduleAppReload() {
|
private _scheduleAppReload() {
|
||||||
if (!this.core._totalProcessingCount) {
|
if (!this.core._totalProcessingCount) {
|
||||||
const __tick = reactiveSource(0);
|
const __tick = reactiveSource(0);
|
||||||
this.core._totalProcessingCount = reactive(() => {
|
this.core._totalProcessingCount = reactive(() => {
|
||||||
@@ -224,7 +225,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
|
|||||||
this.core._totalProcessingCount.onChanged((e) => {
|
this.core._totalProcessingCount.onChanged((e) => {
|
||||||
if (e.value == 0) {
|
if (e.value == 0) {
|
||||||
if (stableCheck-- <= 0) {
|
if (stableCheck-- <= 0) {
|
||||||
this._performAppReload();
|
this.__performAppReload();
|
||||||
}
|
}
|
||||||
this._log(
|
this._log(
|
||||||
`Obsidian will be restarted soon! (Within ${stableCheck} seconds)`,
|
`Obsidian will be restarted soon! (Within ${stableCheck} seconds)`,
|
||||||
@@ -237,4 +238,11 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleLayoutReady(this._everyOnLayoutReady.bind(this));
|
||||||
|
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||||
|
services.appLifecycle.handlePerformRestart(this._performRestart.bind(this));
|
||||||
|
services.appLifecycle.handleAskRestart(this._askReload.bind(this));
|
||||||
|
services.appLifecycle.handleScheduleRestart(this._scheduleAppReload.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { fireAndForget } from "octagonal-wheels/promises";
|
import { fireAndForget } from "octagonal-wheels/promises";
|
||||||
import { addIcon, type Editor, type MarkdownFileInfo, type MarkdownView } from "../../deps.ts";
|
import { addIcon, type Editor, type MarkdownFileInfo, type MarkdownView } from "../../deps.ts";
|
||||||
import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "../../lib/src/common/types.ts";
|
import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "../../lib/src/common/types.ts";
|
||||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsidianModule {
|
export class ModuleObsidianMenu extends AbstractObsidianModule {
|
||||||
$everyOnloadStart(): Promise<boolean> {
|
_everyOnloadStart(): Promise<boolean> {
|
||||||
// UI
|
// UI
|
||||||
addIcon(
|
addIcon(
|
||||||
"replicate",
|
"replicate",
|
||||||
@@ -18,21 +19,21 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.addRibbonIcon("replicate", $msg("moduleObsidianMenu.replicate"), async () => {
|
this.addRibbonIcon("replicate", $msg("moduleObsidianMenu.replicate"), async () => {
|
||||||
await this.core.$$replicate(true);
|
await this.services.replication.replicate(true);
|
||||||
}).addClass("livesync-ribbon-replicate");
|
}).addClass("livesync-ribbon-replicate");
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-replicate",
|
id: "livesync-replicate",
|
||||||
name: "Replicate now",
|
name: "Replicate now",
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-dump",
|
id: "livesync-dump",
|
||||||
name: "Dump information of this doc ",
|
name: "Dump information of this doc ",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const file = this.core.$$getActiveFilePath();
|
const file = this.services.vault.getActiveFilePath();
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
fireAndForget(() => this.localDatabase.getDBEntry(file, {}, true, false));
|
fireAndForget(() => this.localDatabase.getDBEntry(file, {}, true, false));
|
||||||
},
|
},
|
||||||
@@ -43,7 +44,7 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
|
|||||||
editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
|
editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
|
||||||
const file = view.file;
|
const file = view.file;
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
void this.core.$$queueConflictCheckIfOpen(file.path as FilePathWithPrefix);
|
void this.services.conflict.queueCheckForIfOpen(file.path as FilePathWithPrefix);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,23 +59,23 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
|
|||||||
this.settings.liveSync = true;
|
this.settings.liveSync = true;
|
||||||
this._log("LiveSync Enabled.", LOG_LEVEL_NOTICE);
|
this._log("LiveSync Enabled.", LOG_LEVEL_NOTICE);
|
||||||
}
|
}
|
||||||
await this.core.$$realizeSettingSyncMode();
|
await this.services.setting.realiseSetting();
|
||||||
await this.core.$$saveSettingData();
|
await this.services.setting.saveSettingData();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-suspendall",
|
id: "livesync-suspendall",
|
||||||
name: "Toggle All Sync.",
|
name: "Toggle All Sync.",
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
if (this.core.$$isSuspended()) {
|
if (this.services.appLifecycle.isSuspended()) {
|
||||||
this.core.$$setSuspended(false);
|
this.services.appLifecycle.setSuspended(false);
|
||||||
this._log("Self-hosted LiveSync resumed", LOG_LEVEL_NOTICE);
|
this._log("Self-hosted LiveSync resumed", LOG_LEVEL_NOTICE);
|
||||||
} else {
|
} else {
|
||||||
this.core.$$setSuspended(true);
|
this.services.appLifecycle.setSuspended(true);
|
||||||
this._log("Self-hosted LiveSync suspended", LOG_LEVEL_NOTICE);
|
this._log("Self-hosted LiveSync suspended", LOG_LEVEL_NOTICE);
|
||||||
}
|
}
|
||||||
await this.core.$$realizeSettingSyncMode();
|
await this.services.setting.realiseSetting();
|
||||||
await this.core.$$saveSettingData();
|
await this.services.setting.saveSettingData();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
|
|||||||
id: "livesync-scan-files",
|
id: "livesync-scan-files",
|
||||||
name: "Scan storage and database again",
|
name: "Scan storage and database again",
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
await this.core.$$performFullScan(true);
|
await this.services.vault.scanVault(true);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
|
|||||||
id: "livesync-runbatch",
|
id: "livesync-runbatch",
|
||||||
name: "Run pended batch processes",
|
name: "Run pended batch processes",
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
await this.core.$everyCommitPendingFileEvent();
|
await this.services.fileProcessing.commitPendingFileEvents();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,12 +105,15 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
|
|||||||
});
|
});
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
$everyOnload(): Promise<boolean> {
|
private __onWorkspaceReady() {
|
||||||
this.app.workspace.onLayoutReady(this.core.$$onLiveSyncReady.bind(this.core));
|
void this.services.appLifecycle.onReady();
|
||||||
|
}
|
||||||
|
private _everyOnload(): Promise<boolean> {
|
||||||
|
this.app.workspace.onLayoutReady(this.__onWorkspaceReady.bind(this));
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$showView(viewType: string) {
|
private async _showView(viewType: string) {
|
||||||
const leaves = this.app.workspace.getLeavesOfType(viewType);
|
const leaves = this.app.workspace.getLeavesOfType(viewType);
|
||||||
if (leaves.length == 0) {
|
if (leaves.length == 0) {
|
||||||
await this.app.workspace.getLeaf(true).setViewState({
|
await this.app.workspace.getLeaf(true).setViewState({
|
||||||
@@ -126,4 +130,9 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
|
|||||||
await this.app.workspace.revealLeaf(leaves[0]);
|
await this.app.workspace.revealLeaf(leaves[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||||
|
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
|
||||||
|
services.API.handleShowWindow(this._showView.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
|
|
||||||
export class ModuleExtraSyncObsidian extends AbstractObsidianModule implements IObsidianModule {
|
export class ModuleExtraSyncObsidian extends AbstractObsidianModule {
|
||||||
deviceAndVaultName: string = "";
|
deviceAndVaultName: string = "";
|
||||||
|
|
||||||
$$getDeviceAndVaultName(): string {
|
_getDeviceAndVaultName(): string {
|
||||||
return this.deviceAndVaultName;
|
return this.deviceAndVaultName;
|
||||||
}
|
}
|
||||||
$$setDeviceAndVaultName(name: string): void {
|
_setDeviceAndVaultName(name: string): void {
|
||||||
this.deviceAndVaultName = name;
|
this.deviceAndVaultName = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.setting.handleGetDeviceAndVaultName(this._getDeviceAndVaultName.bind(this));
|
||||||
|
services.setting.handleSetDeviceAndVaultName(this._setDeviceAndVaultName.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { delay, fireAndForget } from "octagonal-wheels/promises";
|
import { delay, fireAndForget } from "octagonal-wheels/promises";
|
||||||
import { __onMissingTranslation } from "../../lib/src/common/i18n";
|
import { __onMissingTranslation } from "../../lib/src/common/i18n";
|
||||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
import { LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||||
import { eventHub } from "../../common/events";
|
import { eventHub } from "../../common/events";
|
||||||
import { enableTestFunction } from "./devUtil/testUtils.ts";
|
import { enableTestFunction } from "./devUtil/testUtils.ts";
|
||||||
import { TestPaneView, VIEW_TYPE_TEST } from "./devUtil/TestPaneView.ts";
|
import { TestPaneView, VIEW_TYPE_TEST } from "./devUtil/TestPaneView.ts";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import type { FilePathWithPrefix } from "../../lib/src/common/types.ts";
|
import type { FilePathWithPrefix } from "../../lib/src/common/types.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
export class ModuleDev extends AbstractObsidianModule implements IObsidianModule {
|
export class ModuleDev extends AbstractObsidianModule {
|
||||||
$everyOnloadStart(): Promise<boolean> {
|
_everyOnloadStart(): Promise<boolean> {
|
||||||
__onMissingTranslation(() => {});
|
__onMissingTranslation(() => {});
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,7 @@ export class ModuleDev extends AbstractObsidianModule implements IObsidianModule
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$everyOnloadAfterLoadSettings(): Promise<boolean> {
|
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
|
||||||
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
||||||
this.onMissingTranslation = this.onMissingTranslation.bind(this);
|
this.onMissingTranslation = this.onMissingTranslation.bind(this);
|
||||||
__onMissingTranslation((key) => {
|
__onMissingTranslation((key) => {
|
||||||
@@ -92,12 +93,12 @@ export class ModuleDev extends AbstractObsidianModule implements IObsidianModule
|
|||||||
id: "view-test",
|
id: "view-test",
|
||||||
name: "Open Test dialogue",
|
name: "Open Test dialogue",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
void this.core.$$showView(VIEW_TYPE_TEST);
|
void this.services.API.showWindow(VIEW_TYPE_TEST);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
async $everyOnLayoutReady(): Promise<boolean> {
|
async _everyOnLayoutReady(): Promise<boolean> {
|
||||||
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
||||||
// if (await this.core.storageAccess.isExistsIncludeHidden("_SHOWDIALOGAUTO.md")) {
|
// if (await this.core.storageAccess.isExistsIncludeHidden("_SHOWDIALOGAUTO.md")) {
|
||||||
// void this.core.$$showView(VIEW_TYPE_TEST);
|
// void this.core.$$showView(VIEW_TYPE_TEST);
|
||||||
@@ -121,7 +122,7 @@ export class ModuleDev extends AbstractObsidianModule implements IObsidianModule
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (w) {
|
if (w) {
|
||||||
const id = await this.core.$$path2id(filename as FilePathWithPrefix);
|
const id = await this.services.path.path2id(filename as FilePathWithPrefix);
|
||||||
const f = await this.core.localDatabase.getRaw(id);
|
const f = await this.core.localDatabase.getRaw(id);
|
||||||
console.log(f);
|
console.log(f);
|
||||||
console.log(f._rev);
|
console.log(f._rev);
|
||||||
@@ -139,14 +140,14 @@ export class ModuleDev extends AbstractObsidianModule implements IObsidianModule
|
|||||||
testResults = writable<[boolean, string, string][]>([]);
|
testResults = writable<[boolean, string, string][]>([]);
|
||||||
// testResults: string[] = [];
|
// testResults: string[] = [];
|
||||||
|
|
||||||
$$addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void {
|
private _addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void {
|
||||||
const logLine = `${name}: ${key} ${summary ?? ""}`;
|
const logLine = `${name}: ${key} ${summary ?? ""}`;
|
||||||
this.testResults.update((results) => {
|
this.testResults.update((results) => {
|
||||||
results.push([result, logLine, message ?? ""]);
|
results.push([result, logLine, message ?? ""]);
|
||||||
return results;
|
return results;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
$everyModuleTest(): Promise<boolean> {
|
private _everyModuleTest(): Promise<boolean> {
|
||||||
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
||||||
// this.core.$$addTestResult("DevModule", "Test", true);
|
// this.core.$$addTestResult("DevModule", "Test", true);
|
||||||
// return Promise.resolve(true);
|
// return Promise.resolve(true);
|
||||||
@@ -155,4 +156,11 @@ export class ModuleDev extends AbstractObsidianModule implements IObsidianModule
|
|||||||
// this.addTestResult("Test of test3", true);
|
// this.addTestResult("Test of test3", true);
|
||||||
return this.testDone();
|
return this.testDone();
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleLayoutReady(this._everyOnLayoutReady.bind(this));
|
||||||
|
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||||
|
services.appLifecycle.handleOnSettingLoaded(this._everyOnloadAfterLoadSettings.bind(this));
|
||||||
|
services.test.handleTest(this._everyModuleTest.bind(this));
|
||||||
|
services.test.handleAddTestResult(this._addTestResult.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { delay } from "octagonal-wheels/promises";
|
import { delay } from "octagonal-wheels/promises";
|
||||||
import { LOG_LEVEL_NOTICE, REMOTE_MINIO, type FilePathWithPrefix } from "src/lib/src/common/types";
|
import { LOG_LEVEL_NOTICE, REMOTE_MINIO, type FilePathWithPrefix } from "src/lib/src/common/types";
|
||||||
import { shareRunningResult } from "octagonal-wheels/concurrency/lock";
|
import { shareRunningResult } from "octagonal-wheels/concurrency/lock";
|
||||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule";
|
||||||
|
|
||||||
export class ModuleIntegratedTest extends AbstractObsidianModule implements IObsidianModule {
|
export class ModuleIntegratedTest extends AbstractObsidianModule {
|
||||||
async waitFor(proc: () => Promise<boolean>, timeout = 10000): Promise<boolean> {
|
async waitFor(proc: () => Promise<boolean>, timeout = 10000): Promise<boolean> {
|
||||||
await delay(100);
|
await delay(100);
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
@@ -45,7 +45,7 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async _orDie(key: string, proc: () => Promise<boolean>): Promise<true> | never {
|
async __orDie(key: string, proc: () => Promise<boolean>): Promise<true> | never {
|
||||||
if (!(await this._test(key, proc))) {
|
if (!(await this._test(key, proc))) {
|
||||||
throw new Error(`${key}`);
|
throw new Error(`${key}`);
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
|
|||||||
tryReplicate() {
|
tryReplicate() {
|
||||||
if (!this.settings.liveSync) {
|
if (!this.settings.liveSync) {
|
||||||
return shareRunningResult("replicate-test", async () => {
|
return shareRunningResult("replicate-test", async () => {
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,13 +64,13 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
|
|||||||
}
|
}
|
||||||
return await this.core.storageAccess.readHiddenFileText(file);
|
return await this.core.storageAccess.readHiddenFileText(file);
|
||||||
}
|
}
|
||||||
async _proceed(no: number, title: string): Promise<boolean> {
|
async __proceed(no: number, title: string): Promise<boolean> {
|
||||||
const stepFile = "_STEP.md" as FilePathWithPrefix;
|
const stepFile = "_STEP.md" as FilePathWithPrefix;
|
||||||
const stepAckFile = "_STEP_ACK.md" as FilePathWithPrefix;
|
const stepAckFile = "_STEP_ACK.md" as FilePathWithPrefix;
|
||||||
const stepContent = `Step ${no}`;
|
const stepContent = `Step ${no}`;
|
||||||
await this.core.$anyResolveConflictByNewest(stepFile);
|
await this.services.conflict.resolveByNewest(stepFile);
|
||||||
await this.core.storageAccess.writeFileAuto(stepFile, stepContent);
|
await this.core.storageAccess.writeFileAuto(stepFile, stepContent);
|
||||||
await this._orDie(`Wait for acknowledge ${no}`, async () => {
|
await this.__orDie(`Wait for acknowledge ${no}`, async () => {
|
||||||
if (
|
if (
|
||||||
!(await this.waitWithReplicating(async () => {
|
!(await this.waitWithReplicating(async () => {
|
||||||
return await this.storageContentIsEqual(stepAckFile, stepContent);
|
return await this.storageContentIsEqual(stepAckFile, stepContent);
|
||||||
@@ -81,13 +81,13 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
|
|||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async _join(no: number, title: string): Promise<boolean> {
|
async __join(no: number, title: string): Promise<boolean> {
|
||||||
const stepFile = "_STEP.md" as FilePathWithPrefix;
|
const stepFile = "_STEP.md" as FilePathWithPrefix;
|
||||||
const stepAckFile = "_STEP_ACK.md" as FilePathWithPrefix;
|
const stepAckFile = "_STEP_ACK.md" as FilePathWithPrefix;
|
||||||
// const otherStepFile = `_STEP_${isLeader ? "R" : "L"}.md` as FilePathWithPrefix;
|
// const otherStepFile = `_STEP_${isLeader ? "R" : "L"}.md` as FilePathWithPrefix;
|
||||||
const stepContent = `Step ${no}`;
|
const stepContent = `Step ${no}`;
|
||||||
|
|
||||||
await this._orDie(`Wait for step ${no} (${title})`, async () => {
|
await this.__orDie(`Wait for step ${no} (${title})`, async () => {
|
||||||
if (
|
if (
|
||||||
!(await this.waitWithReplicating(async () => {
|
!(await this.waitWithReplicating(async () => {
|
||||||
return await this.storageContentIsEqual(stepFile, stepContent);
|
return await this.storageContentIsEqual(stepFile, stepContent);
|
||||||
@@ -96,7 +96,7 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
|
|||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
await this.core.$anyResolveConflictByNewest(stepAckFile);
|
await this.services.conflict.resolveByNewest(stepAckFile);
|
||||||
await this.core.storageAccess.writeFileAuto(stepAckFile, stepContent);
|
await this.core.storageAccess.writeFileAuto(stepAckFile, stepContent);
|
||||||
await this.tryReplicate();
|
await this.tryReplicate();
|
||||||
return true;
|
return true;
|
||||||
@@ -116,16 +116,16 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
|
|||||||
check: () => Promise<boolean>;
|
check: () => Promise<boolean>;
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
if (isGameChanger) {
|
if (isGameChanger) {
|
||||||
await this._proceed(step, title);
|
await this.__proceed(step, title);
|
||||||
try {
|
try {
|
||||||
await proc();
|
await proc();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._log(`Error: ${e}`);
|
this._log(`Error: ${e}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return await this._orDie(`Step ${step} - ${title}`, async () => await this.waitWithReplicating(check));
|
return await this.__orDie(`Step ${step} - ${title}`, async () => await this.waitWithReplicating(check));
|
||||||
} else {
|
} else {
|
||||||
return await this._join(step, title);
|
return await this.__join(step, title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// // see scenario.md
|
// // see scenario.md
|
||||||
@@ -151,7 +151,7 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
|
|||||||
`Test as ${isLeader ? "Leader" : "Receiver"} command file ${testCommandFile}`
|
`Test as ${isLeader ? "Leader" : "Receiver"} command file ${testCommandFile}`
|
||||||
);
|
);
|
||||||
if (isLeader) {
|
if (isLeader) {
|
||||||
await this._proceed(0, "start");
|
await this.__proceed(0, "start");
|
||||||
}
|
}
|
||||||
await this.tryReplicate();
|
await this.tryReplicate();
|
||||||
|
|
||||||
@@ -424,9 +424,9 @@ Line4:D`;
|
|||||||
await this._test("basic", async () => await this.nonLiveTestRunner(isLeader, (t, l) => this.testBasic(t, l)));
|
await this._test("basic", async () => await this.nonLiveTestRunner(isLeader, (t, l) => this.testBasic(t, l)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async $everyModuleTestMultiDevice(): Promise<boolean> {
|
async _everyModuleTestMultiDevice(): Promise<boolean> {
|
||||||
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
||||||
const isLeader = this.core.$$vaultName().indexOf("recv") === -1;
|
const isLeader = this.core.services.vault.vaultName().indexOf("recv") === -1;
|
||||||
this.addTestResult("-------", true, `Test as ${isLeader ? "Leader" : "Receiver"}`);
|
this.addTestResult("-------", true, `Test as ${isLeader ? "Leader" : "Receiver"}`);
|
||||||
try {
|
try {
|
||||||
this._log(`Starting Test`);
|
this._log(`Starting Test`);
|
||||||
@@ -440,4 +440,7 @@ Line4:D`;
|
|||||||
|
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: typeof this.core, services: typeof core.services): void {
|
||||||
|
services.test.handleTestMultiDevice(this._everyModuleTestMultiDevice.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { delay } from "octagonal-wheels/promises";
|
import { delay } from "octagonal-wheels/promises";
|
||||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||||
import { eventHub } from "../../common/events";
|
import { eventHub } from "../../common/events";
|
||||||
import { getWebCrypto } from "../../lib/src/mods.ts";
|
import { getWebCrypto } from "../../lib/src/mods.ts";
|
||||||
@@ -8,6 +8,7 @@ import { parseYaml, requestUrl, stringifyYaml } from "obsidian";
|
|||||||
import type { FilePath } from "../../lib/src/common/types.ts";
|
import type { FilePath } from "../../lib/src/common/types.ts";
|
||||||
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
||||||
import { getFileRegExp } from "../../lib/src/common/utils.ts";
|
import { getFileRegExp } from "../../lib/src/common/utils.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface LSEvents {
|
interface LSEvents {
|
||||||
@@ -15,12 +16,15 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ModuleReplicateTest extends AbstractObsidianModule implements IObsidianModule {
|
export class ModuleReplicateTest extends AbstractObsidianModule {
|
||||||
testRootPath = "_test/";
|
testRootPath = "_test/";
|
||||||
testInfoPath = "_testinfo/";
|
testInfoPath = "_testinfo/";
|
||||||
|
|
||||||
get isLeader() {
|
get isLeader() {
|
||||||
return this.core.$$getVaultName().indexOf("dev") >= 0 && this.core.$$vaultName().indexOf("recv") < 0;
|
return (
|
||||||
|
this.services.vault.getVaultName().indexOf("dev") >= 0 &&
|
||||||
|
this.services.vault.vaultName().indexOf("recv") < 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get nameByKind() {
|
get nameByKind() {
|
||||||
@@ -52,24 +56,24 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
|
|||||||
async dumpList() {
|
async dumpList() {
|
||||||
if (this.settings.syncInternalFiles) {
|
if (this.settings.syncInternalFiles) {
|
||||||
this._log("Write file list (Include Hidden)");
|
this._log("Write file list (Include Hidden)");
|
||||||
await this._dumpFileListIncludeHidden("files.md");
|
await this.__dumpFileListIncludeHidden("files.md");
|
||||||
} else {
|
} else {
|
||||||
this._log("Write file list");
|
this._log("Write file list");
|
||||||
await this._dumpFileList("files.md");
|
await this.__dumpFileList("files.md");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async $everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
|
async _everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
|
||||||
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
||||||
await this.dumpList();
|
await this.dumpList();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
$everyOnloadAfterLoadSettings(): Promise<boolean> {
|
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
|
||||||
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "dump-file-structure-normal",
|
id: "dump-file-structure-normal",
|
||||||
name: `Dump Structure (Normal)`,
|
name: `Dump Structure (Normal)`,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
void this._dumpFileList("files.md").finally(() => {
|
void this.__dumpFileList("files.md").finally(() => {
|
||||||
void this.refreshSyncStatus();
|
void this.refreshSyncStatus();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -79,7 +83,7 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
|
|||||||
name: "Dump Structure (Include Hidden)",
|
name: "Dump Structure (Include Hidden)",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const d = "files.md";
|
const d = "files.md";
|
||||||
void this._dumpFileListIncludeHidden(d);
|
void this.__dumpFileListIncludeHidden(d);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
@@ -160,7 +164,7 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _dumpFileList(outFile?: string) {
|
async __dumpFileList(outFile?: string) {
|
||||||
if (!this.core || !this.core.storageAccess) {
|
if (!this.core || !this.core.storageAccess) {
|
||||||
this._log("No storage access", LOG_LEVEL_INFO);
|
this._log("No storage access", LOG_LEVEL_INFO);
|
||||||
return;
|
return;
|
||||||
@@ -169,7 +173,7 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
|
|||||||
const out = [] as any[];
|
const out = [] as any[];
|
||||||
const webcrypto = await getWebCrypto();
|
const webcrypto = await getWebCrypto();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (!(await this.core.$$isTargetFile(file.path))) {
|
if (!(await this.services.vault.isTargetFile(file.path))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (file.path.startsWith(this.testInfoPath)) continue;
|
if (file.path.startsWith(this.testInfoPath)) continue;
|
||||||
@@ -200,7 +204,7 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
|
|||||||
this._log(`Dumped ${out.length} files`, LOG_LEVEL_INFO);
|
this._log(`Dumped ${out.length} files`, LOG_LEVEL_INFO);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _dumpFileListIncludeHidden(outFile?: string) {
|
async __dumpFileListIncludeHidden(outFile?: string) {
|
||||||
const ignorePatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesIgnorePatterns");
|
const ignorePatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesIgnorePatterns");
|
||||||
const targetPatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesTargetPatterns");
|
const targetPatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesTargetPatterns");
|
||||||
const out = [] as any[];
|
const out = [] as any[];
|
||||||
@@ -316,7 +320,7 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
|
|||||||
}
|
}
|
||||||
|
|
||||||
async testConflictedManually1() {
|
async testConflictedManually1() {
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
|
|
||||||
const commonFile = `Resolve!
|
const commonFile = `Resolve!
|
||||||
*****, the amazing chocolatier!!`;
|
*****, the amazing chocolatier!!`;
|
||||||
@@ -325,8 +329,8 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
|
|||||||
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", commonFile);
|
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", commonFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
if (
|
if (
|
||||||
(await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 1?", {
|
(await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 1?", {
|
||||||
timeout: 30,
|
timeout: 30,
|
||||||
@@ -356,12 +360,12 @@ Charlie Bucket, Charlie Bucket, the amazing chocolatier!!`;
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!(await this.waitFor(async () => {
|
!(await this.waitFor(async () => {
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
return (
|
return (
|
||||||
(await this.__assertStorageContent(
|
(await this.__assertStorageContent(
|
||||||
(this.testRootPath + "wonka.md") as FilePath,
|
(this.testRootPath + "wonka.md") as FilePath,
|
||||||
@@ -379,7 +383,7 @@ Charlie Bucket, Charlie Bucket, the amazing chocolatier!!`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
async testConflictedManually2() {
|
async testConflictedManually2() {
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
|
|
||||||
const commonFile = `Resolve To concatenate
|
const commonFile = `Resolve To concatenate
|
||||||
ABCDEFG`;
|
ABCDEFG`;
|
||||||
@@ -388,8 +392,8 @@ ABCDEFG`;
|
|||||||
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", commonFile);
|
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", commonFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
if (
|
if (
|
||||||
(await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 2?", {
|
(await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 2?", {
|
||||||
timeout: 30,
|
timeout: 30,
|
||||||
@@ -420,12 +424,12 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ`;
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!(await this.waitFor(async () => {
|
!(await this.waitFor(async () => {
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
return (
|
return (
|
||||||
(await this.__assertStorageContent(
|
(await this.__assertStorageContent(
|
||||||
(this.testRootPath + "concat.md") as FilePath,
|
(this.testRootPath + "concat.md") as FilePath,
|
||||||
@@ -457,8 +461,8 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ`;
|
|||||||
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", baseDoc);
|
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", baseDoc);
|
||||||
}
|
}
|
||||||
await delay(100);
|
await delay(100);
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(await this.core.confirm.askYesNoDialog("Ready to test conflict?", {
|
(await this.core.confirm.askYesNoDialog("Ready to test conflict?", {
|
||||||
@@ -487,8 +491,8 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ`;
|
|||||||
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", mod2Doc);
|
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", mod2Doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
if (
|
if (
|
||||||
(await this.core.confirm.askYesNoDialog("Ready to check result?", { timeout: 30, defaultOption: "Yes" })) ==
|
(await this.core.confirm.askYesNoDialog("Ready to check result?", { timeout: 30, defaultOption: "Yes" })) ==
|
||||||
@@ -496,8 +500,8 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ`;
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
const mergedDoc = `Tasks!
|
const mergedDoc = `Tasks!
|
||||||
- [ ] Task 1
|
- [ ] Task 1
|
||||||
- [v] Task 2
|
- [v] Task 2
|
||||||
@@ -511,7 +515,7 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ`;
|
|||||||
this._log("Before testing conflicted files, resolve all once", LOG_LEVEL_NOTICE);
|
this._log("Before testing conflicted files, resolve all once", LOG_LEVEL_NOTICE);
|
||||||
await this.core.rebuilder.resolveAllConflictedFilesByNewerOnes();
|
await this.core.rebuilder.resolveAllConflictedFilesByNewerOnes();
|
||||||
await this.core.rebuilder.resolveAllConflictedFilesByNewerOnes();
|
await this.core.rebuilder.resolveAllConflictedFilesByNewerOnes();
|
||||||
await this.core.$$replicate();
|
await this.services.replication.replicate();
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
if (!(await this.testConflictAutomatic())) {
|
if (!(await this.testConflictAutomatic())) {
|
||||||
this._log("Conflict resolution (Auto) failed", LOG_LEVEL_NOTICE);
|
this._log("Conflict resolution (Auto) failed", LOG_LEVEL_NOTICE);
|
||||||
@@ -569,11 +573,16 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ`;
|
|||||||
// return results;
|
// return results;
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
async $everyModuleTestMultiDevice(): Promise<boolean> {
|
private async _everyModuleTestMultiDevice(): Promise<boolean> {
|
||||||
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
||||||
// this.core.$$addTestResult("DevModule", "Test", true);
|
// this.core.$$addTestResult("DevModule", "Test", true);
|
||||||
// return Promise.resolve(true);
|
// return Promise.resolve(true);
|
||||||
await this._test("Conflict resolution", async () => await this.checkConflictResolution());
|
await this._test("Conflict resolution", async () => await this.checkConflictResolution());
|
||||||
return this.testDone();
|
return this.testDone();
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleOnSettingLoaded(this._everyOnloadAfterLoadSettings.bind(this));
|
||||||
|
services.replication.handleBeforeReplicate(this._everyBeforeReplicate.bind(this));
|
||||||
|
services.test.handleTestMultiDevice(this._everyModuleTestMultiDevice.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,14 +57,14 @@
|
|||||||
function moduleMultiDeviceTest() {
|
function moduleMultiDeviceTest() {
|
||||||
if (moduleTesting) return;
|
if (moduleTesting) return;
|
||||||
moduleTesting = true;
|
moduleTesting = true;
|
||||||
plugin.$everyModuleTestMultiDevice().finally(() => {
|
plugin.services.test.testMultiDevice().finally(() => {
|
||||||
moduleTesting = false;
|
moduleTesting = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function moduleSingleDeviceTest() {
|
function moduleSingleDeviceTest() {
|
||||||
if (moduleTesting) return;
|
if (moduleTesting) return;
|
||||||
moduleTesting = true;
|
moduleTesting = true;
|
||||||
plugin.$everyModuleTest().finally(() => {
|
plugin.services.test.test().finally(() => {
|
||||||
moduleTesting = false;
|
moduleTesting = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -72,8 +72,8 @@
|
|||||||
if (moduleTesting) return;
|
if (moduleTesting) return;
|
||||||
moduleTesting = true;
|
moduleTesting = true;
|
||||||
try {
|
try {
|
||||||
await plugin.$everyModuleTest();
|
await plugin.services.test.test();
|
||||||
await plugin.$everyModuleTestMultiDevice();
|
await plugin.services.test.testMultiDevice();
|
||||||
} finally {
|
} finally {
|
||||||
moduleTesting = false;
|
moduleTesting = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { fireAndForget } from "../../../lib/src/common/utils.ts";
|
import { fireAndForget } from "../../../lib/src/common/utils.ts";
|
||||||
import { serialized } from "../../../lib/src/concurrency/lock.ts";
|
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||||
import type ObsidianLiveSyncPlugin from "../../../main.ts";
|
import type ObsidianLiveSyncPlugin from "../../../main.ts";
|
||||||
|
|
||||||
let plugin: ObsidianLiveSyncPlugin;
|
let plugin: ObsidianLiveSyncPlugin;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trench } from "../../../lib/src/memory/memutil.ts";
|
import { Trench } from "octagonal-wheels/memory/memutil";
|
||||||
import type ObsidianLiveSyncPlugin from "../../../main.ts";
|
import type ObsidianLiveSyncPlugin from "../../../main.ts";
|
||||||
type MeasureResult = [times: number, spent: number];
|
type MeasureResult = [times: number, spent: number];
|
||||||
type NamedMeasureResult = [name: string, result: MeasureResult];
|
type NamedMeasureResult = [name: string, result: MeasureResult];
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ function readDocument(w: LoadedEntry) {
|
|||||||
}
|
}
|
||||||
export class DocumentHistoryModal extends Modal {
|
export class DocumentHistoryModal extends Modal {
|
||||||
plugin: ObsidianLiveSyncPlugin;
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
|
get services() {
|
||||||
|
return this.plugin.services;
|
||||||
|
}
|
||||||
range!: HTMLInputElement;
|
range!: HTMLInputElement;
|
||||||
contentView!: HTMLDivElement;
|
contentView!: HTMLDivElement;
|
||||||
info!: HTMLDivElement;
|
info!: HTMLDivElement;
|
||||||
@@ -74,7 +77,7 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
this.id = id;
|
this.id = id;
|
||||||
this.initialRev = revision;
|
this.initialRev = revision;
|
||||||
if (!file && id) {
|
if (!file && id) {
|
||||||
this.file = this.plugin.$$id2path(id);
|
this.file = this.services.path.id2path(id);
|
||||||
}
|
}
|
||||||
if (localStorage.getItem("ols-history-highlightdiff") == "1") {
|
if (localStorage.getItem("ols-history-highlightdiff") == "1") {
|
||||||
this.showDiff = true;
|
this.showDiff = true;
|
||||||
@@ -83,7 +86,7 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
|
|
||||||
async loadFile(initialRev?: string) {
|
async loadFile(initialRev?: string) {
|
||||||
if (!this.id) {
|
if (!this.id) {
|
||||||
this.id = await this.plugin.$$path2id(this.file);
|
this.id = await this.services.path.path2id(this.file);
|
||||||
}
|
}
|
||||||
const db = this.plugin.localDatabase;
|
const db = this.plugin.localDatabase;
|
||||||
try {
|
try {
|
||||||
@@ -126,7 +129,7 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
}
|
}
|
||||||
this.BlobURLs.delete(key);
|
this.BlobURLs.delete(key);
|
||||||
}
|
}
|
||||||
generateBlobURL(key: string, data: Uint8Array) {
|
generateBlobURL(key: string, data: Uint8Array<ArrayBuffer>) {
|
||||||
this.revokeURL(key);
|
this.revokeURL(key);
|
||||||
const v = URL.createObjectURL(new Blob([data], { endings: "transparent", type: "application/octet-stream" }));
|
const v = URL.createObjectURL(new Blob([data], { endings: "transparent", type: "application/octet-stream" }));
|
||||||
this.BlobURLs.set(key, v);
|
this.BlobURLs.set(key, v);
|
||||||
@@ -175,7 +178,10 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
result = result.replace(/\n/g, "<br>");
|
result = result.replace(/\n/g, "<br>");
|
||||||
} else if (isImage(this.file)) {
|
} else if (isImage(this.file)) {
|
||||||
const src = this.generateBlobURL("base", w1data);
|
const src = this.generateBlobURL("base", w1data);
|
||||||
const overlay = this.generateBlobURL("overlay", readDocument(w2) as Uint8Array);
|
const overlay = this.generateBlobURL(
|
||||||
|
"overlay",
|
||||||
|
readDocument(w2) as Uint8Array<ArrayBuffer>
|
||||||
|
);
|
||||||
result = `<div class='ls-imgdiff-wrap'>
|
result = `<div class='ls-imgdiff-wrap'>
|
||||||
<div class='overlay'>
|
<div class='overlay'>
|
||||||
<img class='img-base' src="${src}">
|
<img class='img-base' src="${src}">
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export class ConflictResolveModal extends Modal {
|
|||||||
title: string = "Conflicting changes";
|
title: string = "Conflicting changes";
|
||||||
|
|
||||||
pluginPickMode: boolean = false;
|
pluginPickMode: boolean = false;
|
||||||
localName: string = "Keep A";
|
localName: string = "Base";
|
||||||
remoteName: string = "Keep B";
|
remoteName: string = "Conflicted";
|
||||||
offEvent?: ReturnType<typeof eventHub.onEvent>;
|
offEvent?: ReturnType<typeof eventHub.onEvent>;
|
||||||
|
|
||||||
constructor(app: App, filename: string, diff: diff_result, pluginPickMode?: boolean, remoteName?: string) {
|
constructor(app: App, filename: string, diff: diff_result, pluginPickMode?: boolean, remoteName?: string) {
|
||||||
@@ -36,8 +36,8 @@ export class ConflictResolveModal extends Modal {
|
|||||||
this.pluginPickMode = pluginPickMode || false;
|
this.pluginPickMode = pluginPickMode || false;
|
||||||
if (this.pluginPickMode) {
|
if (this.pluginPickMode) {
|
||||||
this.title = "Pick a version";
|
this.title = "Pick a version";
|
||||||
this.remoteName = `Use ${remoteName || "Remote"}`;
|
this.remoteName = `${remoteName || "Remote"}`;
|
||||||
this.localName = "Use Local";
|
this.localName = "Local";
|
||||||
}
|
}
|
||||||
// Send cancel signal for the previous merge dialogue
|
// Send cancel signal for the previous merge dialogue
|
||||||
// if not there, simply be ignored.
|
// if not there, simply be ignored.
|
||||||
@@ -85,20 +85,19 @@ export class ConflictResolveModal extends Modal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diff = diff.replace(/\n/g, "<br>");
|
|
||||||
div.innerHTML = diff;
|
|
||||||
const div2 = contentEl.createDiv("");
|
const div2 = contentEl.createDiv("");
|
||||||
const date1 =
|
const date1 =
|
||||||
new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : "");
|
new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : "");
|
||||||
const date2 =
|
const date2 =
|
||||||
new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : "");
|
new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : "");
|
||||||
div2.innerHTML = `
|
div2.setHTMLUnsafe(`
|
||||||
<span class='deleted'>A:${date1}</span><br /><span class='added'>B:${date2}</span><br>
|
<span class='deleted'><span class='conflict-dev-name'>${this.localName}</span>: ${date1}</span><br>
|
||||||
`;
|
<span class='added'><span class='conflict-dev-name'>${this.remoteName}</span>: ${date2}</span><br>
|
||||||
contentEl.createEl("button", { text: this.localName }, (e) =>
|
`);
|
||||||
|
contentEl.createEl("button", { text: `Use ${this.localName}` }, (e) =>
|
||||||
e.addEventListener("click", () => this.sendResponse(this.result.right.rev))
|
e.addEventListener("click", () => this.sendResponse(this.result.right.rev))
|
||||||
).style.marginRight = "4px";
|
).style.marginRight = "4px";
|
||||||
contentEl.createEl("button", { text: this.remoteName }, (e) =>
|
contentEl.createEl("button", { text: `Use ${this.remoteName}` }, (e) =>
|
||||||
e.addEventListener("click", () => this.sendResponse(this.result.left.rev))
|
e.addEventListener("click", () => this.sendResponse(this.result.left.rev))
|
||||||
).style.marginRight = "4px";
|
).style.marginRight = "4px";
|
||||||
if (!this.pluginPickMode) {
|
if (!this.pluginPickMode) {
|
||||||
@@ -109,6 +108,13 @@ export class ConflictResolveModal extends Modal {
|
|||||||
contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) =>
|
contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) =>
|
||||||
e.addEventListener("click", () => this.sendResponse(CANCELLED))
|
e.addEventListener("click", () => this.sendResponse(CANCELLED))
|
||||||
).style.marginRight = "4px";
|
).style.marginRight = "4px";
|
||||||
|
diff = diff.replace(/\n/g, "<br>");
|
||||||
|
// div.innerHTML = diff;
|
||||||
|
if (diff.length > 100 * 1024) {
|
||||||
|
div.setText("(Too large diff to display)");
|
||||||
|
} else {
|
||||||
|
div.setHTMLUnsafe(diff);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendResponse(result: MergeDialogResult) {
|
sendResponse(result: MergeDialogResult) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import { logMessages } from "../../../lib/src/mock_and_interop/stores";
|
import { logMessages } from "../../../lib/src/mock_and_interop/stores";
|
||||||
import { reactive, type ReactiveInstance } from "../../../lib/src/dataobject/reactive";
|
import { reactive, type ReactiveInstance } from "octagonal-wheels/dataobject/reactive";
|
||||||
import { Logger } from "../../../lib/src/common/logger";
|
import { Logger } from "../../../lib/src/common/logger";
|
||||||
import { $msg as msg, currentLang as lang } from "../../../lib/src/common/i18n.ts";
|
import { $msg as msg, currentLang as lang } from "../../../lib/src/common/i18n.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
import { VIEW_TYPE_GLOBAL_HISTORY, GlobalHistoryView } from "./GlobalHistory/GlobalHistoryView.ts";
|
import { VIEW_TYPE_GLOBAL_HISTORY, GlobalHistoryView } from "./GlobalHistory/GlobalHistoryView.ts";
|
||||||
|
|
||||||
export class ModuleObsidianGlobalHistory extends AbstractObsidianModule implements IObsidianModule {
|
export class ModuleObsidianGlobalHistory extends AbstractObsidianModule {
|
||||||
$everyOnloadStart(): Promise<boolean> {
|
_everyOnloadStart(): Promise<boolean> {
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-global-history",
|
id: "livesync-global-history",
|
||||||
name: "Show vault history",
|
name: "Show vault history",
|
||||||
@@ -17,6 +17,9 @@ export class ModuleObsidianGlobalHistory extends AbstractObsidianModule implemen
|
|||||||
}
|
}
|
||||||
|
|
||||||
showGlobalHistory() {
|
showGlobalHistory() {
|
||||||
void this.core.$$showView(VIEW_TYPE_GLOBAL_HISTORY);
|
void this.services.API.showWindow(VIEW_TYPE_GLOBAL_HISTORY);
|
||||||
|
}
|
||||||
|
onBindFunction(core: typeof this.core, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ import {
|
|||||||
type diff_result,
|
type diff_result,
|
||||||
} from "../../lib/src/common/types.ts";
|
} from "../../lib/src/common/types.ts";
|
||||||
import { ConflictResolveModal } from "./InteractiveConflictResolving/ConflictResolveModal.ts";
|
import { ConflictResolveModal } from "./InteractiveConflictResolving/ConflictResolveModal.ts";
|
||||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
import { displayRev, getPath, getPathWithoutPrefix } from "../../common/utils.ts";
|
import { displayRev, getPath, getPathWithoutPrefix } from "../../common/utils.ts";
|
||||||
import { fireAndForget } from "octagonal-wheels/promises";
|
import { fireAndForget } from "octagonal-wheels/promises";
|
||||||
import { serialized } from "../../lib/src/concurrency/lock.ts";
|
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
export class ModuleInteractiveConflictResolver extends AbstractObsidianModule implements IObsidianModule {
|
export class ModuleInteractiveConflictResolver extends AbstractObsidianModule {
|
||||||
$everyOnloadStart(): Promise<boolean> {
|
_everyOnloadStart(): Promise<boolean> {
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-conflictcheck",
|
id: "livesync-conflictcheck",
|
||||||
name: "Pick a file to resolve conflict",
|
name: "Pick a file to resolve conflict",
|
||||||
@@ -34,7 +35,7 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im
|
|||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $anyResolveConflictByUI(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean> {
|
async _anyResolveConflictByUI(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean> {
|
||||||
// UI for resolving conflicts should one-by-one.
|
// UI for resolving conflicts should one-by-one.
|
||||||
return await serialized(`conflict-resolve-ui`, async () => {
|
return await serialized(`conflict-resolve-ui`, async () => {
|
||||||
this._log("Merge:open conflict dialog", LOG_LEVEL_VERBOSE);
|
this._log("Merge:open conflict dialog", LOG_LEVEL_VERBOSE);
|
||||||
@@ -68,7 +69,7 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im
|
|||||||
}
|
}
|
||||||
// 2. As usual, delete the conflicted revision and if there are no conflicts, write the resolved content to the storage.
|
// 2. As usual, delete the conflicted revision and if there are no conflicts, write the resolved content to the storage.
|
||||||
if (
|
if (
|
||||||
(await this.core.$$resolveConflictByDeletingRev(filename, delRev, "UI Concatenated")) ==
|
(await this.services.conflict.resolveByDeletingRevision(filename, delRev, "UI Concatenated")) ==
|
||||||
MISSING_OR_ERROR
|
MISSING_OR_ERROR
|
||||||
) {
|
) {
|
||||||
this._log(
|
this._log(
|
||||||
@@ -80,7 +81,7 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im
|
|||||||
} else if (typeof toDelete === "string") {
|
} else if (typeof toDelete === "string") {
|
||||||
// Select one of the conflicted revision to delete.
|
// Select one of the conflicted revision to delete.
|
||||||
if (
|
if (
|
||||||
(await this.core.$$resolveConflictByDeletingRev(filename, toDelete, "UI Selected")) ==
|
(await this.services.conflict.resolveByDeletingRevision(filename, toDelete, "UI Selected")) ==
|
||||||
MISSING_OR_ERROR
|
MISSING_OR_ERROR
|
||||||
) {
|
) {
|
||||||
this._log(`Merge: Something went wrong: ${filename}, (${toDelete})`, LOG_LEVEL_NOTICE);
|
this._log(`Merge: Something went wrong: ${filename}, (${toDelete})`, LOG_LEVEL_NOTICE);
|
||||||
@@ -93,11 +94,11 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im
|
|||||||
// In here, some merge has been processed.
|
// In here, some merge has been processed.
|
||||||
// So we have to run replication if configured.
|
// So we have to run replication if configured.
|
||||||
// TODO: Make this is as a event request
|
// TODO: Make this is as a event request
|
||||||
if (this.settings.syncAfterMerge && !this.core.$$isSuspended()) {
|
if (this.settings.syncAfterMerge && !this.services.appLifecycle.isSuspended()) {
|
||||||
await this.core.$$replicateByEvent();
|
await this.services.replication.replicateByEvent();
|
||||||
}
|
}
|
||||||
// And, check it again.
|
// And, check it again.
|
||||||
await this.core.$$queueConflictCheck(filename);
|
await this.services.conflict.queueCheckFor(filename);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -120,14 +121,14 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im
|
|||||||
const target = await this.core.confirm.askSelectString("File to resolve conflict", notesList);
|
const target = await this.core.confirm.askSelectString("File to resolve conflict", notesList);
|
||||||
if (target) {
|
if (target) {
|
||||||
const targetItem = notes.find((e) => e.dispPath == target)!;
|
const targetItem = notes.find((e) => e.dispPath == target)!;
|
||||||
await this.core.$$queueConflictCheck(targetItem.path);
|
await this.services.conflict.queueCheckFor(targetItem.path);
|
||||||
await this.core.$$waitForAllConflictProcessed();
|
await this.services.conflict.ensureAllProcessed();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $allScanStat(): Promise<boolean> {
|
async _allScanStat(): Promise<boolean> {
|
||||||
const notes: { path: string; mtime: number }[] = [];
|
const notes: { path: string; mtime: number }[] = [];
|
||||||
this._log(`Checking conflicted files`, LOG_LEVEL_VERBOSE);
|
this._log(`Checking conflicted files`, LOG_LEVEL_VERBOSE);
|
||||||
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
|
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
|
||||||
@@ -157,4 +158,9 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleOnScanningStartupIssues(this._allScanStat.bind(this));
|
||||||
|
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||||
|
services.conflict.handleResolveByUserInteraction(this._anyResolveConflictByUI.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
} from "../../lib/src/mock_and_interop/stores.ts";
|
} from "../../lib/src/mock_and_interop/stores.ts";
|
||||||
import { eventHub } from "../../lib/src/hub/hub.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 } from "../../common/events.ts";
|
||||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
import { addIcon, normalizePath, Notice } from "../../deps.ts";
|
import { addIcon, normalizePath, Notice } from "../../deps.ts";
|
||||||
import { LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/logger";
|
||||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||||
@@ -28,6 +28,7 @@ import { LogPaneView, VIEW_TYPE_LOG } from "./Log/LogPaneView.ts";
|
|||||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||||
import { P2PLogCollector } from "../../lib/src/replication/trystero/P2PReplicatorCore.ts";
|
import { P2PLogCollector } from "../../lib/src/replication/trystero/P2PReplicatorCore.ts";
|
||||||
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
|
||||||
// This module cannot be a core module because it depends on the Obsidian UI.
|
// This module cannot be a core module because it depends on the Obsidian UI.
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ const recentLogProcessor = new QueueProcessor(
|
|||||||
|
|
||||||
const showDebugLog = false;
|
const showDebugLog = false;
|
||||||
export const MARK_DONE = "\u{2009}\u{2009}";
|
export const MARK_DONE = "\u{2009}\u{2009}";
|
||||||
export class ModuleLog extends AbstractObsidianModule implements IObsidianModule {
|
export class ModuleLog extends AbstractObsidianModule {
|
||||||
registerView = this.plugin.registerView.bind(this.plugin);
|
registerView = this.plugin.registerView.bind(this.plugin);
|
||||||
|
|
||||||
statusBar?: HTMLElement;
|
statusBar?: HTMLElement;
|
||||||
@@ -178,7 +179,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
|
|||||||
});
|
});
|
||||||
|
|
||||||
const statusBarLabels = reactive(() => {
|
const statusBarLabels = reactive(() => {
|
||||||
const scheduleMessage = this.core.$$isReloadingScheduled()
|
const scheduleMessage = this.services.appLifecycle.isReloadingScheduled()
|
||||||
? `WARNING! RESTARTING OBSIDIAN IS SCHEDULED!\n`
|
? `WARNING! RESTARTING OBSIDIAN IS SCHEDULED!\n`
|
||||||
: "";
|
: "";
|
||||||
const { message } = statusLineLabel();
|
const { message } = statusLineLabel();
|
||||||
@@ -199,7 +200,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
|
|||||||
statusBarLabels.onChanged((label) => applyToDisplay(label.value));
|
statusBarLabels.onChanged((label) => applyToDisplay(label.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
$everyOnload(): Promise<boolean> {
|
private _everyOnload(): Promise<boolean> {
|
||||||
eventHub.onEvent(EVENT_LEAF_ACTIVE_CHANGED, () => this.onActiveLeafChange());
|
eventHub.onEvent(EVENT_LEAF_ACTIVE_CHANGED, () => this.onActiveLeafChange());
|
||||||
eventHub.onceEvent(EVENT_LAYOUT_READY, () => this.onActiveLeafChange());
|
eventHub.onceEvent(EVENT_LAYOUT_READY, () => this.onActiveLeafChange());
|
||||||
|
|
||||||
@@ -219,15 +220,15 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
|
|||||||
const thisFile = this.app.workspace.getActiveFile();
|
const thisFile = this.app.workspace.getActiveFile();
|
||||||
if (!thisFile) return "";
|
if (!thisFile) return "";
|
||||||
// Case Sensitivity
|
// Case Sensitivity
|
||||||
if (this.core.$$shouldCheckCaseInsensitive()) {
|
if (this.services.setting.shouldCheckCaseInsensitively()) {
|
||||||
const f = this.core.storageAccess
|
const f = this.core.storageAccess
|
||||||
.getFiles()
|
.getFiles()
|
||||||
.map((e) => e.path)
|
.map((e) => e.path)
|
||||||
.filter((e) => e.toLowerCase() == thisFile.path.toLowerCase());
|
.filter((e) => e.toLowerCase() == thisFile.path.toLowerCase());
|
||||||
if (f.length > 1) return "Not synchronised: There are multiple files with the same name";
|
if (f.length > 1) return "Not synchronised: There are multiple files with the same name";
|
||||||
}
|
}
|
||||||
if (!(await this.core.$$isTargetFile(thisFile.path))) return "Not synchronised: not a target file";
|
if (!(await this.services.vault.isTargetFile(thisFile.path))) return "Not synchronised: not a target file";
|
||||||
if (this.core.$$isFileSizeExceeded(thisFile.stat.size)) return "Not synchronised: File size exceeded";
|
if (this.services.vault.isFileSizeTooLarge(thisFile.stat.size)) return "Not synchronised: File size exceeded";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
async setFileStatus() {
|
async setFileStatus() {
|
||||||
@@ -287,14 +288,14 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$allStartOnUnload(): Promise<boolean> {
|
private _allStartOnUnload(): Promise<boolean> {
|
||||||
if (this.statusDiv) {
|
if (this.statusDiv) {
|
||||||
this.statusDiv.remove();
|
this.statusDiv.remove();
|
||||||
}
|
}
|
||||||
document.querySelectorAll(`.livesync-status`)?.forEach((e) => e.remove());
|
document.querySelectorAll(`.livesync-status`)?.forEach((e) => e.remove());
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
$everyOnloadStart(): Promise<boolean> {
|
_everyOnloadStart(): Promise<boolean> {
|
||||||
addIcon(
|
addIcon(
|
||||||
"view-log",
|
"view-log",
|
||||||
`<g transform="matrix(1.28 0 0 1.28 -131 -411)" fill="currentColor" fill-rule="evenodd">
|
`<g transform="matrix(1.28 0 0 1.28 -131 -411)" fill="currentColor" fill-rule="evenodd">
|
||||||
@@ -303,23 +304,23 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
|
|||||||
</g>`
|
</g>`
|
||||||
);
|
);
|
||||||
this.addRibbonIcon("view-log", $msg("moduleLog.showLog"), () => {
|
this.addRibbonIcon("view-log", $msg("moduleLog.showLog"), () => {
|
||||||
void this.core.$$showView(VIEW_TYPE_LOG);
|
void this.services.API.showWindow(VIEW_TYPE_LOG);
|
||||||
}).addClass("livesync-ribbon-showlog");
|
}).addClass("livesync-ribbon-showlog");
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "view-log",
|
id: "view-log",
|
||||||
name: "Show log",
|
name: "Show log",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
void this.core.$$showView(VIEW_TYPE_LOG);
|
void this.services.API.showWindow(VIEW_TYPE_LOG);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.registerView(VIEW_TYPE_LOG, (leaf) => new LogPaneView(leaf, this.plugin));
|
this.registerView(VIEW_TYPE_LOG, (leaf) => new LogPaneView(leaf, this.plugin));
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
$everyOnloadAfterLoadSettings(): Promise<boolean> {
|
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
|
||||||
logStore
|
logStore
|
||||||
.pipeTo(
|
.pipeTo(
|
||||||
new QueueProcessor((logs) => logs.forEach((e) => this.core.$$addLog(e.message, e.level, e.key)), {
|
new QueueProcessor((logs) => logs.forEach((e) => this.__addLog(e.message, e.level, e.key)), {
|
||||||
suspended: false,
|
suspended: false,
|
||||||
batchSize: 20,
|
batchSize: 20,
|
||||||
concurrentLimit: 1,
|
concurrentLimit: 1,
|
||||||
@@ -366,17 +367,17 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
$$addLog(message: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key = ""): void {
|
__addLog(message: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key = ""): void {
|
||||||
if (level == LOG_LEVEL_DEBUG && !showDebugLog) {
|
if (level == LOG_LEVEL_DEBUG && !showDebugLog) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (level < LOG_LEVEL_INFO && this.settings && this.settings.lessInformationInLog) {
|
if (level <= LOG_LEVEL_INFO && this.settings && this.settings.lessInformationInLog) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL_VERBOSE) {
|
if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL_VERBOSE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const vaultName = this.core.$$getVaultName();
|
const vaultName = this.services.vault.getVaultName();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const timestamp = now.toLocaleString();
|
const timestamp = now.toLocaleString();
|
||||||
const messageContent =
|
const messageContent =
|
||||||
@@ -437,4 +438,10 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||||
|
services.appLifecycle.handleOnSettingLoaded(this._everyOnloadAfterLoadSettings.bind(this));
|
||||||
|
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
|
||||||
|
services.appLifecycle.handleOnBeforeUnload(this._allStartOnUnload.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,18 @@ import { type TFile } from "obsidian";
|
|||||||
import { eventHub } from "../../common/events.ts";
|
import { eventHub } from "../../common/events.ts";
|
||||||
import { EVENT_REQUEST_SHOW_HISTORY } from "../../common/obsidianEvents.ts";
|
import { EVENT_REQUEST_SHOW_HISTORY } from "../../common/obsidianEvents.ts";
|
||||||
import type { FilePathWithPrefix, LoadedEntry, DocumentID } from "../../lib/src/common/types.ts";
|
import type { FilePathWithPrefix, LoadedEntry, DocumentID } from "../../lib/src/common/types.ts";
|
||||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
import { DocumentHistoryModal } from "./DocumentHistory/DocumentHistoryModal.ts";
|
import { DocumentHistoryModal } from "./DocumentHistory/DocumentHistoryModal.ts";
|
||||||
import { getPath } from "../../common/utils.ts";
|
import { getPath } from "../../common/utils.ts";
|
||||||
import { fireAndForget } from "octagonal-wheels/promises";
|
import { fireAndForget } from "octagonal-wheels/promises";
|
||||||
|
|
||||||
export class ModuleObsidianDocumentHistory extends AbstractObsidianModule implements IObsidianModule {
|
export class ModuleObsidianDocumentHistory extends AbstractObsidianModule {
|
||||||
$everyOnloadStart(): Promise<boolean> {
|
_everyOnloadStart(): Promise<boolean> {
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-history",
|
id: "livesync-history",
|
||||||
name: "Show history",
|
name: "Show history",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const file = this.core.$$getActiveFilePath();
|
const file = this.services.vault.getActiveFilePath();
|
||||||
if (file) this.showHistory(file, undefined);
|
if (file) this.showHistory(file, undefined);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -51,4 +51,7 @@ export class ModuleObsidianDocumentHistory extends AbstractObsidianModule implem
|
|||||||
this.showHistory(targetId.path, targetId.id);
|
this.showHistory(targetId.path, targetId.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: typeof this.core, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type IObsidianModule, AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
|
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
|
||||||
import { EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
|
import { EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED, eventHub } from "../../common/events.ts";
|
||||||
import {
|
import {
|
||||||
type BucketSyncSetting,
|
type BucketSyncSetting,
|
||||||
ChunkAlgorithmNames,
|
ChunkAlgorithmNames,
|
||||||
@@ -9,22 +9,23 @@ import {
|
|||||||
DEFAULT_SETTINGS,
|
DEFAULT_SETTINGS,
|
||||||
type ObsidianLiveSyncSettings,
|
type ObsidianLiveSyncSettings,
|
||||||
SALT_OF_PASSPHRASE,
|
SALT_OF_PASSPHRASE,
|
||||||
|
SETTING_KEY_P2P_DEVICE_NAME,
|
||||||
} from "../../lib/src/common/types";
|
} from "../../lib/src/common/types";
|
||||||
import { LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT } from "octagonal-wheels/common/logger";
|
||||||
import { $msg, setLang } from "../../lib/src/common/i18n";
|
import { $msg, setLang } from "../../lib/src/common/i18n.ts";
|
||||||
import { isCloudantURI } from "../../lib/src/pouchdb/utils_couchdb";
|
import { isCloudantURI } from "../../lib/src/pouchdb/utils_couchdb.ts";
|
||||||
import { getLanguage } from "obsidian";
|
import { getLanguage } from "obsidian";
|
||||||
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../../lib/src/common/rosetta.ts";
|
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../../lib/src/common/rosetta.ts";
|
||||||
import { decryptString, encryptString } from "@/lib/src/encryption/stringEncryption.ts";
|
import { decryptString, encryptString } from "@/lib/src/encryption/stringEncryption.ts";
|
||||||
export class ModuleObsidianSettings extends AbstractObsidianModule implements IObsidianModule {
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
async $everyOnLayoutReady(): Promise<boolean> {
|
export class ModuleObsidianSettings extends AbstractObsidianModule {
|
||||||
|
async _everyOnLayoutReady(): Promise<boolean> {
|
||||||
let isChanged = false;
|
let isChanged = false;
|
||||||
if (this.settings.displayLanguage == "") {
|
if (this.settings.displayLanguage == "") {
|
||||||
const obsidianLanguage = getLanguage();
|
const obsidianLanguage = getLanguage();
|
||||||
if (
|
if (
|
||||||
SUPPORTED_I18N_LANGS.indexOf(obsidianLanguage) !== -1 && // Check if the language is supported
|
SUPPORTED_I18N_LANGS.indexOf(obsidianLanguage) !== -1 && // Check if the language is supported
|
||||||
obsidianLanguage != this.settings.displayLanguage && // Check if the language is different from the current setting
|
obsidianLanguage != this.settings.displayLanguage // Check if the language is different from the current setting
|
||||||
this.settings.displayLanguage != ""
|
|
||||||
) {
|
) {
|
||||||
// Check if the current setting is not empty (Means migrated or installed).
|
// Check if the current setting is not empty (Means migrated or installed).
|
||||||
this.settings.displayLanguage = obsidianLanguage as I18N_LANGS;
|
this.settings.displayLanguage = obsidianLanguage as I18N_LANGS;
|
||||||
@@ -33,7 +34,7 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
|||||||
} else if (this.settings.displayLanguage == "") {
|
} else if (this.settings.displayLanguage == "") {
|
||||||
this.settings.displayLanguage = "def";
|
this.settings.displayLanguage = "def";
|
||||||
setLang(this.settings.displayLanguage);
|
setLang(this.settings.displayLanguage);
|
||||||
await this.core.$$saveSettingData();
|
await this.services.setting.saveSettingData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isChanged) {
|
if (isChanged) {
|
||||||
@@ -47,7 +48,7 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
|||||||
this.settings.displayLanguage = "def";
|
this.settings.displayLanguage = "def";
|
||||||
setLang(this.settings.displayLanguage);
|
setLang(this.settings.displayLanguage);
|
||||||
}
|
}
|
||||||
await this.core.$$saveSettingData();
|
await this.services.setting.saveSettingData();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -62,13 +63,13 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
|||||||
return methodFunc();
|
return methodFunc();
|
||||||
}
|
}
|
||||||
|
|
||||||
$$saveDeviceAndVaultName(): void {
|
_saveDeviceAndVaultName(): void {
|
||||||
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.core.$$getVaultName();
|
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.services.vault.getVaultName();
|
||||||
localStorage.setItem(lsKey, this.core.$$getDeviceAndVaultName() || "");
|
localStorage.setItem(lsKey, this.services.setting.getDeviceAndVaultName() || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
usedPassphrase = "";
|
usedPassphrase = "";
|
||||||
$$clearUsedPassphrase(): void {
|
private _clearUsedPassphrase(): void {
|
||||||
this.usedPassphrase = "";
|
this.usedPassphrase = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,10 +108,15 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
|||||||
return `${"appId" in this.app ? this.app.appId : ""}`;
|
return `${"appId" in this.app ? this.app.appId : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$saveSettingData() {
|
async _saveSettingData() {
|
||||||
this.core.$$saveDeviceAndVaultName();
|
this.services.setting.saveDeviceAndVaultName();
|
||||||
const settings = { ...this.settings };
|
const settings = { ...this.settings };
|
||||||
settings.deviceAndVaultName = "";
|
settings.deviceAndVaultName = "";
|
||||||
|
if (settings.P2P_DevicePeerName && settings.P2P_DevicePeerName.trim() !== "") {
|
||||||
|
console.log("Saving device peer name to small config");
|
||||||
|
this.services.config.setSmallConfig(SETTING_KEY_P2P_DEVICE_NAME, settings.P2P_DevicePeerName.trim());
|
||||||
|
settings.P2P_DevicePeerName = "";
|
||||||
|
}
|
||||||
if (this.usedPassphrase == "" && !(await this.getPassphrase(settings))) {
|
if (this.usedPassphrase == "" && !(await this.getPassphrase(settings))) {
|
||||||
this._log("Failed to retrieve passphrase. data.json contains unencrypted items!", LOG_LEVEL_NOTICE);
|
this._log("Failed to retrieve passphrase. data.json contains unencrypted items!", LOG_LEVEL_NOTICE);
|
||||||
} else {
|
} else {
|
||||||
@@ -141,6 +147,7 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
|||||||
jwtSub: settings.jwtSub,
|
jwtSub: settings.jwtSub,
|
||||||
useRequestAPI: settings.useRequestAPI,
|
useRequestAPI: settings.useRequestAPI,
|
||||||
bucketPrefix: settings.bucketPrefix,
|
bucketPrefix: settings.bucketPrefix,
|
||||||
|
forcePathStyle: settings.forcePathStyle,
|
||||||
};
|
};
|
||||||
settings.encryptedCouchDBConnection = await this.encryptConfigurationItem(
|
settings.encryptedCouchDBConnection = await this.encryptConfigurationItem(
|
||||||
JSON.stringify(connectionSetting),
|
JSON.stringify(connectionSetting),
|
||||||
@@ -174,7 +181,7 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$decryptSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
|
async _decryptSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
|
||||||
const passphrase = await this.getPassphrase(settings);
|
const passphrase = await this.getPassphrase(settings);
|
||||||
if (passphrase === false) {
|
if (passphrase === false) {
|
||||||
this._log("No passphrase found for data.json! Verify configuration before syncing.", LOG_LEVEL_URGENT);
|
this._log("No passphrase found for data.json! Verify configuration before syncing.", LOG_LEVEL_URGENT);
|
||||||
@@ -234,7 +241,7 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
|||||||
* @param settings
|
* @param settings
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
$$adjustSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
|
_adjustSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
|
||||||
// Adjust settings as needed
|
// Adjust settings as needed
|
||||||
|
|
||||||
// Delete this feature to avoid problems on mobile.
|
// Delete this feature to avoid problems on mobile.
|
||||||
@@ -264,7 +271,7 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
|||||||
return Promise.resolve(settings);
|
return Promise.resolve(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$loadSettings(): Promise<void> {
|
async _loadSettings(): Promise<void> {
|
||||||
const settings = Object.assign({}, DEFAULT_SETTINGS, await this.core.loadData()) as ObsidianLiveSyncSettings;
|
const settings = Object.assign({}, DEFAULT_SETTINGS, await this.core.loadData()) as ObsidianLiveSyncSettings;
|
||||||
|
|
||||||
if (typeof settings.isConfigured == "undefined") {
|
if (typeof settings.isConfigured == "undefined") {
|
||||||
@@ -277,17 +284,17 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.settings = await this.core.$$decryptSettings(settings);
|
this.settings = await this.services.setting.decryptSettings(settings);
|
||||||
|
|
||||||
setLang(this.settings.displayLanguage);
|
setLang(this.settings.displayLanguage);
|
||||||
|
|
||||||
await this.core.$$adjustSettings(this.settings);
|
await this.services.setting.adjustSettings(this.settings);
|
||||||
|
|
||||||
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.core.$$getVaultName();
|
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.services.vault.getVaultName();
|
||||||
if (this.settings.deviceAndVaultName != "") {
|
if (this.settings.deviceAndVaultName != "") {
|
||||||
if (!localStorage.getItem(lsKey)) {
|
if (!localStorage.getItem(lsKey)) {
|
||||||
this.core.$$setDeviceAndVaultName(this.settings.deviceAndVaultName);
|
this.services.setting.setDeviceAndVaultName(this.settings.deviceAndVaultName);
|
||||||
this.$$saveDeviceAndVaultName();
|
this.services.setting.saveDeviceAndVaultName();
|
||||||
this.settings.deviceAndVaultName = "";
|
this.settings.deviceAndVaultName = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,8 +305,8 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
|||||||
);
|
);
|
||||||
this.settings.customChunkSize = 0;
|
this.settings.customChunkSize = 0;
|
||||||
}
|
}
|
||||||
this.core.$$setDeviceAndVaultName(localStorage.getItem(lsKey) || "");
|
this.services.setting.setDeviceAndVaultName(localStorage.getItem(lsKey) || "");
|
||||||
if (this.core.$$getDeviceAndVaultName() == "") {
|
if (this.services.setting.getDeviceAndVaultName() == "") {
|
||||||
if (this.settings.usePluginSync) {
|
if (this.settings.usePluginSync) {
|
||||||
this._log("Device name missing. Disabling plug-in sync.", LOG_LEVEL_NOTICE);
|
this._log("Device name missing. Disabling plug-in sync.", LOG_LEVEL_NOTICE);
|
||||||
this.settings.usePluginSync = false;
|
this.settings.usePluginSync = false;
|
||||||
@@ -309,4 +316,14 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
|||||||
// this.core.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
|
// this.core.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
|
||||||
eventHub.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB);
|
eventHub.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB);
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
|
super.onBindFunction(core, services);
|
||||||
|
services.appLifecycle.handleLayoutReady(this._everyOnLayoutReady.bind(this));
|
||||||
|
services.setting.handleClearUsedPassphrase(this._clearUsedPassphrase.bind(this));
|
||||||
|
services.setting.handleDecryptSettings(this._decryptSettings.bind(this));
|
||||||
|
services.setting.handleAdjustSettings(this._adjustSettings.bind(this));
|
||||||
|
services.setting.handleLoadSettings(this._loadSettings.bind(this));
|
||||||
|
services.setting.handleSaveDeviceAndVaultName(this._saveDeviceAndVaultName.bind(this));
|
||||||
|
services.setting.handleSaveSettingData(this._saveSettingData.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type IObsidianModule, AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
|
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
|
||||||
import { isObjectDifferent } from "octagonal-wheels/object";
|
import { isObjectDifferent } from "octagonal-wheels/object";
|
||||||
import { EVENT_SETTING_SAVED, eventHub } from "../../common/events";
|
import { EVENT_SETTING_SAVED, eventHub } from "../../common/events";
|
||||||
@@ -8,8 +8,8 @@ import { parseYaml, stringifyYaml } from "../../deps";
|
|||||||
import { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||||
const SETTING_HEADER = "````yaml:livesync-setting\n";
|
const SETTING_HEADER = "````yaml:livesync-setting\n";
|
||||||
const SETTING_FOOTER = "\n````";
|
const SETTING_FOOTER = "\n````";
|
||||||
export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule implements IObsidianModule {
|
export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule {
|
||||||
$everyOnloadStart(): Promise<boolean> {
|
_everyOnloadStart(): Promise<boolean> {
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-export-config",
|
id: "livesync-export-config",
|
||||||
name: "Write setting markdown manually",
|
name: "Write setting markdown manually",
|
||||||
@@ -18,7 +18,7 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp
|
|||||||
return this.settings.settingSyncFile != "";
|
return this.settings.settingSyncFile != "";
|
||||||
}
|
}
|
||||||
fireAndForget(async () => {
|
fireAndForget(async () => {
|
||||||
await this.core.$$saveSettingData();
|
await this.services.setting.saveSettingData();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -160,7 +160,7 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp
|
|||||||
result == APPLY_AND_FETCH
|
result == APPLY_AND_FETCH
|
||||||
) {
|
) {
|
||||||
this.core.settings = settingToApply;
|
this.core.settings = settingToApply;
|
||||||
await this.core.$$saveSettingData();
|
await this.services.setting.saveSettingData();
|
||||||
if (result == APPLY_ONLY) {
|
if (result == APPLY_ONLY) {
|
||||||
this._log("Loaded settings have been applied!", LOG_LEVEL_NOTICE);
|
this._log("Loaded settings have been applied!", LOG_LEVEL_NOTICE);
|
||||||
return;
|
return;
|
||||||
@@ -171,7 +171,7 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp
|
|||||||
if (result == APPLY_AND_FETCH) {
|
if (result == APPLY_AND_FETCH) {
|
||||||
await this.core.rebuilder.scheduleFetch();
|
await this.core.rebuilder.scheduleFetch();
|
||||||
}
|
}
|
||||||
this.core.$$performRestart();
|
this.services.appLifecycle.performRestart();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -242,4 +242,7 @@ We can perform a command in this file.
|
|||||||
this._log(`Markdown setting: ${filename} has been updated!`, LOG_LEVEL_VERBOSE);
|
this._log(`Markdown setting: ${filename} has been updated!`, LOG_LEVEL_VERBOSE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: typeof this.plugin, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ObsidianLiveSyncSettingTab } from "./SettingDialogue/ObsidianLiveSyncSettingTab.ts";
|
import { ObsidianLiveSyncSettingTab } from "./SettingDialogue/ObsidianLiveSyncSettingTab.ts";
|
||||||
import { type IObsidianModule, AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
|
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
|
||||||
import { EVENT_REQUEST_OPEN_SETTING_WIZARD, EVENT_REQUEST_OPEN_SETTINGS, eventHub } from "../../common/events.ts";
|
import { EVENT_REQUEST_OPEN_SETTING_WIZARD, EVENT_REQUEST_OPEN_SETTINGS, eventHub } from "../../common/events.ts";
|
||||||
|
|
||||||
export class ModuleObsidianSettingDialogue extends AbstractObsidianModule implements IObsidianModule {
|
export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
|
||||||
settingTab!: ObsidianLiveSyncSettingTab;
|
settingTab!: ObsidianLiveSyncSettingTab;
|
||||||
|
|
||||||
$everyOnloadStart(): Promise<boolean> {
|
_everyOnloadStart(): Promise<boolean> {
|
||||||
this.settingTab = new ObsidianLiveSyncSettingTab(this.app, this.plugin);
|
this.settingTab = new ObsidianLiveSyncSettingTab(this.app, this.plugin);
|
||||||
this.plugin.addSettingTab(this.settingTab);
|
this.plugin.addSettingTab(this.settingTab);
|
||||||
eventHub.onEvent(EVENT_REQUEST_OPEN_SETTINGS, () => this.openSetting());
|
eventHub.onEvent(EVENT_REQUEST_OPEN_SETTINGS, () => this.openSetting());
|
||||||
@@ -29,4 +29,7 @@ export class ModuleObsidianSettingDialogue extends AbstractObsidianModule implem
|
|||||||
get appId() {
|
get appId() {
|
||||||
return `${"appId" in this.app ? this.app.appId : ""}`;
|
return `${"appId" in this.app ? this.app.appId : ""}`;
|
||||||
}
|
}
|
||||||
|
onBindFunction(core: typeof this.plugin, services: typeof core.services): void {
|
||||||
|
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,38 @@
|
|||||||
import {
|
import { type ObsidianLiveSyncSettings, LOG_LEVEL_NOTICE } from "../../lib/src/common/types.ts";
|
||||||
type ObsidianLiveSyncSettings,
|
import { configURIBase } from "../../common/types.ts";
|
||||||
DEFAULT_SETTINGS,
|
|
||||||
KeyIndexOfSettings,
|
|
||||||
LOG_LEVEL_NOTICE,
|
|
||||||
LOG_LEVEL_VERBOSE,
|
|
||||||
} from "../../lib/src/common/types.ts";
|
|
||||||
import { configURIBase, configURIBaseQR } from "../../common/types.ts";
|
|
||||||
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.js";
|
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.js";
|
||||||
import { fireAndForget } from "../../lib/src/common/utils.ts";
|
import { fireAndForget } from "../../lib/src/common/utils.ts";
|
||||||
import {
|
import {
|
||||||
EVENT_REQUEST_COPY_SETUP_URI,
|
EVENT_REQUEST_COPY_SETUP_URI,
|
||||||
|
EVENT_REQUEST_OPEN_P2P_SETTINGS,
|
||||||
EVENT_REQUEST_OPEN_SETUP_URI,
|
EVENT_REQUEST_OPEN_SETUP_URI,
|
||||||
EVENT_REQUEST_SHOW_SETUP_QR,
|
EVENT_REQUEST_SHOW_SETUP_QR,
|
||||||
eventHub,
|
eventHub,
|
||||||
} from "../../common/events.ts";
|
} from "../../common/events.ts";
|
||||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
import { decodeAnyArray, encodeAnyArray } from "../../common/utils.ts";
|
|
||||||
import qrcode from "qrcode-generator";
|
|
||||||
import { $msg } from "../../lib/src/common/i18n.ts";
|
import { $msg } from "../../lib/src/common/i18n.ts";
|
||||||
import { performDoctorConsultation, RebuildOptions } from "@/lib/src/common/configForDoc.ts";
|
// import { performDoctorConsultation, RebuildOptions } from "@/lib/src/common/configForDoc.ts";
|
||||||
import { encryptString, decryptString } from "@/lib/src/encryption/stringEncryption.ts";
|
import type { LiveSyncCore } from "../../main.ts";
|
||||||
|
import {
|
||||||
|
encodeQR,
|
||||||
|
encodeSettingsToQRCodeData,
|
||||||
|
encodeSettingsToSetupURI,
|
||||||
|
OutputFormat,
|
||||||
|
} from "../../lib/src/API/processSetting.ts";
|
||||||
|
import { SetupManager, UserMode } from "./SetupManager.ts";
|
||||||
|
|
||||||
export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsidianModule {
|
export class ModuleSetupObsidian extends AbstractObsidianModule {
|
||||||
$everyOnload(): Promise<boolean> {
|
private _setupManager!: SetupManager;
|
||||||
|
private _everyOnload(): Promise<boolean> {
|
||||||
|
this._setupManager = this.plugin.getModule(SetupManager);
|
||||||
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
|
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
|
||||||
if (conf.settings) {
|
if (conf.settings) {
|
||||||
await this.setupWizard(conf.settings);
|
await this._setupManager.onUseSetupURI(
|
||||||
|
UserMode.Unknown,
|
||||||
|
`${configURIBase}${encodeURIComponent(conf.settings)}`
|
||||||
|
);
|
||||||
} else if (conf.settingsQR) {
|
} else if (conf.settingsQR) {
|
||||||
await this.decodeQR(conf.settingsQR);
|
await this._setupManager.decodeQR(conf.settingsQR);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
@@ -58,291 +63,135 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
|
|||||||
name: "Use the copied setup URI (Formerly Open setup URI)",
|
name: "Use the copied setup URI (Formerly Open setup URI)",
|
||||||
callback: () => fireAndForget(this.command_openSetupURI()),
|
callback: () => fireAndForget(this.command_openSetupURI()),
|
||||||
});
|
});
|
||||||
|
|
||||||
eventHub.onEvent(EVENT_REQUEST_OPEN_SETUP_URI, () => fireAndForget(() => this.command_openSetupURI()));
|
eventHub.onEvent(EVENT_REQUEST_OPEN_SETUP_URI, () => fireAndForget(() => this.command_openSetupURI()));
|
||||||
eventHub.onEvent(EVENT_REQUEST_COPY_SETUP_URI, () => fireAndForget(() => this.command_copySetupURI()));
|
eventHub.onEvent(EVENT_REQUEST_COPY_SETUP_URI, () => fireAndForget(() => this.command_copySetupURI()));
|
||||||
eventHub.onEvent(EVENT_REQUEST_SHOW_SETUP_QR, () => fireAndForget(() => this.encodeQR()));
|
eventHub.onEvent(EVENT_REQUEST_SHOW_SETUP_QR, () => fireAndForget(() => this.encodeQR()));
|
||||||
|
eventHub.onEvent(EVENT_REQUEST_OPEN_P2P_SETTINGS, () =>
|
||||||
|
fireAndForget(() => {
|
||||||
|
return this._setupManager.onP2PManualSetup(UserMode.Update, this.settings, false);
|
||||||
|
})
|
||||||
|
);
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
async encodeQR() {
|
async encodeQR() {
|
||||||
const settingArr = [];
|
const settingString = encodeSettingsToQRCodeData(this.settings);
|
||||||
const fullIndexes = Object.entries(KeyIndexOfSettings) as [keyof ObsidianLiveSyncSettings, number][];
|
const codeSVG = encodeQR(settingString, OutputFormat.SVG);
|
||||||
for (const [settingKey, index] of fullIndexes) {
|
const msg = $msg("Setup.QRCode", { qr_image: codeSVG });
|
||||||
const settingValue = this.settings[settingKey];
|
|
||||||
if (index < 0) {
|
|
||||||
// This setting should be ignored.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
settingArr[index] = settingValue;
|
|
||||||
}
|
|
||||||
const w = encodeAnyArray(settingArr);
|
|
||||||
const qr = qrcode(0, "L");
|
|
||||||
const uri = `${configURIBaseQR}${encodeURIComponent(w)}`;
|
|
||||||
qr.addData(uri);
|
|
||||||
qr.make();
|
|
||||||
const img = qr.createSvgTag(3);
|
|
||||||
const msg = $msg("Setup.QRCode", { qr_image: img });
|
|
||||||
await this.core.confirm.confirmWithMessage("Settings QR Code", msg, ["OK"], "OK");
|
await this.core.confirm.confirmWithMessage("Settings QR Code", msg, ["OK"], "OK");
|
||||||
return await Promise.resolve(w);
|
return await Promise.resolve(codeSVG);
|
||||||
}
|
}
|
||||||
async decodeQR(qr: string) {
|
|
||||||
const settingArr = decodeAnyArray(qr);
|
async askEncryptingPassphrase(): Promise<string | false> {
|
||||||
// console.warn(settingArr);
|
const encryptingPassphrase = await this.core.confirm.askString(
|
||||||
const fullIndexes = Object.entries(KeyIndexOfSettings) as [keyof ObsidianLiveSyncSettings, number][];
|
"Encrypt your settings",
|
||||||
const newSettings = { ...DEFAULT_SETTINGS } as ObsidianLiveSyncSettings;
|
"The passphrase to encrypt the setup URI",
|
||||||
for (const [settingKey, index] of fullIndexes) {
|
"",
|
||||||
if (index < 0) {
|
true
|
||||||
// This setting should be ignored.
|
);
|
||||||
continue;
|
return encryptingPassphrase;
|
||||||
}
|
|
||||||
if (index >= settingArr.length) {
|
|
||||||
// Possibly a new setting added.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const settingValue = settingArr[index];
|
|
||||||
//@ts-ignore
|
|
||||||
newSettings[settingKey] = settingValue;
|
|
||||||
}
|
|
||||||
await this.applySettingWizard(this.settings, newSettings, "QR Code");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async command_copySetupURI(stripExtra = true) {
|
async command_copySetupURI(stripExtra = true) {
|
||||||
const encryptingPassphrase = await this.core.confirm.askString(
|
const encryptingPassphrase = await this.askEncryptingPassphrase();
|
||||||
"Encrypt your settings",
|
if (encryptingPassphrase === false) return;
|
||||||
"The passphrase to encrypt the setup URI",
|
const encryptedURI = await encodeSettingsToSetupURI(
|
||||||
"",
|
this.settings,
|
||||||
|
encryptingPassphrase,
|
||||||
|
[...((stripExtra ? ["pluginSyncExtendedSetting"] : []) as (keyof ObsidianLiveSyncSettings)[])],
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
if (encryptingPassphrase === false) return;
|
if (await this.services.UI.promptCopyToClipboard("Setup URI", encryptedURI)) {
|
||||||
const setting = {
|
this._log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
|
||||||
...this.settings,
|
|
||||||
configPassphraseStore: "",
|
|
||||||
encryptedCouchDBConnection: "",
|
|
||||||
encryptedPassphrase: "",
|
|
||||||
} as Partial<ObsidianLiveSyncSettings>;
|
|
||||||
if (stripExtra) {
|
|
||||||
delete setting.pluginSyncExtendedSetting;
|
|
||||||
}
|
}
|
||||||
const keys = Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[];
|
// await navigator.clipboard.writeText(encryptedURI);
|
||||||
for (const k of keys) {
|
|
||||||
if (
|
|
||||||
JSON.stringify(k in setting ? setting[k] : "") ==
|
|
||||||
JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*")
|
|
||||||
) {
|
|
||||||
delete setting[k];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const encryptedSetting = encodeURIComponent(await encryptString(JSON.stringify(setting), encryptingPassphrase));
|
|
||||||
const uri = `${configURIBase}${encryptedSetting} `;
|
|
||||||
await navigator.clipboard.writeText(uri);
|
|
||||||
this._log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async command_copySetupURIFull() {
|
async command_copySetupURIFull() {
|
||||||
const encryptingPassphrase = await this.core.confirm.askString(
|
const encryptingPassphrase = await this.askEncryptingPassphrase();
|
||||||
"Encrypt your settings",
|
|
||||||
"The passphrase to encrypt the setup URI",
|
|
||||||
"",
|
|
||||||
true
|
|
||||||
);
|
|
||||||
if (encryptingPassphrase === false) return;
|
if (encryptingPassphrase === false) return;
|
||||||
const setting = {
|
const encryptedURI = await encodeSettingsToSetupURI(this.settings, encryptingPassphrase, [], false);
|
||||||
...this.settings,
|
await navigator.clipboard.writeText(encryptedURI);
|
||||||
configPassphraseStore: "",
|
|
||||||
encryptedCouchDBConnection: "",
|
|
||||||
encryptedPassphrase: "",
|
|
||||||
};
|
|
||||||
const encryptedSetting = encodeURIComponent(await encryptString(JSON.stringify(setting), encryptingPassphrase));
|
|
||||||
const uri = `${configURIBase}${encryptedSetting} `;
|
|
||||||
await navigator.clipboard.writeText(uri);
|
|
||||||
this._log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
|
this._log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
async command_copySetupURIWithSync() {
|
async command_copySetupURIWithSync() {
|
||||||
await this.command_copySetupURI(false);
|
await this.command_copySetupURI(false);
|
||||||
}
|
}
|
||||||
async command_openSetupURI() {
|
async command_openSetupURI() {
|
||||||
const setupURI = await this.core.confirm.askString("Easy setup", "Set up URI", `${configURIBase} aaaaa`);
|
await this._setupManager.onUseSetupURI(UserMode.Unknown);
|
||||||
if (setupURI === false) return;
|
|
||||||
if (!setupURI.startsWith(`${configURIBase}`)) {
|
|
||||||
this._log("Set up URI looks wrong.", LOG_LEVEL_NOTICE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const config = decodeURIComponent(setupURI.substring(configURIBase.length));
|
|
||||||
await this.setupWizard(config);
|
|
||||||
}
|
|
||||||
async askSyncWithRemoteConfig(tryingSettings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
|
|
||||||
const buttons = {
|
|
||||||
fetch: $msg("Setup.FetchRemoteConf.Buttons.Fetch"),
|
|
||||||
no: $msg("Setup.FetchRemoteConf.Buttons.Skip"),
|
|
||||||
} as const;
|
|
||||||
const fetchRemoteConf = await this.core.confirm.askSelectStringDialogue(
|
|
||||||
$msg("Setup.FetchRemoteConf.Message"),
|
|
||||||
Object.values(buttons),
|
|
||||||
{ defaultAction: buttons.fetch, timeout: 0, title: $msg("Setup.FetchRemoteConf.Title") }
|
|
||||||
);
|
|
||||||
if (fetchRemoteConf == buttons.no) {
|
|
||||||
return tryingSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newSettings = JSON.parse(JSON.stringify(tryingSettings)) as ObsidianLiveSyncSettings;
|
|
||||||
const remoteConfig = await this.core.$$fetchRemotePreferredTweakValues(newSettings);
|
|
||||||
if (remoteConfig) {
|
|
||||||
this._log("Remote configuration found.", LOG_LEVEL_NOTICE);
|
|
||||||
const resultSettings = {
|
|
||||||
...DEFAULT_SETTINGS,
|
|
||||||
...tryingSettings,
|
|
||||||
...remoteConfig,
|
|
||||||
} satisfies ObsidianLiveSyncSettings;
|
|
||||||
return resultSettings;
|
|
||||||
} else {
|
|
||||||
this._log("Remote configuration not applied.", LOG_LEVEL_NOTICE);
|
|
||||||
return {
|
|
||||||
...DEFAULT_SETTINGS,
|
|
||||||
...tryingSettings,
|
|
||||||
} satisfies ObsidianLiveSyncSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async askPerformDoctor(
|
|
||||||
tryingSettings: ObsidianLiveSyncSettings
|
|
||||||
): Promise<{ settings: ObsidianLiveSyncSettings; shouldRebuild: boolean; isModified: boolean }> {
|
|
||||||
const buttons = {
|
|
||||||
yes: $msg("Setup.Doctor.Buttons.Yes"),
|
|
||||||
no: $msg("Setup.Doctor.Buttons.No"),
|
|
||||||
} as const;
|
|
||||||
const performDoctor = await this.core.confirm.askSelectStringDialogue(
|
|
||||||
$msg("Setup.Doctor.Message"),
|
|
||||||
Object.values(buttons),
|
|
||||||
{ defaultAction: buttons.yes, timeout: 0, title: $msg("Setup.Doctor.Title") }
|
|
||||||
);
|
|
||||||
if (performDoctor == buttons.no) {
|
|
||||||
return { settings: tryingSettings, shouldRebuild: false, isModified: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
const newSettings = JSON.parse(JSON.stringify(tryingSettings)) as ObsidianLiveSyncSettings;
|
|
||||||
const { settings, shouldRebuild, isModified } = await performDoctorConsultation(this.core, newSettings, {
|
|
||||||
localRebuild: RebuildOptions.AutomaticAcceptable, // Because we are in the setup wizard, we can skip the confirmation.
|
|
||||||
remoteRebuild: RebuildOptions.SkipEvenIfRequired,
|
|
||||||
activateReason: "New settings from URI",
|
|
||||||
});
|
|
||||||
if (isModified) {
|
|
||||||
this._log("Doctor has fixed some issues!", LOG_LEVEL_NOTICE);
|
|
||||||
return {
|
|
||||||
settings,
|
|
||||||
shouldRebuild,
|
|
||||||
isModified,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this._log("Doctor detected no issues!", LOG_LEVEL_NOTICE);
|
|
||||||
return { settings: tryingSettings, shouldRebuild: false, isModified: false };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async applySettingWizard(
|
// TODO: Where to implement these?
|
||||||
oldConf: ObsidianLiveSyncSettings,
|
|
||||||
newConf: ObsidianLiveSyncSettings,
|
|
||||||
method = "Setup URI"
|
|
||||||
) {
|
|
||||||
const result = await this.core.confirm.askYesNoDialog(
|
|
||||||
"Importing Configuration from the " + method + ". Are you sure to proceed ? ",
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
if (result == "yes") {
|
|
||||||
let newSettingW = Object.assign({}, DEFAULT_SETTINGS, newConf) as ObsidianLiveSyncSettings;
|
|
||||||
this.core.replicator.closeReplication();
|
|
||||||
this.settings.suspendFileWatching = true;
|
|
||||||
newSettingW = await this.askSyncWithRemoteConfig(newSettingW);
|
|
||||||
const { settings, shouldRebuild, isModified } = await this.askPerformDoctor(newSettingW);
|
|
||||||
if (isModified) {
|
|
||||||
newSettingW = settings;
|
|
||||||
}
|
|
||||||
// Back into the default method once.
|
|
||||||
newSettingW.configPassphraseStore = "";
|
|
||||||
newSettingW.encryptedPassphrase = "";
|
|
||||||
newSettingW.encryptedCouchDBConnection = "";
|
|
||||||
newSettingW.additionalSuffixOfDatabaseName = `${"appId" in this.app ? this.app.appId : ""} `;
|
|
||||||
const setupJustImport = $msg("Setup.Apply.Buttons.OnlyApply");
|
|
||||||
const setupAsNew = $msg("Setup.Apply.Buttons.ApplyAndFetch");
|
|
||||||
const setupAsMerge = $msg("Setup.Apply.Buttons.ApplyAndMerge");
|
|
||||||
const setupAgain = $msg("Setup.Apply.Buttons.ApplyAndRebuild");
|
|
||||||
const setupCancel = $msg("Setup.Apply.Buttons.Cancel");
|
|
||||||
newSettingW.syncInternalFiles = false;
|
|
||||||
newSettingW.usePluginSync = false;
|
|
||||||
newSettingW.isConfigured = true;
|
|
||||||
// Migrate completely obsoleted configuration.
|
|
||||||
if (!newSettingW.useIndexedDBAdapter) {
|
|
||||||
newSettingW.useIndexedDBAdapter = true;
|
|
||||||
}
|
|
||||||
const warn = shouldRebuild ? $msg("Setup.Apply.WarningRebuildRecommended") : "";
|
|
||||||
const message = $msg("Setup.Apply.Message", {
|
|
||||||
method,
|
|
||||||
warn,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setupType = await this.core.confirm.askSelectStringDialogue(
|
// async askSyncWithRemoteConfig(tryingSettings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
|
||||||
message,
|
// const buttons = {
|
||||||
[setupAsNew, setupAsMerge, setupAgain, setupJustImport, setupCancel],
|
// fetch: $msg("Setup.FetchRemoteConf.Buttons.Fetch"),
|
||||||
{ defaultAction: setupAsNew, title: $msg("Setup.Apply.Title", { method }), timeout: 0 }
|
// no: $msg("Setup.FetchRemoteConf.Buttons.Skip"),
|
||||||
);
|
// } as const;
|
||||||
if (setupType == setupJustImport) {
|
// const fetchRemoteConf = await this.core.confirm.askSelectStringDialogue(
|
||||||
this.core.settings = newSettingW;
|
// $msg("Setup.FetchRemoteConf.Message"),
|
||||||
this.core.$$clearUsedPassphrase();
|
// Object.values(buttons),
|
||||||
await this.core.saveSettings();
|
// { defaultAction: buttons.fetch, timeout: 0, title: $msg("Setup.FetchRemoteConf.Title") }
|
||||||
} else if (setupType == setupAsNew) {
|
// );
|
||||||
this.core.settings = newSettingW;
|
// if (fetchRemoteConf == buttons.no) {
|
||||||
this.core.$$clearUsedPassphrase();
|
// return tryingSettings;
|
||||||
await this.core.saveSettings();
|
// }
|
||||||
await this.core.rebuilder.$fetchLocal();
|
|
||||||
} else if (setupType == setupAsMerge) {
|
// const newSettings = JSON.parse(JSON.stringify(tryingSettings)) as ObsidianLiveSyncSettings;
|
||||||
this.core.settings = newSettingW;
|
// const remoteConfig = await this.services.tweakValue.fetchRemotePreferred(newSettings);
|
||||||
this.core.$$clearUsedPassphrase();
|
// if (remoteConfig) {
|
||||||
await this.core.saveSettings();
|
// this._log("Remote configuration found.", LOG_LEVEL_NOTICE);
|
||||||
await this.core.rebuilder.$fetchLocal(true);
|
// const resultSettings = {
|
||||||
} else if (setupType == setupAgain) {
|
// ...DEFAULT_SETTINGS,
|
||||||
const confirm =
|
// ...tryingSettings,
|
||||||
"This operation will rebuild all databases with files on this device. Any files on the remote database not synced here will be lost.";
|
// ...remoteConfig,
|
||||||
if (
|
// } satisfies ObsidianLiveSyncSettings;
|
||||||
(await this.core.confirm.askSelectStringDialogue(
|
// return resultSettings;
|
||||||
"Are you sure you want to do this?",
|
// } else {
|
||||||
["Cancel", confirm],
|
// this._log("Remote configuration not applied.", LOG_LEVEL_NOTICE);
|
||||||
{ defaultAction: "Cancel" }
|
// return {
|
||||||
)) != confirm
|
// ...DEFAULT_SETTINGS,
|
||||||
) {
|
// ...tryingSettings,
|
||||||
return;
|
// } satisfies ObsidianLiveSyncSettings;
|
||||||
}
|
// }
|
||||||
this.core.settings = newSettingW;
|
// }
|
||||||
await this.core.saveSettings();
|
// async askPerformDoctor(
|
||||||
this.core.$$clearUsedPassphrase();
|
// tryingSettings: ObsidianLiveSyncSettings
|
||||||
await this.core.rebuilder.$rebuildEverything();
|
// ): Promise<{ settings: ObsidianLiveSyncSettings; shouldRebuild: boolean; isModified: boolean }> {
|
||||||
} else {
|
// const buttons = {
|
||||||
// Explicitly cancel the operation or the dialog was closed.
|
// yes: $msg("Setup.Doctor.Buttons.Yes"),
|
||||||
this._log("Cancelled", LOG_LEVEL_NOTICE);
|
// no: $msg("Setup.Doctor.Buttons.No"),
|
||||||
this.core.settings = oldConf;
|
// } as const;
|
||||||
return;
|
// const performDoctor = await this.core.confirm.askSelectStringDialogue(
|
||||||
}
|
// $msg("Setup.Doctor.Message"),
|
||||||
this._log("Configuration loaded.", LOG_LEVEL_NOTICE);
|
// Object.values(buttons),
|
||||||
} else {
|
// { defaultAction: buttons.yes, timeout: 0, title: $msg("Setup.Doctor.Title") }
|
||||||
this._log("Cancelled", LOG_LEVEL_NOTICE);
|
// );
|
||||||
this.core.settings = oldConf;
|
// if (performDoctor == buttons.no) {
|
||||||
return;
|
// return { settings: tryingSettings, shouldRebuild: false, isModified: false };
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
async setupWizard(confString: string) {
|
// const newSettings = JSON.parse(JSON.stringify(tryingSettings)) as ObsidianLiveSyncSettings;
|
||||||
try {
|
// const { settings, shouldRebuild, isModified } = await performDoctorConsultation(this.core, newSettings, {
|
||||||
const oldConf = JSON.parse(JSON.stringify(this.settings));
|
// localRebuild: RebuildOptions.AutomaticAcceptable, // Because we are in the setup wizard, we can skip the confirmation.
|
||||||
const encryptingPassphrase = await this.core.confirm.askString(
|
// remoteRebuild: RebuildOptions.SkipEvenIfRequired,
|
||||||
"Passphrase",
|
// activateReason: "New settings from URI",
|
||||||
"The passphrase to decrypt your setup URI",
|
// });
|
||||||
"",
|
// if (isModified) {
|
||||||
true
|
// this._log("Doctor has fixed some issues!", LOG_LEVEL_NOTICE);
|
||||||
);
|
// return {
|
||||||
if (encryptingPassphrase === false) return;
|
// settings,
|
||||||
const newConf = await JSON.parse(await decryptString(confString, encryptingPassphrase));
|
// shouldRebuild,
|
||||||
if (newConf) {
|
// isModified,
|
||||||
await this.applySettingWizard(oldConf, newConf);
|
// };
|
||||||
this._log("Configuration loaded.", LOG_LEVEL_NOTICE);
|
// } else {
|
||||||
} else {
|
// this._log("Doctor detected no issues!", LOG_LEVEL_NOTICE);
|
||||||
this._log("Cancelled.", LOG_LEVEL_NOTICE);
|
// return { settings: tryingSettings, shouldRebuild: false, isModified: false };
|
||||||
}
|
// }
|
||||||
} catch (ex) {
|
// }
|
||||||
this._log("Couldn't parse or decrypt configuration uri.", LOG_LEVEL_NOTICE);
|
|
||||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
}
|
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/modules/features/SettingDialogue/InfoPanel.svelte
Normal file
15
src/modules/features/SettingDialogue/InfoPanel.svelte
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Info Panel to display key-value information from the port
|
||||||
|
* Mostly used in the Setting Dialogue
|
||||||
|
*/
|
||||||
|
import { type SveltePanelProps } from "./SveltePanel";
|
||||||
|
import InfoTable from "@lib/UI/components/InfoTable.svelte";
|
||||||
|
type Props = SveltePanelProps<{
|
||||||
|
info: Record<string, any>;
|
||||||
|
}>;
|
||||||
|
const { port }: Props = $props();
|
||||||
|
const info = $derived.by(() => $port?.info ?? {});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<InfoTable {info} />
|
||||||
@@ -17,9 +17,7 @@ import { delay, isObjectDifferent, sizeToHumanReadable } from "../../../lib/src/
|
|||||||
import { versionNumberString2Number } from "../../../lib/src/string_and_binary/convert.ts";
|
import { versionNumberString2Number } from "../../../lib/src/string_and_binary/convert.ts";
|
||||||
import { Logger } from "../../../lib/src/common/logger.ts";
|
import { Logger } from "../../../lib/src/common/logger.ts";
|
||||||
import { checkSyncInfo } from "@/lib/src/pouchdb/negotiation.ts";
|
import { checkSyncInfo } from "@/lib/src/pouchdb/negotiation.ts";
|
||||||
import { balanceChunkPurgedDBs } from "@/lib/src/pouchdb/chunks.ts";
|
import { testCrypt } from "octagonal-wheels/encryption/encryption";
|
||||||
import { purgeUnreferencedChunks } from "@/lib/src/pouchdb/chunks.ts";
|
|
||||||
import { testCrypt } from "../../../lib/src/encryption/e2ee_v2.ts";
|
|
||||||
import ObsidianLiveSyncPlugin from "../../../main.ts";
|
import ObsidianLiveSyncPlugin from "../../../main.ts";
|
||||||
import { scheduleTask } from "../../../common/utils.ts";
|
import { scheduleTask } from "../../../common/utils.ts";
|
||||||
import { LiveSyncCouchDBReplicator } from "../../../lib/src/replication/couchdb/LiveSyncReplicator.ts";
|
import { LiveSyncCouchDBReplicator } from "../../../lib/src/replication/couchdb/LiveSyncReplicator.ts";
|
||||||
@@ -38,7 +36,6 @@ import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
|||||||
import { fireAndForget, yieldNextAnimationFrame } from "octagonal-wheels/promises";
|
import { fireAndForget, yieldNextAnimationFrame } from "octagonal-wheels/promises";
|
||||||
import { confirmWithMessage } from "../../coreObsidian/UILib/dialogs.ts";
|
import { confirmWithMessage } from "../../coreObsidian/UILib/dialogs.ts";
|
||||||
import { EVENT_REQUEST_RELOAD_SETTING_TAB, eventHub } from "../../../common/events.ts";
|
import { EVENT_REQUEST_RELOAD_SETTING_TAB, eventHub } from "../../../common/events.ts";
|
||||||
import { skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
|
|
||||||
import { JournalSyncMinio } from "../../../lib/src/replication/journal/objectstore/JournalSyncMinio.ts";
|
import { JournalSyncMinio } from "../../../lib/src/replication/journal/objectstore/JournalSyncMinio.ts";
|
||||||
import { paneChangeLog } from "./PaneChangeLog.ts";
|
import { paneChangeLog } from "./PaneChangeLog.ts";
|
||||||
import {
|
import {
|
||||||
@@ -89,6 +86,9 @@ export function createStub(name: string, key: string, value: string, panel: stri
|
|||||||
|
|
||||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||||
plugin: ObsidianLiveSyncPlugin;
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
|
get services() {
|
||||||
|
return this.plugin.services;
|
||||||
|
}
|
||||||
selectedScreen = "";
|
selectedScreen = "";
|
||||||
|
|
||||||
_editingSettings?: AllSettings;
|
_editingSettings?: AllSettings;
|
||||||
@@ -142,8 +142,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
return await Promise.resolve();
|
return await Promise.resolve();
|
||||||
}
|
}
|
||||||
if (key == "deviceAndVaultName") {
|
if (key == "deviceAndVaultName") {
|
||||||
this.plugin.$$setDeviceAndVaultName(this.editingSettings?.[key] ?? "");
|
this.services.setting.setDeviceAndVaultName(this.editingSettings?.[key] ?? "");
|
||||||
this.plugin.$$saveDeviceAndVaultName();
|
this.services.setting.saveDeviceAndVaultName();
|
||||||
return await Promise.resolve();
|
return await Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +213,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
const ret = { ...OnDialogSettingsDefault };
|
const ret = { ...OnDialogSettingsDefault };
|
||||||
ret.configPassphrase = localStorage.getItem("ls-setting-passphrase") || "";
|
ret.configPassphrase = localStorage.getItem("ls-setting-passphrase") || "";
|
||||||
ret.preset = "";
|
ret.preset = "";
|
||||||
ret.deviceAndVaultName = this.plugin.$$getDeviceAndVaultName();
|
ret.deviceAndVaultName = this.services.setting.getDeviceAndVaultName();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
computeAllLocalSettings(): Partial<OnDialogSettings> {
|
computeAllLocalSettings(): Partial<OnDialogSettings> {
|
||||||
@@ -298,7 +298,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
async testConnection(settingOverride: Partial<ObsidianLiveSyncSettings> = {}): Promise<void> {
|
async testConnection(settingOverride: Partial<ObsidianLiveSyncSettings> = {}): Promise<void> {
|
||||||
const trialSetting = { ...this.editingSettings, ...settingOverride };
|
const trialSetting = { ...this.editingSettings, ...settingOverride };
|
||||||
const replicator = await this.plugin.$anyNewReplicator(trialSetting);
|
const replicator = await this.services.replicator.getNewReplicator(trialSetting);
|
||||||
|
if (!replicator) {
|
||||||
|
Logger("No replicator available for the current settings.", LOG_LEVEL_NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await replicator.tryConnectRemote(trialSetting);
|
await replicator.tryConnectRemote(trialSetting);
|
||||||
const status = await replicator.getRemoteStatus(trialSetting);
|
const status = await replicator.getRemoteStatus(trialSetting);
|
||||||
if (status) {
|
if (status) {
|
||||||
@@ -549,10 +553,14 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
const settingForCheck: RemoteDBSettings = {
|
const settingForCheck: RemoteDBSettings = {
|
||||||
...this.editingSettings,
|
...this.editingSettings,
|
||||||
};
|
};
|
||||||
const replicator = this.plugin.$anyNewReplicator(settingForCheck);
|
const replicator = this.services.replicator.getNewReplicator(settingForCheck);
|
||||||
if (!(replicator instanceof LiveSyncCouchDBReplicator)) return true;
|
if (!(replicator instanceof LiveSyncCouchDBReplicator)) return true;
|
||||||
|
|
||||||
const db = await replicator.connectRemoteCouchDBWithSetting(settingForCheck, this.plugin.$$isMobile(), true);
|
const db = await replicator.connectRemoteCouchDBWithSetting(
|
||||||
|
settingForCheck,
|
||||||
|
this.services.API.isMobile(),
|
||||||
|
true
|
||||||
|
);
|
||||||
if (typeof db === "string") {
|
if (typeof db === "string") {
|
||||||
Logger($msg("obsidianLiveSyncSettingTab.logCheckPassphraseFailed", { db }), LOG_LEVEL_NOTICE);
|
Logger($msg("obsidianLiveSyncSettingTab.logCheckPassphraseFailed", { db }), LOG_LEVEL_NOTICE);
|
||||||
return false;
|
return false;
|
||||||
@@ -591,8 +599,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
this.editingSettings.passphrase = "";
|
this.editingSettings.passphrase = "";
|
||||||
}
|
}
|
||||||
this.applyAllSettings();
|
this.applyAllSettings();
|
||||||
await this.plugin.$allSuspendAllSync();
|
await this.services.setting.suspendAllSync();
|
||||||
await this.plugin.$allSuspendExtraSync();
|
await this.services.setting.suspendExtraSync();
|
||||||
this.reloadAllSettings();
|
this.reloadAllSettings();
|
||||||
this.editingSettings.isConfigured = true;
|
this.editingSettings.isConfigured = true;
|
||||||
Logger($msg("obsidianLiveSyncSettingTab.logRebuildNote"), LOG_LEVEL_NOTICE);
|
Logger($msg("obsidianLiveSyncSettingTab.logRebuildNote"), LOG_LEVEL_NOTICE);
|
||||||
@@ -641,12 +649,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
await this.applyAllSettings();
|
await this.applyAllSettings();
|
||||||
if (result == OPTION_FETCH) {
|
if (result == OPTION_FETCH) {
|
||||||
await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, "");
|
await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, "");
|
||||||
this.plugin.$$scheduleAppReload();
|
this.services.appLifecycle.scheduleRestart();
|
||||||
this.closeSetting();
|
this.closeSetting();
|
||||||
// await rebuildDB("localOnly");
|
// await rebuildDB("localOnly");
|
||||||
} else if (result == OPTION_REBUILD_BOTH) {
|
} else if (result == OPTION_REBUILD_BOTH) {
|
||||||
await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, "");
|
await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, "");
|
||||||
this.plugin.$$scheduleAppReload();
|
this.services.appLifecycle.scheduleRestart();
|
||||||
this.closeSetting();
|
this.closeSetting();
|
||||||
} else if (result == OPTION_ONLY_SETTING) {
|
} else if (result == OPTION_ONLY_SETTING) {
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
@@ -861,70 +869,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async dryRunGC() {
|
|
||||||
await skipIfDuplicated("cleanup", async () => {
|
|
||||||
const replicator = this.plugin.$$getReplicator();
|
|
||||||
if (!(replicator instanceof LiveSyncCouchDBReplicator)) return;
|
|
||||||
const remoteDBConn = await replicator.connectRemoteCouchDBWithSetting(
|
|
||||||
this.plugin.settings,
|
|
||||||
this.plugin.$$isMobile()
|
|
||||||
);
|
|
||||||
if (typeof remoteDBConn == "string") {
|
|
||||||
Logger(remoteDBConn);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await purgeUnreferencedChunks(remoteDBConn.db, true, this.plugin.settings, false);
|
|
||||||
await purgeUnreferencedChunks(this.plugin.localDatabase.localDatabase, true);
|
|
||||||
this.plugin.localDatabase.clearCaches();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async dbGC() {
|
|
||||||
// Lock the remote completely once.
|
|
||||||
await skipIfDuplicated("cleanup", async () => {
|
|
||||||
const replicator = this.plugin.$$getReplicator();
|
|
||||||
if (!(replicator instanceof LiveSyncCouchDBReplicator)) return;
|
|
||||||
await this.plugin.$$getReplicator().markRemoteLocked(this.plugin.settings, true, true);
|
|
||||||
const remoteDBConnection = await replicator.connectRemoteCouchDBWithSetting(
|
|
||||||
this.plugin.settings,
|
|
||||||
this.plugin.$$isMobile()
|
|
||||||
);
|
|
||||||
if (typeof remoteDBConnection == "string") {
|
|
||||||
Logger(remoteDBConnection);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await purgeUnreferencedChunks(remoteDBConnection.db, false, this.plugin.settings, true);
|
|
||||||
await purgeUnreferencedChunks(this.plugin.localDatabase.localDatabase, false);
|
|
||||||
this.plugin.localDatabase.clearCaches();
|
|
||||||
await balanceChunkPurgedDBs(this.plugin.localDatabase.localDatabase, remoteDBConnection.db);
|
|
||||||
this.plugin.localDatabase.refreshSettings();
|
|
||||||
Logger(
|
|
||||||
"The remote database has been cleaned up! Other devices will be cleaned up on the next synchronisation."
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getMinioJournalSyncClient() {
|
getMinioJournalSyncClient() {
|
||||||
const id = this.plugin.settings.accessKey;
|
return new JournalSyncMinio(this.plugin.settings, this.plugin.simpleStore, this.plugin);
|
||||||
const key = this.plugin.settings.secretKey;
|
|
||||||
const bucket = this.plugin.settings.bucket;
|
|
||||||
const prefix = this.plugin.settings.bucketPrefix;
|
|
||||||
const region = this.plugin.settings.region;
|
|
||||||
const endpoint = this.plugin.settings.endpoint;
|
|
||||||
const useCustomRequestHandler = this.plugin.settings.useCustomRequestHandler;
|
|
||||||
const customHeaders = this.plugin.settings.bucketCustomHeaders;
|
|
||||||
return new JournalSyncMinio(
|
|
||||||
id,
|
|
||||||
key,
|
|
||||||
endpoint,
|
|
||||||
bucket,
|
|
||||||
prefix,
|
|
||||||
this.plugin.simpleStore,
|
|
||||||
this.plugin,
|
|
||||||
useCustomRequestHandler,
|
|
||||||
region,
|
|
||||||
customHeaders
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
async resetRemoteBucket() {
|
async resetRemoteBucket() {
|
||||||
const minioJournal = this.getMinioJournalSyncClient();
|
const minioJournal = this.getMinioJournalSyncClient();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { versionNumberString2Number } from "../../../lib/src/string_and_binary/c
|
|||||||
import { $msg } from "../../../lib/src/common/i18n.ts";
|
import { $msg } from "../../../lib/src/common/i18n.ts";
|
||||||
import { fireAndForget } from "octagonal-wheels/promises";
|
import { fireAndForget } from "octagonal-wheels/promises";
|
||||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||||
|
import { visibleOnly } from "./SettingPane.ts";
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const manifestVersion: string = MANIFEST_VERSION || "-";
|
const manifestVersion: string = MANIFEST_VERSION || "-";
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@@ -10,8 +11,34 @@ const updateInformation: string = UPDATE_INFO || "";
|
|||||||
|
|
||||||
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
|
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
|
||||||
export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement): void {
|
export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement): void {
|
||||||
const informationDivEl = this.createEl(paneEl, "div", { text: "" });
|
const cx = this.createEl(
|
||||||
|
paneEl,
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
cls: "op-warn-info",
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
visibleOnly(() => !this.isConfiguredAs("versionUpFlash", ""))
|
||||||
|
);
|
||||||
|
|
||||||
|
this.createEl(
|
||||||
|
cx,
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
text: this.editingSettings.versionUpFlash,
|
||||||
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
this.createEl(cx, "button", { text: $msg("obsidianLiveSyncSettingTab.btnGotItAndUpdated") }, (e) => {
|
||||||
|
e.addClass("mod-cta");
|
||||||
|
e.addEventListener("click", () => {
|
||||||
|
fireAndForget(async () => {
|
||||||
|
this.editingSettings.versionUpFlash = "";
|
||||||
|
await this.saveAllDirtySettings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const informationDivEl = this.createEl(paneEl, "div", { text: "" });
|
||||||
const tmpDiv = createDiv();
|
const tmpDiv = createDiv();
|
||||||
// tmpDiv.addClass("sls-header-button");
|
// tmpDiv.addClass("sls-header-button");
|
||||||
tmpDiv.addClass("op-warn-info");
|
tmpDiv.addClass("op-warn-info");
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { addPrefix, shouldBeIgnored, stripAllPrefixes } from "../../../lib/src/s
|
|||||||
import { $msg } from "../../../lib/src/common/i18n.ts";
|
import { $msg } from "../../../lib/src/common/i18n.ts";
|
||||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||||
import { EVENT_REQUEST_RUN_DOCTOR, eventHub } from "../../../common/events.ts";
|
import { EVENT_REQUEST_RUN_DOCTOR, EVENT_REQUEST_RUN_FIX_INCOMPLETE, eventHub } from "../../../common/events.ts";
|
||||||
import { ICHeader, ICXHeader, PSCHeader } from "../../../common/types.ts";
|
import { ICHeader, ICXHeader, PSCHeader } from "../../../common/types.ts";
|
||||||
import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSync.ts";
|
import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSync.ts";
|
||||||
import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts";
|
import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts";
|
||||||
@@ -50,6 +50,19 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
|
|||||||
eventHub.emitEvent(EVENT_REQUEST_RUN_DOCTOR, "you wanted(Thank you)!");
|
eventHub.emitEvent(EVENT_REQUEST_RUN_DOCTOR, "you wanted(Thank you)!");
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
new Setting(paneEl)
|
||||||
|
.setName($msg("Setting.TroubleShooting.ScanBrokenFiles"))
|
||||||
|
.setDesc($msg("Setting.TroubleShooting.ScanBrokenFiles.Desc"))
|
||||||
|
.addButton((button) =>
|
||||||
|
button
|
||||||
|
.setButtonText("Scan for Broken files")
|
||||||
|
.setCta()
|
||||||
|
.setDisabled(false)
|
||||||
|
.onClick(() => {
|
||||||
|
this.closeSetting();
|
||||||
|
eventHub.emitEvent(EVENT_REQUEST_RUN_FIX_INCOMPLETE);
|
||||||
|
})
|
||||||
|
);
|
||||||
new Setting(paneEl).setName("Prepare the 'report' to create an issue").addButton((button) =>
|
new Setting(paneEl).setName("Prepare the 'report' to create an issue").addButton((button) =>
|
||||||
button
|
button
|
||||||
.setButtonText("Copy Report to clipboard")
|
.setButtonText("Copy Report to clipboard")
|
||||||
@@ -130,6 +143,9 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
|
|||||||
pluginConfig.jwtKid = redact(pluginConfig.jwtKid);
|
pluginConfig.jwtKid = redact(pluginConfig.jwtKid);
|
||||||
pluginConfig.bucketCustomHeaders = redact(pluginConfig.bucketCustomHeaders);
|
pluginConfig.bucketCustomHeaders = redact(pluginConfig.bucketCustomHeaders);
|
||||||
pluginConfig.couchDB_CustomHeaders = redact(pluginConfig.couchDB_CustomHeaders);
|
pluginConfig.couchDB_CustomHeaders = redact(pluginConfig.couchDB_CustomHeaders);
|
||||||
|
pluginConfig.P2P_turnCredential = redact(pluginConfig.P2P_turnCredential);
|
||||||
|
pluginConfig.P2P_turnUsername = redact(pluginConfig.P2P_turnUsername);
|
||||||
|
pluginConfig.P2P_turnServers = `(${pluginConfig.P2P_turnServers.split(",").length} servers configured)`;
|
||||||
const endpoint = pluginConfig.endpoint;
|
const endpoint = pluginConfig.endpoint;
|
||||||
if (endpoint == "") {
|
if (endpoint == "") {
|
||||||
pluginConfig.endpoint = "Not configured or AWS";
|
pluginConfig.endpoint = "Not configured or AWS";
|
||||||
@@ -143,7 +159,7 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
|
|||||||
}
|
}
|
||||||
const obsidianInfo = {
|
const obsidianInfo = {
|
||||||
navigator: navigator.userAgent,
|
navigator: navigator.userAgent,
|
||||||
fileSystem: this.plugin.$$isStorageInsensitive() ? "insensitive" : "sensitive",
|
fileSystem: this.plugin.services.vault.isStorageInsensitive() ? "insensitive" : "sensitive",
|
||||||
};
|
};
|
||||||
const msgConfig = `# ---- Obsidian info ----
|
const msgConfig = `# ---- Obsidian info ----
|
||||||
${stringifyYaml(obsidianInfo)}
|
${stringifyYaml(obsidianInfo)}
|
||||||
@@ -157,11 +173,13 @@ ${stringifyYaml({
|
|||||||
...pluginConfig,
|
...pluginConfig,
|
||||||
})}`;
|
})}`;
|
||||||
console.log(msgConfig);
|
console.log(msgConfig);
|
||||||
await navigator.clipboard.writeText(msgConfig);
|
if ((await this.services.UI.promptCopyToClipboard("Generated report", msgConfig)) == true) {
|
||||||
Logger(
|
// await navigator.clipboard.writeText(msgConfig);
|
||||||
`Generated report has been copied to clipboard. Please report the issue with this! Thank you for your cooperation!`,
|
// Logger(
|
||||||
LOG_LEVEL_NOTICE
|
// `Generated report has been copied to clipboard. Please report the issue with this! Thank you for your cooperation!`,
|
||||||
);
|
// LOG_LEVEL_NOTICE
|
||||||
|
// );
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
new Setting(paneEl).autoWireToggle("writeLogToTheFile");
|
new Setting(paneEl).autoWireToggle("writeLogToTheFile");
|
||||||
@@ -169,10 +187,10 @@ ${stringifyYaml({
|
|||||||
|
|
||||||
void addPanel(paneEl, "Scram Switches").then((paneEl) => {
|
void addPanel(paneEl, "Scram Switches").then((paneEl) => {
|
||||||
new Setting(paneEl).autoWireToggle("suspendFileWatching");
|
new Setting(paneEl).autoWireToggle("suspendFileWatching");
|
||||||
this.addOnSaved("suspendFileWatching", () => this.plugin.$$askReload());
|
this.addOnSaved("suspendFileWatching", () => this.services.appLifecycle.askRestart());
|
||||||
|
|
||||||
new Setting(paneEl).autoWireToggle("suspendParseReplicationResult");
|
new Setting(paneEl).autoWireToggle("suspendParseReplicationResult");
|
||||||
this.addOnSaved("suspendParseReplicationResult", () => this.plugin.$$askReload());
|
this.addOnSaved("suspendParseReplicationResult", () => this.services.appLifecycle.askRestart());
|
||||||
});
|
});
|
||||||
|
|
||||||
void addPanel(paneEl, "Recovery and Repair").then((paneEl) => {
|
void addPanel(paneEl, "Recovery and Repair").then((paneEl) => {
|
||||||
@@ -190,7 +208,7 @@ ${stringifyYaml({
|
|||||||
);
|
);
|
||||||
infoGroupEl.appendChild(
|
infoGroupEl.appendChild(
|
||||||
this.createEl(infoGroupEl, "div", {
|
this.createEl(infoGroupEl, "div", {
|
||||||
text: `Database: Modified: ${!fileOnDB ? `Missing:` : `${new Date(fileOnDB.mtime).toLocaleString()}, Size:${fileOnDB.size}`}`,
|
text: `Database: Modified: ${!fileOnDB ? `Missing:` : `${new Date(fileOnDB.mtime).toLocaleString()}, Size:${fileOnDB.size} (actual size:${readAsBlob(fileOnDB).size})`}`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -371,15 +389,16 @@ ${stringifyYaml({
|
|||||||
? await this.plugin.storageAccess.statHidden(path)
|
? await this.plugin.storageAccess.statHidden(path)
|
||||||
: false;
|
: false;
|
||||||
const fileOnStorage = stat != null ? stat : false;
|
const fileOnStorage = stat != null ? stat : false;
|
||||||
if (!(await this.plugin.$$isTargetFile(path))) return incProc();
|
if (!(await this.services.vault.isTargetFile(path))) return incProc();
|
||||||
const releaser = await semaphore.acquire(1);
|
const releaser = await semaphore.acquire(1);
|
||||||
if (fileOnStorage && this.plugin.$$isFileSizeExceeded(fileOnStorage.size))
|
if (fileOnStorage && this.services.vault.isFileSizeTooLarge(fileOnStorage.size))
|
||||||
return incProc();
|
return incProc();
|
||||||
try {
|
try {
|
||||||
const isHiddenFile = path.startsWith(".");
|
const isHiddenFile = path.startsWith(".");
|
||||||
const dbPath = isHiddenFile ? addPrefix(path, ICHeader) : path;
|
const dbPath = isHiddenFile ? addPrefix(path, ICHeader) : path;
|
||||||
const fileOnDB = await this.plugin.localDatabase.getDBEntry(dbPath);
|
const fileOnDB = await this.plugin.localDatabase.getDBEntry(dbPath);
|
||||||
if (fileOnDB && this.plugin.$$isFileSizeExceeded(fileOnDB.size)) return incProc();
|
if (fileOnDB && this.services.vault.isFileSizeTooLarge(fileOnDB.size))
|
||||||
|
return incProc();
|
||||||
|
|
||||||
if (!fileOnDB && fileOnStorage) {
|
if (!fileOnDB && fileOnStorage) {
|
||||||
Logger(`Compare: Not found on the local database: ${path}`, LOG_LEVEL_NOTICE);
|
Logger(`Compare: Not found on the local database: ${path}`, LOG_LEVEL_NOTICE);
|
||||||
@@ -423,7 +442,7 @@ ${stringifyYaml({
|
|||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
for await (const docName of this.plugin.localDatabase.findAllDocNames()) {
|
for await (const docName of this.plugin.localDatabase.findAllDocNames()) {
|
||||||
if (!docName.startsWith("f:")) {
|
if (!docName.startsWith("f:")) {
|
||||||
const idEncoded = await this.plugin.$$path2id(docName as FilePathWithPrefix);
|
const idEncoded = await this.services.path.path2id(docName as FilePathWithPrefix);
|
||||||
const doc = await this.plugin.localDatabase.getRaw(docName as DocumentID);
|
const doc = await this.plugin.localDatabase.getRaw(docName as DocumentID);
|
||||||
if (!doc) continue;
|
if (!doc) continue;
|
||||||
if (doc.type != "newnote" && doc.type != "plain") {
|
if (doc.type != "newnote" && doc.type != "plain") {
|
||||||
@@ -464,7 +483,7 @@ ${stringifyYaml({
|
|||||||
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.$$queueConflictCheckIfOpen(docName as FilePathWithPrefix);
|
await this.services.conflict.queueCheckForIfOpen(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);
|
||||||
@@ -499,7 +518,7 @@ ${stringifyYaml({
|
|||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
this.editingSettings.isConfigured = false;
|
this.editingSettings.isConfigured = false;
|
||||||
await this.saveAllDirtySettings();
|
await this.saveAllDirtySettings();
|
||||||
this.plugin.$$askReload();
|
this.services.appLifecycle.askRestart();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LocalDatabaseMaintenance } from "../../../features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
|
import { LocalDatabaseMaintenance } from "../../../features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
|
||||||
import { LOG_LEVEL_NOTICE, Logger } from "../../../lib/src/common/logger.ts";
|
import { LOG_LEVEL_NOTICE, Logger } from "../../../lib/src/common/logger.ts";
|
||||||
import { FLAGMD_REDFLAG, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3_HR } from "../../../lib/src/common/types.ts";
|
import { FlagFilesHumanReadable, FLAGMD_REDFLAG } from "../../../lib/src/common/types.ts";
|
||||||
import { fireAndForget } from "../../../lib/src/common/utils.ts";
|
import { fireAndForget } from "../../../lib/src/common/utils.ts";
|
||||||
import { LiveSyncCouchDBReplicator } from "../../../lib/src/replication/couchdb/LiveSyncReplicator.ts";
|
import { LiveSyncCouchDBReplicator } from "../../../lib/src/replication/couchdb/LiveSyncReplicator.ts";
|
||||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||||
@@ -32,7 +32,7 @@ export function paneMaintenance(
|
|||||||
(e) => {
|
(e) => {
|
||||||
e.addEventListener("click", () => {
|
e.addEventListener("click", () => {
|
||||||
fireAndForget(async () => {
|
fireAndForget(async () => {
|
||||||
await this.plugin.$$markRemoteResolved();
|
await this.services.remote.markResolved();
|
||||||
this.display();
|
this.display();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -59,7 +59,7 @@ export function paneMaintenance(
|
|||||||
(e) => {
|
(e) => {
|
||||||
e.addEventListener("click", () => {
|
e.addEventListener("click", () => {
|
||||||
fireAndForget(async () => {
|
fireAndForget(async () => {
|
||||||
await this.plugin.$$markRemoteUnlocked();
|
await this.services.remote.markUnlocked();
|
||||||
this.display();
|
this.display();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -78,7 +78,7 @@ export function paneMaintenance(
|
|||||||
.setDisabled(false)
|
.setDisabled(false)
|
||||||
.setWarning()
|
.setWarning()
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
await this.plugin.$$markRemoteLocked();
|
await this.services.remote.markLocked();
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.addOnUpdate(this.onlyOnCouchDBOrMinIO);
|
.addOnUpdate(this.onlyOnCouchDBOrMinIO);
|
||||||
@@ -93,7 +93,36 @@ export function paneMaintenance(
|
|||||||
.setWarning()
|
.setWarning()
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG, "");
|
await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG, "");
|
||||||
this.plugin.$$performRestart();
|
this.services.appLifecycle.performRestart();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
void addPanel(paneEl, "Reset Synchronisation information").then((paneEl) => {
|
||||||
|
new Setting(paneEl)
|
||||||
|
.setName("Reset Synchronisation on This Device")
|
||||||
|
.setDesc("Restore or reconstruct local database from remote.")
|
||||||
|
.addButton((button) =>
|
||||||
|
button
|
||||||
|
.setButtonText("Schedule and Restart")
|
||||||
|
.setCta()
|
||||||
|
.setDisabled(false)
|
||||||
|
.onClick(async () => {
|
||||||
|
await this.plugin.storageAccess.writeFileAuto(FlagFilesHumanReadable.FETCH_ALL, "");
|
||||||
|
this.services.appLifecycle.performRestart();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
new Setting(paneEl)
|
||||||
|
.setName("Overwrite Server Data with This Device's Files")
|
||||||
|
.setDesc("Rebuild local and remote database with local files.")
|
||||||
|
.addButton((button) =>
|
||||||
|
button
|
||||||
|
.setButtonText("Schedule and Restart")
|
||||||
|
.setCta()
|
||||||
|
.setDisabled(false)
|
||||||
|
.onClick(async () => {
|
||||||
|
await this.plugin.storageAccess.writeFileAuto(FlagFilesHumanReadable.REBUILD_ALL, "");
|
||||||
|
this.services.appLifecycle.performRestart();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -158,36 +187,40 @@ export function paneMaintenance(
|
|||||||
)
|
)
|
||||||
.addOnUpdate(this.onlyOnMinIO);
|
.addOnUpdate(this.onlyOnMinIO);
|
||||||
});
|
});
|
||||||
void addPanel(paneEl, "Garbage Collection (Beta)", (e) => e, this.onlyOnP2POrCouchDB).then((paneEl) => {
|
void addPanel(paneEl, "Garbage Collection (Beta2)", (e) => e, this.onlyOnP2POrCouchDB).then((paneEl) => {
|
||||||
new Setting(paneEl)
|
new Setting(paneEl)
|
||||||
.setName("Remove all orphaned chunks")
|
.setName("Scan garbage")
|
||||||
.setDesc("Remove all orphaned chunks from the local database.")
|
.setDesc("Scan for garbage chunks in the database.")
|
||||||
.addButton((button) =>
|
.addButton((button) =>
|
||||||
button
|
button
|
||||||
.setButtonText("Remove")
|
.setButtonText("Scan")
|
||||||
.setWarning()
|
// .setWarning()
|
||||||
.setDisabled(false)
|
.setDisabled(false)
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
await this.plugin
|
await this.plugin
|
||||||
.getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
|
.getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
|
||||||
?.removeUnusedChunks();
|
?.trackChanges(false, true);
|
||||||
})
|
})
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(paneEl)
|
|
||||||
.setName("Resurrect deleted chunks")
|
|
||||||
.setDesc(
|
|
||||||
"If you have deleted chunks before fully synchronised and missed some chunks, you possibly can resurrect them."
|
|
||||||
)
|
)
|
||||||
|
.addButton((button) =>
|
||||||
|
button.setButtonText("Rescan").onClick(async () => {
|
||||||
|
await this.plugin
|
||||||
|
.getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
|
||||||
|
?.trackChanges(true, true);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
new Setting(paneEl)
|
||||||
|
.setName("Collect garbage")
|
||||||
|
.setDesc("Remove all unused chunks from the local database.")
|
||||||
.addButton((button) =>
|
.addButton((button) =>
|
||||||
button
|
button
|
||||||
.setButtonText("Try resurrect")
|
.setButtonText("Collect")
|
||||||
.setWarning()
|
.setWarning()
|
||||||
.setDisabled(false)
|
.setDisabled(false)
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
await this.plugin
|
await this.plugin
|
||||||
.getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
|
.getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
|
||||||
?.resurrectChunks();
|
?.performGC(true);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
new Setting(paneEl)
|
new Setting(paneEl)
|
||||||
@@ -205,69 +238,42 @@ export function paneMaintenance(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
void addPanel(paneEl, "Rebuilding Operations (Local)").then((paneEl) => {
|
void addPanel(paneEl, "Garbage Collection (Old and Experimental)", (e) => e, this.onlyOnP2POrCouchDB).then(
|
||||||
new Setting(paneEl)
|
(paneEl) => {
|
||||||
.setName("Fetch from remote")
|
new Setting(paneEl)
|
||||||
.setDesc("Restore or reconstruct local database from remote.")
|
.setName("Remove all orphaned chunks")
|
||||||
.addButton((button) =>
|
.setDesc("Remove all orphaned chunks from the local database.")
|
||||||
button
|
.addButton((button) =>
|
||||||
.setButtonText("Fetch")
|
button
|
||||||
.setWarning()
|
.setButtonText("Remove")
|
||||||
.setDisabled(false)
|
.setWarning()
|
||||||
.onClick(async () => {
|
.setDisabled(false)
|
||||||
await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, "");
|
.onClick(async () => {
|
||||||
this.plugin.$$performRestart();
|
await this.plugin
|
||||||
})
|
.getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
|
||||||
)
|
?.removeUnusedChunks();
|
||||||
.addButton((button) =>
|
})
|
||||||
button
|
);
|
||||||
.setButtonText("Fetch w/o restarting")
|
|
||||||
.setWarning()
|
|
||||||
.setDisabled(false)
|
|
||||||
.onClick(async () => {
|
|
||||||
await this.rebuildDB("localOnly");
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(paneEl)
|
new Setting(paneEl)
|
||||||
.setName("Fetch rebuilt DB (Save local documents before)")
|
.setName("Resurrect deleted chunks")
|
||||||
.setDesc("Restore or reconstruct local database from remote database but use local chunks.")
|
.setDesc(
|
||||||
.addButton((button) =>
|
"If you have deleted chunks before fully synchronised and missed some chunks, you possibly can resurrect them."
|
||||||
button
|
)
|
||||||
.setButtonText("Save and Fetch")
|
.addButton((button) =>
|
||||||
.setWarning()
|
button
|
||||||
.setDisabled(false)
|
.setButtonText("Try resurrect")
|
||||||
.onClick(async () => {
|
.setWarning()
|
||||||
await this.rebuildDB("localOnlyWithChunks");
|
.setDisabled(false)
|
||||||
})
|
.onClick(async () => {
|
||||||
)
|
await this.plugin
|
||||||
.addOnUpdate(this.onlyOnCouchDB);
|
.getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
|
||||||
});
|
?.resurrectChunks();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
void addPanel(paneEl, "Total Overhaul", () => {}, this.onlyOnCouchDBOrMinIO).then((paneEl) => {
|
|
||||||
new Setting(paneEl)
|
|
||||||
.setName("Rebuild everything")
|
|
||||||
.setDesc("Rebuild local and remote database with local files.")
|
|
||||||
.addButton((button) =>
|
|
||||||
button
|
|
||||||
.setButtonText("Rebuild")
|
|
||||||
.setWarning()
|
|
||||||
.setDisabled(false)
|
|
||||||
.onClick(async () => {
|
|
||||||
await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, "");
|
|
||||||
this.plugin.$$performRestart();
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.addButton((button) =>
|
|
||||||
button
|
|
||||||
.setButtonText("Rebuild w/o restarting")
|
|
||||||
.setWarning()
|
|
||||||
.setDisabled(false)
|
|
||||||
.onClick(async () => {
|
|
||||||
await this.rebuildDB("rebuildBothByThisDevice");
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
void addPanel(paneEl, "Rebuilding Operations (Remote Only)", () => {}, this.onlyOnCouchDBOrMinIO).then((paneEl) => {
|
void addPanel(paneEl, "Rebuilding Operations (Remote Only)", () => {}, this.onlyOnCouchDBOrMinIO).then((paneEl) => {
|
||||||
new Setting(paneEl)
|
new Setting(paneEl)
|
||||||
.setName("Perform cleanup")
|
.setName("Perform cleanup")
|
||||||
@@ -366,8 +372,8 @@ export function paneMaintenance(
|
|||||||
.setWarning()
|
.setWarning()
|
||||||
.setDisabled(false)
|
.setDisabled(false)
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
await this.plugin.$$resetLocalDatabase();
|
await this.services.database.resetDatabase();
|
||||||
await this.plugin.$$initializeDatabase();
|
await this.services.databaseEvents.initialiseDatabase();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
|
|||||||
|
|
||||||
this.addOnSaved("additionalSuffixOfDatabaseName", async (key) => {
|
this.addOnSaved("additionalSuffixOfDatabaseName", async (key) => {
|
||||||
Logger("Suffix has been changed. Reopening database...", LOG_LEVEL_NOTICE);
|
Logger("Suffix has been changed. Reopening database...", LOG_LEVEL_NOTICE);
|
||||||
await this.plugin.$$initializeDatabase();
|
await this.services.databaseEvents.initialiseDatabase();
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(paneEl).autoWireDropDown("hashAlg", {
|
new Setting(paneEl).autoWireDropDown("hashAlg", {
|
||||||
@@ -82,6 +82,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
|
|||||||
void addPanel(paneEl, "Edge case addressing (Behaviour)").then((paneEl) => {
|
void addPanel(paneEl, "Edge case addressing (Behaviour)").then((paneEl) => {
|
||||||
new Setting(paneEl).autoWireToggle("doNotSuspendOnFetching");
|
new Setting(paneEl).autoWireToggle("doNotSuspendOnFetching");
|
||||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("doNotDeleteFolder");
|
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("doNotDeleteFolder");
|
||||||
|
new Setting(paneEl).autoWireToggle("processSizeMismatchedFiles");
|
||||||
});
|
});
|
||||||
|
|
||||||
void addPanel(paneEl, "Edge case addressing (Processing)").then((paneEl) => {
|
void addPanel(paneEl, "Edge case addressing (Processing)").then((paneEl) => {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ import type { PageFunctions } from "./SettingPane.ts";
|
|||||||
import { visibleOnly } from "./SettingPane.ts";
|
import { visibleOnly } from "./SettingPane.ts";
|
||||||
import { DEFAULT_SETTINGS } from "../../../lib/src/common/types.ts";
|
import { DEFAULT_SETTINGS } from "../../../lib/src/common/types.ts";
|
||||||
import { request } from "obsidian";
|
import { request } from "obsidian";
|
||||||
|
import { SetupManager, UserMode } from "../SetupManager.ts";
|
||||||
export function paneSetup(
|
export function paneSetup(
|
||||||
this: ObsidianLiveSyncSettingTab,
|
this: ObsidianLiveSyncSettingTab,
|
||||||
paneEl: HTMLElement,
|
paneEl: HTMLElement,
|
||||||
@@ -30,11 +31,13 @@ export function paneSetup(
|
|||||||
});
|
});
|
||||||
|
|
||||||
new Setting(paneEl)
|
new Setting(paneEl)
|
||||||
.setName($msg("obsidianLiveSyncSettingTab.nameManualSetup"))
|
.setName("Rerun Onboarding Wizard")
|
||||||
.setDesc($msg("obsidianLiveSyncSettingTab.descManualSetup"))
|
.setDesc("Rerun the onboarding wizard to set up Self-hosted LiveSync again.")
|
||||||
.addButton((text) => {
|
.addButton((text) => {
|
||||||
text.setButtonText($msg("obsidianLiveSyncSettingTab.btnStart")).onClick(async () => {
|
text.setButtonText("Rerun Wizard").onClick(async () => {
|
||||||
await this.enableMinimalSetup();
|
const setupManager = this.plugin.getModule(SetupManager);
|
||||||
|
await setupManager.onOnboard(UserMode.ExistingUser);
|
||||||
|
// await this.plugin.moduleSetupObsidian.onBoardingWizard(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,7 +49,7 @@ export function paneSetup(
|
|||||||
text.setButtonText($msg("obsidianLiveSyncSettingTab.btnEnable")).onClick(async () => {
|
text.setButtonText($msg("obsidianLiveSyncSettingTab.btnEnable")).onClick(async () => {
|
||||||
this.editingSettings.isConfigured = true;
|
this.editingSettings.isConfigured = true;
|
||||||
await this.saveAllDirtySettings();
|
await this.saveAllDirtySettings();
|
||||||
this.plugin.$$askReload();
|
this.services.appLifecycle.askRestart();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -91,10 +94,10 @@ export function paneSetup(
|
|||||||
this.editingSettings = { ...this.editingSettings, ...DEFAULT_SETTINGS };
|
this.editingSettings = { ...this.editingSettings, ...DEFAULT_SETTINGS };
|
||||||
await this.saveAllDirtySettings();
|
await this.saveAllDirtySettings();
|
||||||
this.plugin.settings = { ...DEFAULT_SETTINGS };
|
this.plugin.settings = { ...DEFAULT_SETTINGS };
|
||||||
await this.plugin.$$saveSettingData();
|
await this.services.setting.saveSettingData();
|
||||||
await this.plugin.$$resetLocalDatabase();
|
await this.services.database.resetDatabase();
|
||||||
// await this.plugin.initializeDatabase();
|
// await this.plugin.initializeDatabase();
|
||||||
this.plugin.$$askReload();
|
this.services.appLifecycle.askRestart();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setWarning();
|
.setWarning();
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
import { Logger } from "../../../lib/src/common/logger.ts";
|
import { Logger } from "../../../lib/src/common/logger.ts";
|
||||||
import { $msg } from "../../../lib/src/common/i18n.ts";
|
import { $msg } from "../../../lib/src/common/i18n.ts";
|
||||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||||
import { fireAndForget } from "octagonal-wheels/promises";
|
|
||||||
import { EVENT_REQUEST_COPY_SETUP_URI, eventHub } from "../../../common/events.ts";
|
import { EVENT_REQUEST_COPY_SETUP_URI, eventHub } from "../../../common/events.ts";
|
||||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||||
import type { PageFunctions } from "./SettingPane.ts";
|
import type { PageFunctions } from "./SettingPane.ts";
|
||||||
@@ -17,30 +16,6 @@ export function paneSyncSettings(
|
|||||||
paneEl: HTMLElement,
|
paneEl: HTMLElement,
|
||||||
{ addPanel, addPane }: PageFunctions
|
{ addPanel, addPane }: PageFunctions
|
||||||
): void {
|
): void {
|
||||||
if (this.editingSettings.versionUpFlash != "") {
|
|
||||||
const c = this.createEl(
|
|
||||||
paneEl,
|
|
||||||
"div",
|
|
||||||
{
|
|
||||||
text: this.editingSettings.versionUpFlash,
|
|
||||||
cls: "op-warn sls-setting-hidden",
|
|
||||||
},
|
|
||||||
(el) => {
|
|
||||||
this.createEl(el, "button", { text: $msg("obsidianLiveSyncSettingTab.btnGotItAndUpdated") }, (e) => {
|
|
||||||
e.addClass("mod-cta");
|
|
||||||
e.addEventListener("click", () => {
|
|
||||||
fireAndForget(async () => {
|
|
||||||
this.editingSettings.versionUpFlash = "";
|
|
||||||
await this.saveAllDirtySettings();
|
|
||||||
c.remove();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
visibleOnly(() => !this.isConfiguredAs("versionUpFlash", ""))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.createEl(paneEl, "div", {
|
this.createEl(paneEl, "div", {
|
||||||
text: $msg("obsidianLiveSyncSettingTab.msgSelectAndApplyPreset"),
|
text: $msg("obsidianLiveSyncSettingTab.msgSelectAndApplyPreset"),
|
||||||
cls: "wizardOnly",
|
cls: "wizardOnly",
|
||||||
@@ -130,7 +105,7 @@ export function paneSyncSettings(
|
|||||||
if (!this.editingSettings.isConfigured) {
|
if (!this.editingSettings.isConfigured) {
|
||||||
this.editingSettings.isConfigured = true;
|
this.editingSettings.isConfigured = true;
|
||||||
await this.saveAllDirtySettings();
|
await this.saveAllDirtySettings();
|
||||||
await this.plugin.$$realizeSettingSyncMode();
|
await this.services.setting.realiseSetting();
|
||||||
await this.rebuildDB("localOnly");
|
await this.rebuildDB("localOnly");
|
||||||
// this.resetEditingSettings();
|
// this.resetEditingSettings();
|
||||||
if (
|
if (
|
||||||
@@ -149,13 +124,13 @@ export function paneSyncSettings(
|
|||||||
await this.confirmRebuild();
|
await this.confirmRebuild();
|
||||||
} else {
|
} else {
|
||||||
await this.saveAllDirtySettings();
|
await this.saveAllDirtySettings();
|
||||||
await this.plugin.$$realizeSettingSyncMode();
|
await this.services.setting.realiseSetting();
|
||||||
this.plugin.$$askReload();
|
this.services.appLifecycle.askRestart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.saveAllDirtySettings();
|
await this.saveAllDirtySettings();
|
||||||
await this.plugin.$$realizeSettingSyncMode();
|
await this.services.setting.realiseSetting();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -194,7 +169,7 @@ export function paneSyncSettings(
|
|||||||
}
|
}
|
||||||
await this.saveSettings(["liveSync", "periodicReplication"]);
|
await this.saveSettings(["liveSync", "periodicReplication"]);
|
||||||
|
|
||||||
await this.plugin.$$realizeSettingSyncMode();
|
await this.services.setting.realiseSetting();
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(paneEl)
|
new Setting(paneEl)
|
||||||
@@ -314,21 +289,21 @@ export function paneSyncSettings(
|
|||||||
button.setButtonText("Merge").onClick(async () => {
|
button.setButtonText("Merge").onClick(async () => {
|
||||||
this.closeSetting();
|
this.closeSetting();
|
||||||
// this.resetEditingSettings();
|
// this.resetEditingSettings();
|
||||||
await this.plugin.$anyConfigureOptionalSyncFeature("MERGE");
|
await this.services.setting.enableOptionalFeature("MERGE");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.addButton((button) => {
|
.addButton((button) => {
|
||||||
button.setButtonText("Fetch").onClick(async () => {
|
button.setButtonText("Fetch").onClick(async () => {
|
||||||
this.closeSetting();
|
this.closeSetting();
|
||||||
// this.resetEditingSettings();
|
// this.resetEditingSettings();
|
||||||
await this.plugin.$anyConfigureOptionalSyncFeature("FETCH");
|
await this.services.setting.enableOptionalFeature("FETCH");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.addButton((button) => {
|
.addButton((button) => {
|
||||||
button.setButtonText("Overwrite").onClick(async () => {
|
button.setButtonText("Overwrite").onClick(async () => {
|
||||||
this.closeSetting();
|
this.closeSetting();
|
||||||
// this.resetEditingSettings();
|
// this.resetEditingSettings();
|
||||||
await this.plugin.$anyConfigureOptionalSyncFeature("OVERWRITE");
|
await this.services.setting.enableOptionalFeature("OVERWRITE");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
54
src/modules/features/SettingDialogue/SveltePanel.ts
Normal file
54
src/modules/features/SettingDialogue/SveltePanel.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { mount, type Component, unmount } from "svelte";
|
||||||
|
import { type Writable, writable, get } from "svelte/store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props passed to Svelte panels, containing a writable port
|
||||||
|
* to communicate with the panel
|
||||||
|
*/
|
||||||
|
export type SveltePanelProps<T = any> = {
|
||||||
|
port: Writable<T | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to manage a Svelte panel within Obsidian
|
||||||
|
* Especially useful for settings panels
|
||||||
|
*/
|
||||||
|
export class SveltePanel<T = any> {
|
||||||
|
private _mountedComponent: ReturnType<typeof mount>;
|
||||||
|
private _componentValue = writable<T | undefined>(undefined);
|
||||||
|
/**
|
||||||
|
* Creates a Svelte panel instance
|
||||||
|
* @param component Component to mount
|
||||||
|
* @param mountTo HTMLElement to mount the component to
|
||||||
|
* @param valueStore Optional writable store to bind to the component's port, if not provided a new one will be created
|
||||||
|
* @returns The SveltePanel instance
|
||||||
|
*/
|
||||||
|
constructor(component: Component<SveltePanelProps<T>>, mountTo: HTMLElement, valueStore?: Writable<T>) {
|
||||||
|
this._componentValue = valueStore ?? writable<T | undefined>(undefined);
|
||||||
|
this._mountedComponent = mount(component, {
|
||||||
|
target: mountTo,
|
||||||
|
props: {
|
||||||
|
port: this._componentValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Destroys the Svelte panel instance by unmounting the component
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
if (this._mountedComponent) {
|
||||||
|
void unmount(this._mountedComponent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets or sets the current value of the component's port
|
||||||
|
*/
|
||||||
|
get componentValue() {
|
||||||
|
return get(this._componentValue);
|
||||||
|
}
|
||||||
|
set componentValue(value: T | undefined) {
|
||||||
|
this._componentValue.set(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/modules/features/SettingDialogue/settingUtils.ts
Normal file
78
src/modules/features/SettingDialogue/settingUtils.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { escapeStringToHTML } from "octagonal-wheels/string";
|
||||||
|
import { E2EEAlgorithmNames, type ObsidianLiveSyncSettings } from "../../../lib/src/common/types";
|
||||||
|
import {
|
||||||
|
pickCouchDBSyncSettings,
|
||||||
|
pickBucketSyncSettings,
|
||||||
|
pickP2PSyncSettings,
|
||||||
|
pickEncryptionSettings,
|
||||||
|
} from "../../../lib/src/common/utils";
|
||||||
|
import { getConfig, type AllSettingItemKey } from "./settingConstants";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a summary of P2P configuration settings
|
||||||
|
* @param setting Settings object
|
||||||
|
* @param additional Additional summary information to include
|
||||||
|
* @param showAdvanced Whether to include advanced settings
|
||||||
|
* @returns Summary object
|
||||||
|
*/
|
||||||
|
export function getP2PConfigSummary(
|
||||||
|
setting: ObsidianLiveSyncSettings,
|
||||||
|
additional: Record<string, string> = {},
|
||||||
|
showAdvanced = false
|
||||||
|
) {
|
||||||
|
const settingTable: Partial<ObsidianLiveSyncSettings> = pickP2PSyncSettings(setting);
|
||||||
|
return { ...getSummaryFromPartialSettings({ ...settingTable }, showAdvanced), ...additional };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Generates a summary of Object Storage configuration settings
|
||||||
|
* @param setting Settings object
|
||||||
|
* @param showAdvanced Whether to include advanced settings
|
||||||
|
* @returns Summary object
|
||||||
|
*/
|
||||||
|
export function getBucketConfigSummary(setting: ObsidianLiveSyncSettings, showAdvanced = false) {
|
||||||
|
const settingTable: Partial<ObsidianLiveSyncSettings> = pickBucketSyncSettings(setting);
|
||||||
|
return getSummaryFromPartialSettings(settingTable, showAdvanced);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Generates a summary of CouchDB configuration settings
|
||||||
|
* @param setting Settings object
|
||||||
|
* @param showAdvanced Whether to include advanced settings
|
||||||
|
* @returns Summary object
|
||||||
|
*/
|
||||||
|
export function getCouchDBConfigSummary(setting: ObsidianLiveSyncSettings, showAdvanced = false) {
|
||||||
|
const settingTable: Partial<ObsidianLiveSyncSettings> = pickCouchDBSyncSettings(setting);
|
||||||
|
return getSummaryFromPartialSettings(settingTable, showAdvanced);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a summary of E2EE configuration settings
|
||||||
|
* @param setting Settings object
|
||||||
|
* @param showAdvanced Whether to include advanced settings
|
||||||
|
* @returns Summary object
|
||||||
|
*/
|
||||||
|
export function getE2EEConfigSummary(setting: ObsidianLiveSyncSettings, showAdvanced = false) {
|
||||||
|
const settingTable: Partial<ObsidianLiveSyncSettings> = pickEncryptionSettings(setting);
|
||||||
|
return getSummaryFromPartialSettings(settingTable, showAdvanced);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts partial settings into a summary object
|
||||||
|
* @param setting Partial settings object
|
||||||
|
* @param showAdvanced Whether to include advanced settings
|
||||||
|
* @returns Summary object
|
||||||
|
*/
|
||||||
|
export function getSummaryFromPartialSettings(setting: Partial<ObsidianLiveSyncSettings>, showAdvanced = false) {
|
||||||
|
const outputSummary: Record<string, string> = {};
|
||||||
|
for (const key of Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[]) {
|
||||||
|
const config = getConfig(key as AllSettingItemKey);
|
||||||
|
if (!config) continue;
|
||||||
|
if (config.isAdvanced && !showAdvanced) continue;
|
||||||
|
const value =
|
||||||
|
key != "E2EEAlgorithm"
|
||||||
|
? `${setting[key]}`
|
||||||
|
: E2EEAlgorithmNames[`${setting[key]}` as keyof typeof E2EEAlgorithmNames];
|
||||||
|
const displayValue = config.isHidden ? "•".repeat(value.length) : escapeStringToHTML(value);
|
||||||
|
outputSummary[config.name] = displayValue;
|
||||||
|
}
|
||||||
|
return outputSummary;
|
||||||
|
}
|
||||||
274
src/modules/features/SettingDialogue/utilFixCouchDBSetting.ts
Normal file
274
src/modules/features/SettingDialogue/utilFixCouchDBSetting.ts
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import { requestToCouchDBWithCredentials } from "../../../common/utils";
|
||||||
|
import { $msg } from "../../../lib/src/common/i18n";
|
||||||
|
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, Logger } from "../../../lib/src/common/logger";
|
||||||
|
import type { ObsidianLiveSyncSettings } from "../../../lib/src/common/types";
|
||||||
|
import { fireAndForget, parseHeaderValues } from "../../../lib/src/common/utils";
|
||||||
|
import { isCloudantURI } from "../../../lib/src/pouchdb/utils_couchdb";
|
||||||
|
import { generateCredentialObject } from "../../../lib/src/replication/httplib";
|
||||||
|
|
||||||
|
export const checkConfig = async (
|
||||||
|
checkResultDiv: HTMLDivElement | undefined,
|
||||||
|
editingSettings: ObsidianLiveSyncSettings
|
||||||
|
) => {
|
||||||
|
Logger($msg("obsidianLiveSyncSettingTab.logCheckingDbConfig"), LOG_LEVEL_INFO);
|
||||||
|
let isSuccessful = true;
|
||||||
|
const emptyDiv = createDiv();
|
||||||
|
emptyDiv.innerHTML = "<span></span>";
|
||||||
|
checkResultDiv?.replaceChildren(...[emptyDiv]);
|
||||||
|
const addResult = (msg: string, classes?: string[]) => {
|
||||||
|
const tmpDiv = createDiv();
|
||||||
|
tmpDiv.addClass("ob-btn-config-fix");
|
||||||
|
if (classes) {
|
||||||
|
tmpDiv.addClasses(classes);
|
||||||
|
}
|
||||||
|
tmpDiv.innerHTML = `${msg}`;
|
||||||
|
checkResultDiv?.appendChild(tmpDiv);
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
if (isCloudantURI(editingSettings.couchDB_URI)) {
|
||||||
|
Logger($msg("obsidianLiveSyncSettingTab.logCannotUseCloudant"), LOG_LEVEL_NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Tip: Add log for cloudant as Logger($msg("obsidianLiveSyncSettingTab.logServerConfigurationCheck"));
|
||||||
|
const customHeaders = parseHeaderValues(editingSettings.couchDB_CustomHeaders);
|
||||||
|
const credential = generateCredentialObject(editingSettings);
|
||||||
|
const r = await requestToCouchDBWithCredentials(
|
||||||
|
editingSettings.couchDB_URI,
|
||||||
|
credential,
|
||||||
|
window.origin,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
customHeaders
|
||||||
|
);
|
||||||
|
const responseConfig = r.json;
|
||||||
|
|
||||||
|
const addConfigFixButton = (title: string, key: string, value: string) => {
|
||||||
|
if (!checkResultDiv) return;
|
||||||
|
const tmpDiv = createDiv();
|
||||||
|
tmpDiv.addClass("ob-btn-config-fix");
|
||||||
|
tmpDiv.innerHTML = `<label>${title}</label><button>${$msg("obsidianLiveSyncSettingTab.btnFix")}</button>`;
|
||||||
|
const x = checkResultDiv.appendChild(tmpDiv);
|
||||||
|
x.querySelector("button")?.addEventListener("click", () => {
|
||||||
|
fireAndForget(async () => {
|
||||||
|
Logger($msg("obsidianLiveSyncSettingTab.logCouchDbConfigSet", { title, key, value }));
|
||||||
|
const res = await requestToCouchDBWithCredentials(
|
||||||
|
editingSettings.couchDB_URI,
|
||||||
|
credential,
|
||||||
|
undefined,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
undefined,
|
||||||
|
customHeaders
|
||||||
|
);
|
||||||
|
if (res.status == 200) {
|
||||||
|
Logger($msg("obsidianLiveSyncSettingTab.logCouchDbConfigUpdated", { title }), LOG_LEVEL_NOTICE);
|
||||||
|
checkResultDiv.removeChild(x);
|
||||||
|
await checkConfig(checkResultDiv, editingSettings);
|
||||||
|
} else {
|
||||||
|
Logger($msg("obsidianLiveSyncSettingTab.logCouchDbConfigFail", { title }), LOG_LEVEL_NOTICE);
|
||||||
|
Logger(res.text, LOG_LEVEL_VERBOSE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.msgNotice"), ["ob-btn-config-head"]);
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.msgIfConfigNotPersistent"), ["ob-btn-config-info"]);
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.msgConfigCheck"), ["ob-btn-config-head"]);
|
||||||
|
|
||||||
|
const serverBanner = r.headers["server"] ?? r.headers["Server"] ?? "unknown";
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.serverVersion", { info: serverBanner }));
|
||||||
|
const versionMatch = serverBanner.match(/CouchDB(\/([0-9.]+))?/);
|
||||||
|
const versionStr = versionMatch ? versionMatch[2] : "0.0.0";
|
||||||
|
const versionParts = `${versionStr}.0.0.0`.split(".");
|
||||||
|
// Compare version string with the target version.
|
||||||
|
// version must be a string like "3.2.1" or "3.10.2", and must be two or three parts.
|
||||||
|
function isGreaterThanOrEqual(version: string) {
|
||||||
|
const targetParts = version.split(".");
|
||||||
|
for (let i = 0; i < targetParts.length; i++) {
|
||||||
|
// compare as number if possible (so 3.10 > 3.2, 3.10.1b > 3.10.1a)
|
||||||
|
const result = versionParts[i].localeCompare(targetParts[i], undefined, { numeric: true });
|
||||||
|
if (result > 0) return true;
|
||||||
|
if (result < 0) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Admin check
|
||||||
|
// for database creation and deletion
|
||||||
|
if (!(editingSettings.couchDB_USER in responseConfig.admins)) {
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.warnNoAdmin"));
|
||||||
|
} else {
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.okAdminPrivileges"));
|
||||||
|
}
|
||||||
|
if (isGreaterThanOrEqual("3.2.0")) {
|
||||||
|
// HTTP user-authorization check
|
||||||
|
if (responseConfig?.chttpd?.require_valid_user != "true") {
|
||||||
|
isSuccessful = false;
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.errRequireValidUser"));
|
||||||
|
addConfigFixButton(
|
||||||
|
$msg("obsidianLiveSyncSettingTab.msgSetRequireValidUser"),
|
||||||
|
"chttpd/require_valid_user",
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.okRequireValidUser"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (responseConfig?.chttpd_auth?.require_valid_user != "true") {
|
||||||
|
isSuccessful = false;
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.errRequireValidUserAuth"));
|
||||||
|
addConfigFixButton(
|
||||||
|
$msg("obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth"),
|
||||||
|
"chttpd_auth/require_valid_user",
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.okRequireValidUserAuth"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// HTTPD check
|
||||||
|
// Check Authentication header
|
||||||
|
if (!responseConfig?.httpd["WWW-Authenticate"]) {
|
||||||
|
isSuccessful = false;
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.errMissingWwwAuth"));
|
||||||
|
addConfigFixButton(
|
||||||
|
$msg("obsidianLiveSyncSettingTab.msgSetWwwAuth"),
|
||||||
|
"httpd/WWW-Authenticate",
|
||||||
|
'Basic realm="couchdb"'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.okWwwAuth"));
|
||||||
|
}
|
||||||
|
if (isGreaterThanOrEqual("3.2.0")) {
|
||||||
|
if (responseConfig?.chttpd?.enable_cors != "true") {
|
||||||
|
isSuccessful = false;
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.errEnableCorsChttpd"));
|
||||||
|
addConfigFixButton(
|
||||||
|
$msg("obsidianLiveSyncSettingTab.msgEnableCorsChttpd"),
|
||||||
|
"chttpd/enable_cors",
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.okEnableCorsChttpd"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (responseConfig?.httpd?.enable_cors != "true") {
|
||||||
|
isSuccessful = false;
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.errEnableCors"));
|
||||||
|
addConfigFixButton($msg("obsidianLiveSyncSettingTab.msgEnableCors"), "httpd/enable_cors", "true");
|
||||||
|
} else {
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.okEnableCors"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the server is not cloudant, configure request size
|
||||||
|
if (!isCloudantURI(editingSettings.couchDB_URI)) {
|
||||||
|
// REQUEST SIZE
|
||||||
|
if (Number(responseConfig?.chttpd?.max_http_request_size ?? 0) < 4294967296) {
|
||||||
|
isSuccessful = false;
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.errMaxRequestSize"));
|
||||||
|
addConfigFixButton(
|
||||||
|
$msg("obsidianLiveSyncSettingTab.msgSetMaxRequestSize"),
|
||||||
|
"chttpd/max_http_request_size",
|
||||||
|
"4294967296"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.okMaxRequestSize"));
|
||||||
|
}
|
||||||
|
if (Number(responseConfig?.couchdb?.max_document_size ?? 0) < 50000000) {
|
||||||
|
isSuccessful = false;
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.errMaxDocumentSize"));
|
||||||
|
addConfigFixButton(
|
||||||
|
$msg("obsidianLiveSyncSettingTab.msgSetMaxDocSize"),
|
||||||
|
"couchdb/max_document_size",
|
||||||
|
"50000000"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.okMaxDocumentSize"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// CORS check
|
||||||
|
// checking connectivity for mobile
|
||||||
|
if (responseConfig?.cors?.credentials != "true") {
|
||||||
|
isSuccessful = false;
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.errCorsCredentials"));
|
||||||
|
addConfigFixButton($msg("obsidianLiveSyncSettingTab.msgSetCorsCredentials"), "cors/credentials", "true");
|
||||||
|
} else {
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.okCorsCredentials"));
|
||||||
|
}
|
||||||
|
const ConfiguredOrigins = ((responseConfig?.cors?.origins ?? "") + "").split(",");
|
||||||
|
if (
|
||||||
|
responseConfig?.cors?.origins == "*" ||
|
||||||
|
(ConfiguredOrigins.indexOf("app://obsidian.md") !== -1 &&
|
||||||
|
ConfiguredOrigins.indexOf("capacitor://localhost") !== -1 &&
|
||||||
|
ConfiguredOrigins.indexOf("http://localhost") !== -1)
|
||||||
|
) {
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.okCorsOrigins"));
|
||||||
|
} else {
|
||||||
|
const fixedValue = [
|
||||||
|
...new Set([
|
||||||
|
...ConfiguredOrigins.map((e) => e.trim()),
|
||||||
|
"app://obsidian.md",
|
||||||
|
"capacitor://localhost",
|
||||||
|
"http://localhost",
|
||||||
|
]),
|
||||||
|
].join(",");
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.errCorsOrigins"));
|
||||||
|
addConfigFixButton($msg("obsidianLiveSyncSettingTab.msgSetCorsOrigins"), "cors/origins", fixedValue);
|
||||||
|
isSuccessful = false;
|
||||||
|
}
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.msgConnectionCheck"), ["ob-btn-config-head"]);
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.msgCurrentOrigin", { origin: window.location.origin }));
|
||||||
|
|
||||||
|
// Request header check
|
||||||
|
const origins = ["app://obsidian.md", "capacitor://localhost", "http://localhost"];
|
||||||
|
for (const org of origins) {
|
||||||
|
const rr = await requestToCouchDBWithCredentials(
|
||||||
|
editingSettings.couchDB_URI,
|
||||||
|
credential,
|
||||||
|
org,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
customHeaders
|
||||||
|
);
|
||||||
|
const responseHeaders = Object.fromEntries(
|
||||||
|
Object.entries(rr.headers).map((e) => {
|
||||||
|
e[0] = `${e[0]}`.toLowerCase();
|
||||||
|
return e;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.msgOriginCheck", { org }));
|
||||||
|
if (responseHeaders["access-control-allow-credentials"] != "true") {
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials"));
|
||||||
|
isSuccessful = false;
|
||||||
|
} else {
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin"));
|
||||||
|
}
|
||||||
|
if (responseHeaders["access-control-allow-origin"] != org) {
|
||||||
|
addResult(
|
||||||
|
$msg("obsidianLiveSyncSettingTab.warnCorsOriginUnmatched", {
|
||||||
|
from: origin,
|
||||||
|
to: responseHeaders["access-control-allow-origin"],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.okCorsOriginMatched"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.msgDone"), ["ob-btn-config-head"]);
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.msgConnectionProxyNote"), ["ob-btn-config-info"]);
|
||||||
|
Logger($msg("obsidianLiveSyncSettingTab.logCheckingConfigDone"), LOG_LEVEL_INFO);
|
||||||
|
} catch (ex: any) {
|
||||||
|
if (ex?.status == 401) {
|
||||||
|
isSuccessful = false;
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.errAccessForbidden"));
|
||||||
|
addResult($msg("obsidianLiveSyncSettingTab.errCannotContinueTest"));
|
||||||
|
Logger($msg("obsidianLiveSyncSettingTab.logCheckingConfigDone"), LOG_LEVEL_INFO);
|
||||||
|
} else {
|
||||||
|
Logger($msg("obsidianLiveSyncSettingTab.logCheckingConfigFailed"), LOG_LEVEL_NOTICE);
|
||||||
|
Logger(ex);
|
||||||
|
isSuccessful = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isSuccessful;
|
||||||
|
};
|
||||||
378
src/modules/features/SetupManager.ts
Normal file
378
src/modules/features/SetupManager.ts
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
import {
|
||||||
|
type ObsidianLiveSyncSettings,
|
||||||
|
DEFAULT_SETTINGS,
|
||||||
|
LOG_LEVEL_NOTICE,
|
||||||
|
LOG_LEVEL_VERBOSE,
|
||||||
|
REMOTE_COUCHDB,
|
||||||
|
REMOTE_MINIO,
|
||||||
|
REMOTE_P2P,
|
||||||
|
} from "../../lib/src/common/types.ts";
|
||||||
|
import { generatePatchObj, isObjectDifferent } from "../../lib/src/common/utils.ts";
|
||||||
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
|
import { SvelteDialogManager } from "./SetupWizard/ObsidianSvelteDialog.ts";
|
||||||
|
import Intro from "./SetupWizard/dialogs/Intro.svelte";
|
||||||
|
import SelectMethodNewUser from "./SetupWizard/dialogs/SelectMethodNewUser.svelte";
|
||||||
|
import SelectMethodExisting from "./SetupWizard/dialogs/SelectMethodExisting.svelte";
|
||||||
|
import ScanQRCode from "./SetupWizard/dialogs/ScanQRCode.svelte";
|
||||||
|
import UseSetupURI from "./SetupWizard/dialogs/UseSetupURI.svelte";
|
||||||
|
import OutroNewUser from "./SetupWizard/dialogs/OutroNewUser.svelte";
|
||||||
|
import OutroExistingUser from "./SetupWizard/dialogs/OutroExistingUser.svelte";
|
||||||
|
import OutroAskUserMode from "./SetupWizard/dialogs/OutroAskUserMode.svelte";
|
||||||
|
import SetupRemote from "./SetupWizard/dialogs/SetupRemote.svelte";
|
||||||
|
import SetupRemoteCouchDB from "./SetupWizard/dialogs/SetupRemoteCouchDB.svelte";
|
||||||
|
import SetupRemoteBucket from "./SetupWizard/dialogs/SetupRemoteBucket.svelte";
|
||||||
|
import SetupRemoteP2P from "./SetupWizard/dialogs/SetupRemoteP2P.svelte";
|
||||||
|
import SetupRemoteE2EE from "./SetupWizard/dialogs/SetupRemoteE2EE.svelte";
|
||||||
|
import { decodeSettingsFromQRCodeData } from "../../lib/src/API/processSetting.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User modes for onboarding and setup
|
||||||
|
*/
|
||||||
|
export const enum UserMode {
|
||||||
|
/**
|
||||||
|
* New User Mode - for users who are new to the plugin
|
||||||
|
*/
|
||||||
|
NewUser = "new-user",
|
||||||
|
/**
|
||||||
|
* Existing User Mode - for users who have used the plugin before, or just configuring again
|
||||||
|
*/
|
||||||
|
ExistingUser = "existing-user",
|
||||||
|
/**
|
||||||
|
* Unknown User Mode - for cases where the user mode is not determined
|
||||||
|
*/
|
||||||
|
Unknown = "unknown",
|
||||||
|
/**
|
||||||
|
* Update User Mode - for users who are updating configuration. May be `existing-user` as well, but possibly they want to treat it differently.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
|
||||||
|
Update = "unknown", // Alias for Unknown for better readability
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup Manager to handle onboarding and configuration setup
|
||||||
|
*/
|
||||||
|
export class SetupManager extends AbstractObsidianModule {
|
||||||
|
/**
|
||||||
|
* Dialog manager for handling Svelte dialogs
|
||||||
|
*/
|
||||||
|
private dialogManager: SvelteDialogManager = new SvelteDialogManager(this.plugin);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the onboarding process
|
||||||
|
* @returns Promise that resolves to true if onboarding completed successfully, false otherwise
|
||||||
|
*/
|
||||||
|
async startOnBoarding(): Promise<boolean> {
|
||||||
|
const isUserNewOrExisting = await this.dialogManager.openWithExplicitCancel(Intro);
|
||||||
|
if (isUserNewOrExisting === "new-user") {
|
||||||
|
await this.onOnboard(UserMode.NewUser);
|
||||||
|
} else if (isUserNewOrExisting === "existing-user") {
|
||||||
|
await this.onOnboard(UserMode.ExistingUser);
|
||||||
|
} else if (isUserNewOrExisting === "cancelled") {
|
||||||
|
this._log("Onboarding cancelled by user.", LOG_LEVEL_NOTICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the onboarding process based on user mode
|
||||||
|
* @param userMode
|
||||||
|
* @returns Promise that resolves to true if onboarding completed successfully, false otherwise
|
||||||
|
*/
|
||||||
|
async onOnboard(userMode: UserMode): Promise<boolean> {
|
||||||
|
const originalSetting = userMode === UserMode.NewUser ? DEFAULT_SETTINGS : this.core.settings;
|
||||||
|
if (userMode === UserMode.NewUser) {
|
||||||
|
//Ask how to apply initial setup
|
||||||
|
const method = await this.dialogManager.openWithExplicitCancel(SelectMethodNewUser);
|
||||||
|
if (method === "use-setup-uri") {
|
||||||
|
await this.onUseSetupURI(userMode);
|
||||||
|
} else if (method === "configure-manually") {
|
||||||
|
await this.onConfigureManually(originalSetting, userMode);
|
||||||
|
} else if (method === "cancelled") {
|
||||||
|
this._log("Onboarding cancelled by user.", LOG_LEVEL_NOTICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (userMode === UserMode.ExistingUser) {
|
||||||
|
const method = await this.dialogManager.openWithExplicitCancel(SelectMethodExisting);
|
||||||
|
if (method === "use-setup-uri") {
|
||||||
|
await this.onUseSetupURI(userMode);
|
||||||
|
} else if (method === "configure-manually") {
|
||||||
|
await this.onConfigureManually(originalSetting, userMode);
|
||||||
|
} else if (method === "scan-qr-code") {
|
||||||
|
await this.onPromptQRCodeInstruction();
|
||||||
|
} else if (method === "cancelled") {
|
||||||
|
this._log("Onboarding cancelled by user.", LOG_LEVEL_NOTICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles setup using a setup URI
|
||||||
|
* @param userMode
|
||||||
|
* @param setupURI
|
||||||
|
* @returns Promise that resolves to true if onboarding completed successfully, false otherwise
|
||||||
|
*/
|
||||||
|
async onUseSetupURI(userMode: UserMode, setupURI: string = ""): Promise<boolean> {
|
||||||
|
const newSetting = await this.dialogManager.openWithExplicitCancel(UseSetupURI, setupURI);
|
||||||
|
if (newSetting === "cancelled") {
|
||||||
|
this._log("Setup URI dialog cancelled.", LOG_LEVEL_NOTICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this._log("Setup URI dialog closed.", LOG_LEVEL_VERBOSE);
|
||||||
|
return await this.onConfirmApplySettingsFromWizard(newSetting, userMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles manual setup for CouchDB
|
||||||
|
* @param userMode
|
||||||
|
* @param currentSetting
|
||||||
|
* @param activate Whether to activate the CouchDB as remote type
|
||||||
|
* @returns Promise that resolves to true if setup completed successfully, false otherwise
|
||||||
|
*/
|
||||||
|
async onCouchDBManualSetup(
|
||||||
|
userMode: UserMode,
|
||||||
|
currentSetting: ObsidianLiveSyncSettings,
|
||||||
|
activate = true
|
||||||
|
): Promise<boolean> {
|
||||||
|
const originalSetting = JSON.parse(JSON.stringify(currentSetting)) as ObsidianLiveSyncSettings;
|
||||||
|
const baseSetting = JSON.parse(JSON.stringify(originalSetting)) as ObsidianLiveSyncSettings;
|
||||||
|
const couchConf = await this.dialogManager.openWithExplicitCancel(SetupRemoteCouchDB, originalSetting);
|
||||||
|
if (couchConf === "cancelled") {
|
||||||
|
this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE);
|
||||||
|
return await this.onOnboard(userMode);
|
||||||
|
}
|
||||||
|
const newSetting = { ...baseSetting, ...couchConf } as ObsidianLiveSyncSettings;
|
||||||
|
if (activate) {
|
||||||
|
newSetting.remoteType = REMOTE_COUCHDB;
|
||||||
|
}
|
||||||
|
return await this.onConfirmApplySettingsFromWizard(newSetting, userMode, activate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles manual setup for S3-compatible bucket
|
||||||
|
* @param userMode
|
||||||
|
* @param currentSetting
|
||||||
|
* @param activate Whether to activate the Bucket as remote type
|
||||||
|
* @returns Promise that resolves to true if setup completed successfully, false otherwise
|
||||||
|
*/
|
||||||
|
async onBucketManualSetup(
|
||||||
|
userMode: UserMode,
|
||||||
|
currentSetting: ObsidianLiveSyncSettings,
|
||||||
|
activate = true
|
||||||
|
): Promise<boolean> {
|
||||||
|
const bucketConf = await this.dialogManager.openWithExplicitCancel(SetupRemoteBucket, currentSetting);
|
||||||
|
if (bucketConf === "cancelled") {
|
||||||
|
this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE);
|
||||||
|
return await this.onOnboard(userMode);
|
||||||
|
}
|
||||||
|
const newSetting = { ...currentSetting, ...bucketConf } as ObsidianLiveSyncSettings;
|
||||||
|
if (activate) {
|
||||||
|
newSetting.remoteType = REMOTE_MINIO;
|
||||||
|
}
|
||||||
|
return await this.onConfirmApplySettingsFromWizard(newSetting, userMode, activate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles manual setup for P2P
|
||||||
|
* @param userMode
|
||||||
|
* @param currentSetting
|
||||||
|
* @param activate Whether to activate the P2P as remote type (as P2P Only setup)
|
||||||
|
* @returns Promise that resolves to true if setup completed successfully, false otherwise
|
||||||
|
*/
|
||||||
|
async onP2PManualSetup(
|
||||||
|
userMode: UserMode,
|
||||||
|
currentSetting: ObsidianLiveSyncSettings,
|
||||||
|
activate = true
|
||||||
|
): Promise<boolean> {
|
||||||
|
const p2pConf = await this.dialogManager.openWithExplicitCancel(SetupRemoteP2P, currentSetting);
|
||||||
|
if (p2pConf === "cancelled") {
|
||||||
|
this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE);
|
||||||
|
return await this.onOnboard(userMode);
|
||||||
|
}
|
||||||
|
const newSetting = { ...currentSetting, ...p2pConf } as ObsidianLiveSyncSettings;
|
||||||
|
if (activate) {
|
||||||
|
newSetting.remoteType = REMOTE_P2P;
|
||||||
|
}
|
||||||
|
return await this.onConfirmApplySettingsFromWizard(newSetting, userMode, activate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles only E2EE configuration
|
||||||
|
* @param userMode
|
||||||
|
* @param currentSetting
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async onlyE2EEConfiguration(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings): Promise<boolean> {
|
||||||
|
const e2eeConf = await this.dialogManager.openWithExplicitCancel(SetupRemoteE2EE, currentSetting);
|
||||||
|
if (e2eeConf === "cancelled") {
|
||||||
|
this._log("E2EE configuration cancelled.", LOG_LEVEL_NOTICE);
|
||||||
|
return await false;
|
||||||
|
}
|
||||||
|
const newSetting = {
|
||||||
|
...currentSetting,
|
||||||
|
...e2eeConf,
|
||||||
|
} as ObsidianLiveSyncSettings;
|
||||||
|
return await this.onConfirmApplySettingsFromWizard(newSetting, userMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles manual configuration flow (E2EE + select server)
|
||||||
|
* @param originalSetting
|
||||||
|
* @param userMode
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async onConfigureManually(originalSetting: ObsidianLiveSyncSettings, userMode: UserMode): Promise<boolean> {
|
||||||
|
const e2eeConf = await this.dialogManager.openWithExplicitCancel(SetupRemoteE2EE, originalSetting);
|
||||||
|
if (e2eeConf === "cancelled") {
|
||||||
|
this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE);
|
||||||
|
return await this.onOnboard(userMode);
|
||||||
|
}
|
||||||
|
const currentSetting = {
|
||||||
|
...originalSetting,
|
||||||
|
...e2eeConf,
|
||||||
|
} as ObsidianLiveSyncSettings;
|
||||||
|
return await this.onSelectServer(currentSetting, userMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles server selection during manual configuration
|
||||||
|
* @param currentSetting
|
||||||
|
* @param userMode
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async onSelectServer(currentSetting: ObsidianLiveSyncSettings, userMode: UserMode): Promise<boolean> {
|
||||||
|
const method = await this.dialogManager.openWithExplicitCancel(SetupRemote);
|
||||||
|
if (method === "couchdb") {
|
||||||
|
return await this.onCouchDBManualSetup(userMode, currentSetting, true);
|
||||||
|
} else if (method === "bucket") {
|
||||||
|
return await this.onBucketManualSetup(userMode, currentSetting, true);
|
||||||
|
} else if (method === "p2p") {
|
||||||
|
return await this.onP2PManualSetup(userMode, currentSetting, true);
|
||||||
|
} else if (method === "cancelled") {
|
||||||
|
this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE);
|
||||||
|
if (userMode !== UserMode.Unknown) {
|
||||||
|
return await this.onOnboard(userMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Should not reach here.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Confirms and applies settings obtained from the wizard
|
||||||
|
* @param newConf
|
||||||
|
* @param _userMode
|
||||||
|
* @param activate Whether to activate the remote type in the new settings
|
||||||
|
* @param extra Extra function to run before applying settings
|
||||||
|
* @returns Promise that resolves to true if settings applied successfully, false otherwise
|
||||||
|
*/
|
||||||
|
async onConfirmApplySettingsFromWizard(
|
||||||
|
newConf: ObsidianLiveSyncSettings,
|
||||||
|
_userMode: UserMode,
|
||||||
|
activate: boolean = true,
|
||||||
|
extra: () => void = () => {}
|
||||||
|
): Promise<boolean> {
|
||||||
|
let userMode = _userMode;
|
||||||
|
if (userMode === UserMode.Unknown) {
|
||||||
|
if (isObjectDifferent(this.settings, newConf, true) === false) {
|
||||||
|
this._log("No changes in settings detected. Skipping applying settings from wizard.", LOG_LEVEL_NOTICE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const patch = generatePatchObj(this.settings, newConf);
|
||||||
|
console.log(`Changes:`);
|
||||||
|
console.dir(patch);
|
||||||
|
if (!activate) {
|
||||||
|
extra();
|
||||||
|
await this.applySetting(newConf, UserMode.ExistingUser);
|
||||||
|
this._log("Setting Applied", LOG_LEVEL_NOTICE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Check virtual changes
|
||||||
|
const original = { ...this.settings, P2P_DevicePeerName: "" } as ObsidianLiveSyncSettings;
|
||||||
|
const modified = { ...newConf, P2P_DevicePeerName: "" } as ObsidianLiveSyncSettings;
|
||||||
|
const isOnlyVirtualChange = isObjectDifferent(original, modified, true) === false;
|
||||||
|
if (isOnlyVirtualChange) {
|
||||||
|
extra();
|
||||||
|
await this.applySetting(newConf, UserMode.ExistingUser);
|
||||||
|
this._log("Settings from wizard applied.", LOG_LEVEL_NOTICE);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const userModeResult = await this.dialogManager.openWithExplicitCancel(OutroAskUserMode);
|
||||||
|
if (userModeResult === "new-user") {
|
||||||
|
userMode = UserMode.NewUser;
|
||||||
|
} else if (userModeResult === "existing-user") {
|
||||||
|
userMode = UserMode.ExistingUser;
|
||||||
|
} else if (userModeResult === "compatible-existing-user") {
|
||||||
|
extra();
|
||||||
|
await this.applySetting(newConf, UserMode.ExistingUser);
|
||||||
|
this._log("Settings from wizard applied.", LOG_LEVEL_NOTICE);
|
||||||
|
return true;
|
||||||
|
} else if (userModeResult === "cancelled") {
|
||||||
|
this._log("User cancelled applying settings from wizard.", LOG_LEVEL_NOTICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const component = userMode === UserMode.NewUser ? OutroNewUser : OutroExistingUser;
|
||||||
|
const confirm = await this.dialogManager.openWithExplicitCancel(component);
|
||||||
|
if (confirm === "cancelled") {
|
||||||
|
this._log("User cancelled applying settings from wizard..", LOG_LEVEL_NOTICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (confirm) {
|
||||||
|
extra();
|
||||||
|
await this.applySetting(newConf, userMode);
|
||||||
|
if (userMode === UserMode.NewUser) {
|
||||||
|
// For new users, schedule a rebuild everything.
|
||||||
|
await this.core.rebuilder.scheduleRebuild();
|
||||||
|
} else {
|
||||||
|
// For existing users, schedule a fetch.
|
||||||
|
await this.core.rebuilder.scheduleFetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Settings applied, but may require rebuild to take effect.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompts the user with QR code scanning instructions
|
||||||
|
* @returns Promise that resolves to false as QR code instruction dialog does not yield settings directly
|
||||||
|
*/
|
||||||
|
|
||||||
|
async onPromptQRCodeInstruction(): Promise<boolean> {
|
||||||
|
const qrResult = await this.dialogManager.open(ScanQRCode);
|
||||||
|
this._log("QR Code dialog closed.", LOG_LEVEL_VERBOSE);
|
||||||
|
// Result is not used, but log it for debugging.
|
||||||
|
this._log(`QR Code result: ${qrResult}`, LOG_LEVEL_VERBOSE);
|
||||||
|
// QR Code instruction dialog never yields settings directly.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes settings from a QR code string and applies them
|
||||||
|
* @param qr QR code string containing encoded settings
|
||||||
|
* @returns Promise that resolves to true if settings applied successfully, false otherwise
|
||||||
|
*/
|
||||||
|
async decodeQR(qr: string) {
|
||||||
|
const newSettings = decodeSettingsFromQRCodeData(qr);
|
||||||
|
return await this.onConfirmApplySettingsFromWizard(newSettings, UserMode.Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the new settings to the core settings and saves them
|
||||||
|
* @param newConf
|
||||||
|
* @param userMode
|
||||||
|
* @returns Promise that resolves to true if settings applied successfully, false otherwise
|
||||||
|
*/
|
||||||
|
async applySetting(newConf: ObsidianLiveSyncSettings, userMode: UserMode) {
|
||||||
|
const newSetting = {
|
||||||
|
...this.core.settings,
|
||||||
|
...newConf,
|
||||||
|
};
|
||||||
|
this.core.settings = newSetting;
|
||||||
|
this.services.setting.clearUsedPassphrase();
|
||||||
|
await this.services.setting.saveSettingData();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
141
src/modules/features/SetupWizard/ObsidianSvelteDialog.ts
Normal file
141
src/modules/features/SetupWizard/ObsidianSvelteDialog.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { eventHub, EVENT_PLUGIN_UNLOADED } from "@/common/events";
|
||||||
|
import { Modal } from "@/deps";
|
||||||
|
import type ObsidianLiveSyncPlugin from "@/main";
|
||||||
|
import { mount, unmount } from "svelte";
|
||||||
|
import DialogHost from "@lib/UI/DialogHost.svelte";
|
||||||
|
import { fireAndForget, promiseWithResolvers, type PromiseWithResolvers } from "octagonal-wheels/promises";
|
||||||
|
import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger";
|
||||||
|
import {
|
||||||
|
type DialogControlBase,
|
||||||
|
type DialogSvelteComponentBaseProps,
|
||||||
|
type ComponentHasResult,
|
||||||
|
setupDialogContext,
|
||||||
|
getDialogContext,
|
||||||
|
type SvelteDialogManagerBase,
|
||||||
|
} from "@/lib/src/UI/svelteDialog.ts";
|
||||||
|
|
||||||
|
export type DialogSvelteComponentProps = DialogSvelteComponentBaseProps & {
|
||||||
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
|
services: ObsidianLiveSyncPlugin["services"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DialogControls<T = any, U = any> = DialogControlBase<T, U> & {
|
||||||
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
|
services: ObsidianLiveSyncPlugin["services"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DialogMessageProps = Record<string, any>;
|
||||||
|
// type DialogSvelteComponent<T extends DialogSvelteComponentProps = DialogSvelteComponentProps> = Component<SvelteComponent<T>,any>;
|
||||||
|
|
||||||
|
export class SvelteDialog<T, U> extends Modal {
|
||||||
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
|
mountedComponent?: ReturnType<typeof mount>;
|
||||||
|
component: ComponentHasResult<T, U>;
|
||||||
|
result?: T;
|
||||||
|
initialData?: U;
|
||||||
|
title: string = "Obsidian LiveSync - Setup Wizard";
|
||||||
|
constructor(plugin: ObsidianLiveSyncPlugin, component: ComponentHasResult<T, U>, initialData?: U) {
|
||||||
|
super(plugin.app);
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.component = component;
|
||||||
|
this.initialData = initialData;
|
||||||
|
}
|
||||||
|
resolveResult() {
|
||||||
|
this.resultPromiseWithResolvers?.resolve(this.result);
|
||||||
|
this.resultPromiseWithResolvers = undefined;
|
||||||
|
}
|
||||||
|
resultPromiseWithResolvers?: PromiseWithResolvers<T | undefined>;
|
||||||
|
onOpen() {
|
||||||
|
const { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
|
const dialog = this;
|
||||||
|
|
||||||
|
if (this.resultPromiseWithResolvers) {
|
||||||
|
this.resultPromiseWithResolvers.reject("Dialog opened again");
|
||||||
|
}
|
||||||
|
const pr = promiseWithResolvers<any>();
|
||||||
|
eventHub.once(EVENT_PLUGIN_UNLOADED, () => {
|
||||||
|
if (this.resultPromiseWithResolvers === pr) {
|
||||||
|
pr.reject("Plugin unloaded");
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.resultPromiseWithResolvers = pr;
|
||||||
|
this.mountedComponent = mount(DialogHost, {
|
||||||
|
target: contentEl,
|
||||||
|
props: {
|
||||||
|
onSetupContext: (props: DialogSvelteComponentBaseProps) => {
|
||||||
|
setupDialogContext({
|
||||||
|
...props,
|
||||||
|
plugin: this.plugin,
|
||||||
|
services: this.plugin.services,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setTitle: (title: string) => {
|
||||||
|
dialog.setTitle(title);
|
||||||
|
},
|
||||||
|
closeDialog: () => {
|
||||||
|
dialog.close();
|
||||||
|
},
|
||||||
|
setResult: (result: T) => {
|
||||||
|
this.result = result;
|
||||||
|
},
|
||||||
|
getInitialData: () => this.initialData,
|
||||||
|
mountComponent: this.component,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
waitForClose(): Promise<T | undefined> {
|
||||||
|
if (!this.resultPromiseWithResolvers) {
|
||||||
|
throw new Error("Dialog not opened yet");
|
||||||
|
}
|
||||||
|
return this.resultPromiseWithResolvers.promise;
|
||||||
|
}
|
||||||
|
onClose() {
|
||||||
|
this.resolveResult();
|
||||||
|
fireAndForget(async () => {
|
||||||
|
if (this.mountedComponent) {
|
||||||
|
await unmount(this.mountedComponent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openSvelteDialog<T, U>(
|
||||||
|
plugin: ObsidianLiveSyncPlugin,
|
||||||
|
component: ComponentHasResult<T, U>,
|
||||||
|
initialData?: U
|
||||||
|
): Promise<T | undefined> {
|
||||||
|
const dialog = new SvelteDialog<T, U>(plugin, component, initialData);
|
||||||
|
dialog.open();
|
||||||
|
|
||||||
|
return await dialog.waitForClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SvelteDialogManager implements SvelteDialogManagerBase {
|
||||||
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
|
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
async open<T, U>(component: ComponentHasResult<T, U>, initialData?: U): Promise<T | undefined> {
|
||||||
|
return await openSvelteDialog<T, U>(this.plugin, component, initialData);
|
||||||
|
}
|
||||||
|
async openWithExplicitCancel<T, U>(component: ComponentHasResult<T, U>, initialData?: U): Promise<T> {
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const ret = await openSvelteDialog<T, U>(this.plugin, component, initialData);
|
||||||
|
if (ret !== undefined) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
if (this.plugin.services.appLifecycle.hasUnloaded()) {
|
||||||
|
throw new Error("Operation cancelled due to app shutdown.");
|
||||||
|
}
|
||||||
|
Logger("Please select 'Cancel' explicitly to cancel this operation.", LOG_LEVEL_NOTICE);
|
||||||
|
}
|
||||||
|
throw new Error("Operation Forcibly cancelled by user.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getObsidianDialogContext<T = any>(): DialogControls<T> {
|
||||||
|
return getDialogContext<T>() as DialogControls<T>;
|
||||||
|
}
|
||||||
154
src/modules/features/SetupWizard/dialogs/FetchEverything.svelte
Normal file
154
src/modules/features/SetupWizard/dialogs/FetchEverything.svelte
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||||
|
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||||
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
|
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||||
|
import Option from "@/lib/src/UI/components/Option.svelte";
|
||||||
|
import Options from "@/lib/src/UI/components/Options.svelte";
|
||||||
|
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||||
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||||
|
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
|
||||||
|
import Check from "@/lib/src/UI/components/Check.svelte";
|
||||||
|
const TYPE_IDENTICAL = "identical";
|
||||||
|
const TYPE_INDEPENDENT = "independent";
|
||||||
|
const TYPE_UNBALANCED = "unbalanced";
|
||||||
|
const TYPE_CANCEL = "cancelled";
|
||||||
|
|
||||||
|
const TYPE_BACKUP_DONE = "backup_done";
|
||||||
|
const TYPE_BACKUP_SKIPPED = "backup_skipped";
|
||||||
|
const TYPE_UNABLE_TO_BACKUP = "unable_to_backup";
|
||||||
|
|
||||||
|
type ResultTypeVault =
|
||||||
|
| typeof TYPE_IDENTICAL
|
||||||
|
| typeof TYPE_INDEPENDENT
|
||||||
|
| typeof TYPE_UNBALANCED
|
||||||
|
| typeof TYPE_CANCEL;
|
||||||
|
type ResultTypeBackup =
|
||||||
|
| typeof TYPE_BACKUP_DONE
|
||||||
|
| typeof TYPE_BACKUP_SKIPPED
|
||||||
|
| typeof TYPE_UNABLE_TO_BACKUP
|
||||||
|
| typeof TYPE_CANCEL;
|
||||||
|
|
||||||
|
type ResultTypeExtra = {
|
||||||
|
preventFetchingConfig: boolean;
|
||||||
|
};
|
||||||
|
type ResultType =
|
||||||
|
| {
|
||||||
|
vault: ResultTypeVault;
|
||||||
|
backup: ResultTypeBackup;
|
||||||
|
extra: ResultTypeExtra;
|
||||||
|
}
|
||||||
|
| typeof TYPE_CANCEL;
|
||||||
|
type Props = {
|
||||||
|
setResult: (result: ResultType) => void;
|
||||||
|
};
|
||||||
|
const { setResult }: Props = $props();
|
||||||
|
let vaultType = $state<ResultTypeVault>(TYPE_CANCEL);
|
||||||
|
let backupType = $state<ResultTypeBackup>(TYPE_CANCEL);
|
||||||
|
const canProceed = $derived.by(() => {
|
||||||
|
return (
|
||||||
|
(vaultType === TYPE_IDENTICAL || vaultType === TYPE_INDEPENDENT || vaultType === TYPE_UNBALANCED) &&
|
||||||
|
(backupType === TYPE_BACKUP_DONE || backupType === TYPE_BACKUP_SKIPPED)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
let preventFetchingConfig = $state(false);
|
||||||
|
|
||||||
|
function commit() {
|
||||||
|
setResult({
|
||||||
|
vault: vaultType,
|
||||||
|
backup: backupType,
|
||||||
|
extra: {
|
||||||
|
preventFetchingConfig,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogHeader title="Reset Synchronisation on This Device" />
|
||||||
|
<Guidance
|
||||||
|
>This will rebuild the local database on this device using the most recent data from the server. This action is
|
||||||
|
designed to resolve synchronisation inconsistencies and restore correct functionality.</Guidance
|
||||||
|
>
|
||||||
|
<Guidance important title="⚠️ Important Notice">
|
||||||
|
<strong
|
||||||
|
>If you have unsynchronised changes in your Vault on this device, they will likely diverge from the server's
|
||||||
|
versions after the reset. This may result in a large number of file conflicts.</strong
|
||||||
|
><br />
|
||||||
|
Furthermore, if conflicts are already present in the server data, they will be synchronised to this device as they are,
|
||||||
|
and you will need to resolve them locally.
|
||||||
|
</Guidance>
|
||||||
|
<hr />
|
||||||
|
<Instruction>
|
||||||
|
<Question
|
||||||
|
><strong>To minimise the creation of new conflicts</strong>, please select the option that best describes the
|
||||||
|
current state of your Vault. The application will then check your files in the most appropriate way based on
|
||||||
|
your selection.</Question
|
||||||
|
>
|
||||||
|
<Options>
|
||||||
|
<Option
|
||||||
|
selectedValue={TYPE_IDENTICAL}
|
||||||
|
title="The files in this Vault are almost identical to the server's."
|
||||||
|
bind:value={vaultType}
|
||||||
|
>
|
||||||
|
(e.g., immediately after restoring on another computer, or having recovered from a backup)
|
||||||
|
</Option>
|
||||||
|
<Option
|
||||||
|
selectedValue={TYPE_INDEPENDENT}
|
||||||
|
title="This Vault is empty, or contains only new files that are not on the server."
|
||||||
|
bind:value={vaultType}
|
||||||
|
>
|
||||||
|
(e.g., setting up for the first time on a new smartphone, starting from a clean slate)
|
||||||
|
</Option>
|
||||||
|
<Option
|
||||||
|
selectedValue={TYPE_UNBALANCED}
|
||||||
|
title="There may be differences between the files in this Vault and the server."
|
||||||
|
bind:value={vaultType}
|
||||||
|
>
|
||||||
|
(e.g., after editing many files whilst offline)
|
||||||
|
<InfoNote info>
|
||||||
|
In this scenario, Self-hosted LiveSync will recreate metadata for every file and deliberately generate
|
||||||
|
conflicts. Where the file content is identical, these conflicts will be resolved automatically.
|
||||||
|
</InfoNote>
|
||||||
|
</Option>
|
||||||
|
</Options>
|
||||||
|
</Instruction>
|
||||||
|
<hr />
|
||||||
|
<Instruction>
|
||||||
|
<Question>Have you created a backup before proceeding?</Question>
|
||||||
|
<InfoNote>
|
||||||
|
We recommend that you copy your Vault folder to a safe location. This will provide a safeguard in case a large
|
||||||
|
number of conflicts arise, or if you accidentally synchronise with an incorrect destination.
|
||||||
|
</InfoNote>
|
||||||
|
<Options>
|
||||||
|
<Option selectedValue={TYPE_BACKUP_DONE} title="I have created a backup of my Vault." bind:value={backupType} />
|
||||||
|
<Option
|
||||||
|
selectedValue={TYPE_BACKUP_SKIPPED}
|
||||||
|
title="I understand the risks and will proceed without a backup."
|
||||||
|
bind:value={backupType}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
selectedValue={TYPE_UNABLE_TO_BACKUP}
|
||||||
|
title="I am unable to create a backup of my Vault."
|
||||||
|
bind:value={backupType}
|
||||||
|
>
|
||||||
|
<InfoNote error visible={backupType === TYPE_UNABLE_TO_BACKUP}>
|
||||||
|
<strong
|
||||||
|
>It is strongly advised to create a backup before proceeding. Continuing without a backup may lead
|
||||||
|
to data loss.
|
||||||
|
</strong>
|
||||||
|
<br />
|
||||||
|
If you understand the risks and still wish to proceed, select so.
|
||||||
|
</InfoNote>
|
||||||
|
</Option>
|
||||||
|
</Options>
|
||||||
|
</Instruction>
|
||||||
|
<Instruction>
|
||||||
|
<ExtraItems title="Advanced">
|
||||||
|
<Check title="Prevent fetching configuration from server" bind:value={preventFetchingConfig} />
|
||||||
|
</ExtraItems>
|
||||||
|
</Instruction>
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title="Reset and Resume Synchronisation" important disabled={!canProceed} commit={() => commit()} />
|
||||||
|
<Decision title="Cancel" commit={() => setResult(TYPE_CANCEL)} />
|
||||||
|
</UserDecisions>
|
||||||
55
src/modules/features/SetupWizard/dialogs/Intro.svelte
Normal file
55
src/modules/features/SetupWizard/dialogs/Intro.svelte
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||||
|
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||||
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
|
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||||
|
import Option from "@/lib/src/UI/components/Option.svelte";
|
||||||
|
import Options from "@/lib/src/UI/components/Options.svelte";
|
||||||
|
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||||
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
const TYPE_NEW_USER = "new-user";
|
||||||
|
const TYPE_EXISTING_USER = "existing-user";
|
||||||
|
const TYPE_CANCELLED = "cancelled";
|
||||||
|
type ResultType = typeof TYPE_NEW_USER | typeof TYPE_EXISTING_USER | typeof TYPE_CANCELLED;
|
||||||
|
type Props = {
|
||||||
|
setResult: (result: ResultType) => void;
|
||||||
|
};
|
||||||
|
const { setResult }: Props = $props();
|
||||||
|
let userType = $state<ResultType>(TYPE_CANCELLED);
|
||||||
|
let proceedTitle = $derived.by(() => {
|
||||||
|
if (userType === TYPE_NEW_USER) {
|
||||||
|
return "Yes, I want to set up a new synchronisation";
|
||||||
|
} else if (userType === TYPE_EXISTING_USER) {
|
||||||
|
return "Yes, I want to add this device to my existing synchronisation";
|
||||||
|
} else {
|
||||||
|
return "Please select an option to proceed";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const canProceed = $derived.by(() => {
|
||||||
|
return userType === TYPE_NEW_USER || userType === TYPE_EXISTING_USER;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogHeader title="Welcome to Self-hosted LiveSync" />
|
||||||
|
<Guidance>We will now guide you through a few questions to simplify the synchronisation setup.</Guidance>
|
||||||
|
<Instruction>
|
||||||
|
<Question>First, please select the option that best describes your current situation.</Question>
|
||||||
|
<Options>
|
||||||
|
<Option selectedValue={TYPE_NEW_USER} title="I am setting this up for the first time" bind:value={userType}>
|
||||||
|
(Select this if you are configuring this device as the first synchronisation device.) This option is
|
||||||
|
suitable if you are new to LiveSync and want to set it up from scratch.
|
||||||
|
</Option>
|
||||||
|
<Option
|
||||||
|
selectedValue={TYPE_EXISTING_USER}
|
||||||
|
title="I am adding a device to an existing synchronisation setup"
|
||||||
|
bind:value={userType}
|
||||||
|
>
|
||||||
|
(Select this if you are already using synchronisation on another computer or smartphone.) This option is
|
||||||
|
suitable if you are new to LiveSync and want to set it up from scratch.
|
||||||
|
</Option>
|
||||||
|
</Options>
|
||||||
|
</Instruction>
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title={proceedTitle} important={canProceed} disabled={!canProceed} commit={() => setResult(userType)} />
|
||||||
|
<Decision title="No, please take me back" commit={() => setResult(TYPE_CANCELLED)} />
|
||||||
|
</UserDecisions>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||||
|
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||||
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
|
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||||
|
import Option from "@/lib/src/UI/components/Option.svelte";
|
||||||
|
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||||
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||||
|
const TYPE_EXISTING = "existing-user";
|
||||||
|
const TYPE_NEW = "new-user";
|
||||||
|
const TYPE_COMPATIBLE_EXISTING = "compatible-existing-user";
|
||||||
|
const TYPE_CANCELLED = "cancelled";
|
||||||
|
type ResultType = typeof TYPE_EXISTING | typeof TYPE_NEW | typeof TYPE_COMPATIBLE_EXISTING | typeof TYPE_CANCELLED;
|
||||||
|
type Props = {
|
||||||
|
setResult: (result: ResultType) => void;
|
||||||
|
};
|
||||||
|
const { setResult }: Props = $props();
|
||||||
|
let userType = $state<ResultType>(TYPE_CANCELLED);
|
||||||
|
const canProceed = $derived.by(() => {
|
||||||
|
return userType === TYPE_EXISTING || userType === TYPE_NEW || userType === TYPE_COMPATIBLE_EXISTING;
|
||||||
|
});
|
||||||
|
const proceedMessage = $derived.by(() => {
|
||||||
|
if (userType === TYPE_NEW) {
|
||||||
|
return "Proceed to the next step.";
|
||||||
|
} else if (userType === TYPE_EXISTING) {
|
||||||
|
return "Proceed to the next step.";
|
||||||
|
} else if (userType === TYPE_COMPATIBLE_EXISTING) {
|
||||||
|
return "Apply the settings";
|
||||||
|
} else {
|
||||||
|
return "Please select an option to proceed";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogHeader title="Mostly Complete: Decision Required" />
|
||||||
|
<Guidance>
|
||||||
|
The connection to the server has been configured successfully. As the next step, <strong
|
||||||
|
>the local database, that is to say the synchronisation information, must be reconstituted.</strong
|
||||||
|
>
|
||||||
|
</Guidance>
|
||||||
|
<Instruction>
|
||||||
|
<Question>Please select your situation.</Question>
|
||||||
|
<Option title="I am setting up a new server for the first time / I want to reset my existing server." bind:value={userType} selectedValue={TYPE_NEW}>
|
||||||
|
<InfoNote>
|
||||||
|
Selecting this option will result in the current data on this device being used to initialise the server.
|
||||||
|
Any existing data on the server will be completely overwritten.
|
||||||
|
</InfoNote>
|
||||||
|
</Option>
|
||||||
|
<Option
|
||||||
|
title="My remote server is already set up. I want to join this device."
|
||||||
|
bind:value={userType}
|
||||||
|
selectedValue={TYPE_EXISTING}
|
||||||
|
>
|
||||||
|
<InfoNote>
|
||||||
|
Selecting this option will result in this device joining the existing server. You need to fetching the
|
||||||
|
existing synchronisation data from the server to this device.
|
||||||
|
</InfoNote>
|
||||||
|
</Option>
|
||||||
|
<Option
|
||||||
|
title="The remote is already set up, and the configuration is compatible (or got compatible by this operation)."
|
||||||
|
bind:value={userType}
|
||||||
|
selectedValue={TYPE_COMPATIBLE_EXISTING}
|
||||||
|
>
|
||||||
|
<InfoNote warning>
|
||||||
|
Unless you are certain, selecting this options is bit dangerous. It assumes that the server configuration is
|
||||||
|
compatible with this device. If this is not the case, data loss may occur. Please ensure you know what you
|
||||||
|
are doing.
|
||||||
|
</InfoNote>
|
||||||
|
</Option>
|
||||||
|
</Instruction>
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title={proceedMessage} important={true} disabled={!canProceed} commit={() => setResult(userType)} />
|
||||||
|
<Decision title="No, please take me back" commit={() => setResult(TYPE_CANCELLED)} />
|
||||||
|
</UserDecisions>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||||
|
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||||
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
|
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||||
|
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||||
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
const TYPE_APPLY = "apply";
|
||||||
|
const TYPE_CANCELLED = "cancelled";
|
||||||
|
type ResultType = typeof TYPE_APPLY | typeof TYPE_CANCELLED;
|
||||||
|
type Props = {
|
||||||
|
setResult: (result: ResultType) => void;
|
||||||
|
};
|
||||||
|
const { setResult }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogHeader title="Setup Complete: Preparing to Fetch Synchronisation Data" />
|
||||||
|
<Guidance>
|
||||||
|
<p>
|
||||||
|
The connection to the server has been configured successfully. As the next step, <strong
|
||||||
|
>the latest synchronisation data will be downloaded from the server to this device.</strong
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>PLEASE NOTE</strong>
|
||||||
|
<br />
|
||||||
|
After restarting, the database on this device will be rebuilt using data from the server. If there are any unsynchronised
|
||||||
|
files in this vault, conflicts may occur with the server data.
|
||||||
|
</p>
|
||||||
|
</Guidance>
|
||||||
|
<Instruction>
|
||||||
|
<Question>Please select the button below to restart and proceed to the data fetching confirmation.</Question>
|
||||||
|
</Instruction>
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title="Restart and Fetch Data" important={true} commit={() => setResult(TYPE_APPLY)} />
|
||||||
|
<Decision title="No, please take me back" commit={() => setResult(TYPE_CANCELLED)} />
|
||||||
|
</UserDecisions>
|
||||||
38
src/modules/features/SetupWizard/dialogs/OutroNewUser.svelte
Normal file
38
src/modules/features/SetupWizard/dialogs/OutroNewUser.svelte
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||||
|
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||||
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
|
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||||
|
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||||
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
const TYPE_APPLY = "apply";
|
||||||
|
const TYPE_CANCELLED = "cancelled";
|
||||||
|
type ResultType = typeof TYPE_APPLY | typeof TYPE_CANCELLED;
|
||||||
|
type Props = {
|
||||||
|
setResult: (result: ResultType) => void;
|
||||||
|
};
|
||||||
|
const { setResult }: Props = $props();
|
||||||
|
// let userType = $state<ResultType>(TYPE_CANCELLED);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogHeader title="Setup Complete: Preparing to Initialise Server" />
|
||||||
|
<Guidance>
|
||||||
|
<p>
|
||||||
|
The connection to the server has been configured successfully. As the next step, <strong
|
||||||
|
>the synchronisation data on the server will be built based on the current data on this device.</strong
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>IMPORTANT</strong>
|
||||||
|
<br />
|
||||||
|
After restarting, the data on this device will be uploaded to the server as the 'master copy'. Please be aware that
|
||||||
|
any unintended data currently on the server will be completely overwritten.
|
||||||
|
</p>
|
||||||
|
</Guidance>
|
||||||
|
<Instruction>
|
||||||
|
<Question>Please select the button below to restart and proceed to the final confirmation.</Question>
|
||||||
|
</Instruction>
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title="Restart and Initialise Server" important={true} commit={() => setResult(TYPE_APPLY)} />
|
||||||
|
<Decision title="No, please take me back" commit={() => setResult(TYPE_CANCELLED)} />
|
||||||
|
</UserDecisions>
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Panel to check and fix CouchDB configuration issues
|
||||||
|
*/
|
||||||
|
import type { ObsidianLiveSyncSettings } from "../../../../lib/src/common/types";
|
||||||
|
import Decision from "../../../../lib/src/UI/components/Decision.svelte";
|
||||||
|
import UserDecisions from "../../../../lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
import { checkConfig, type ConfigCheckResult, type ResultError, type ResultErrorMessage } from "./utilCheckCouchDB";
|
||||||
|
type Props = {
|
||||||
|
trialRemoteSetting: ObsidianLiveSyncSettings;
|
||||||
|
};
|
||||||
|
const { trialRemoteSetting }: Props = $props();
|
||||||
|
let detectedIssues = $state<ConfigCheckResult[]>([]);
|
||||||
|
async function testAndFixSettings() {
|
||||||
|
detectedIssues = [];
|
||||||
|
try {
|
||||||
|
const fixResults = await checkConfig(trialRemoteSetting);
|
||||||
|
console.dir(fixResults);
|
||||||
|
detectedIssues = fixResults;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error during testAndFixSettings:", e);
|
||||||
|
detectedIssues.push({ message: `Error during testAndFixSettings: ${e}`, result: "error", classes: [] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function isErrorResult(result: ConfigCheckResult): result is ResultError | ResultErrorMessage {
|
||||||
|
return "result" in result && result.result === "error";
|
||||||
|
}
|
||||||
|
function isFixableError(result: ConfigCheckResult): result is ResultError {
|
||||||
|
return isErrorResult(result) && "fix" in result && typeof result.fix === "function";
|
||||||
|
}
|
||||||
|
function isSuccessResult(result: ConfigCheckResult): result is { message: string; result: "ok"; value?: any } {
|
||||||
|
return "result" in result && result.result === "ok";
|
||||||
|
}
|
||||||
|
let processing = $state(false);
|
||||||
|
async function fixIssue(issue: ResultError) {
|
||||||
|
try {
|
||||||
|
processing = true;
|
||||||
|
await issue.fix();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error during fixIssue:", e);
|
||||||
|
}
|
||||||
|
await testAndFixSettings();
|
||||||
|
processing = false;
|
||||||
|
}
|
||||||
|
const errorIssueCount = $derived.by(() => {
|
||||||
|
return detectedIssues.filter((issue) => isErrorResult(issue)).length;
|
||||||
|
});
|
||||||
|
const isAllSuccess = $derived.by(() => {
|
||||||
|
return !(errorIssueCount > 0 && detectedIssues.length > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet result(issue: ConfigCheckResult)}
|
||||||
|
<div class="check-result {isErrorResult(issue) ? 'error' : isSuccessResult(issue) ? 'success' : ''}">
|
||||||
|
<div class="message">
|
||||||
|
{issue.message}
|
||||||
|
</div>
|
||||||
|
{#if isFixableError(issue)}
|
||||||
|
<div class="operations">
|
||||||
|
<button onclick={() => fixIssue(issue)} class="mod-cta" disabled={processing}>Fix</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title="Detect and Fix CouchDB Issues" important={true} commit={testAndFixSettings} />
|
||||||
|
</UserDecisions>
|
||||||
|
<div class="check-results">
|
||||||
|
<details open={!isAllSuccess}>
|
||||||
|
<summary>
|
||||||
|
{#if detectedIssues.length === 0}
|
||||||
|
No checks have been performed yet.
|
||||||
|
{:else if isAllSuccess}
|
||||||
|
All checks passed successfully!
|
||||||
|
{:else}
|
||||||
|
{errorIssueCount} issue(s) detected!
|
||||||
|
{/if}
|
||||||
|
</summary>
|
||||||
|
{#if detectedIssues.length > 0}
|
||||||
|
<h3>Issue detection log:</h3>
|
||||||
|
{#each detectedIssues as issue}
|
||||||
|
{@render result(issue)}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Make .check-result a CSS Grid: let .message expand and keep .operations at minimum width, aligned to the right */
|
||||||
|
.check-results {
|
||||||
|
/* Adjust spacing as required */
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-result {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto; /* message takes remaining space, operations use minimum width */
|
||||||
|
align-items: center; /* vertically centre align */
|
||||||
|
gap: 0.5rem 1rem;
|
||||||
|
padding: 0rem 0.5rem;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
border-left: 0.5em solid var(--interactive-accent);
|
||||||
|
margin-bottom: 0.25lh;
|
||||||
|
}
|
||||||
|
.check-result.error {
|
||||||
|
border-left: 0.5em solid var(--text-error);
|
||||||
|
}
|
||||||
|
.check-result.success {
|
||||||
|
border-left: 0.5em solid var(--text-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-result .message {
|
||||||
|
/* Wrap long messages */
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-result .operations {
|
||||||
|
/* Centre the button(s) vertically and align to the right */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For small screens: move .operations below and stack vertically */
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.check-result {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
}
|
||||||
|
.check-result .operations {
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||||
|
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||||
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
|
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||||
|
import Option from "@/lib/src/UI/components/Option.svelte";
|
||||||
|
import Options from "@/lib/src/UI/components/Options.svelte";
|
||||||
|
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||||
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||||
|
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
|
||||||
|
import Check from "@/lib/src/UI/components/Check.svelte";
|
||||||
|
const TYPE_CANCEL = "cancelled";
|
||||||
|
|
||||||
|
const TYPE_BACKUP_DONE = "backup_done";
|
||||||
|
const TYPE_BACKUP_SKIPPED = "backup_skipped";
|
||||||
|
const TYPE_UNABLE_TO_BACKUP = "unable_to_backup";
|
||||||
|
|
||||||
|
type ResultTypeBackup =
|
||||||
|
| typeof TYPE_BACKUP_DONE
|
||||||
|
| typeof TYPE_BACKUP_SKIPPED
|
||||||
|
| typeof TYPE_UNABLE_TO_BACKUP
|
||||||
|
| typeof TYPE_CANCEL;
|
||||||
|
|
||||||
|
type ResultTypeExtra = {
|
||||||
|
preventFetchingConfig: boolean;
|
||||||
|
};
|
||||||
|
type ResultType =
|
||||||
|
| {
|
||||||
|
backup: ResultTypeBackup;
|
||||||
|
extra: ResultTypeExtra;
|
||||||
|
}
|
||||||
|
| typeof TYPE_CANCEL;
|
||||||
|
type Props = {
|
||||||
|
setResult: (result: ResultType) => void;
|
||||||
|
};
|
||||||
|
const { setResult }: Props = $props();
|
||||||
|
|
||||||
|
let backupType = $state<ResultTypeBackup>(TYPE_CANCEL);
|
||||||
|
let confirmationCheck1 = $state(false);
|
||||||
|
let confirmationCheck2 = $state(false);
|
||||||
|
let confirmationCheck3 = $state(false);
|
||||||
|
const canProceed = $derived.by(() => {
|
||||||
|
return (
|
||||||
|
(backupType === TYPE_BACKUP_DONE || backupType === TYPE_BACKUP_SKIPPED) &&
|
||||||
|
confirmationCheck1 &&
|
||||||
|
confirmationCheck2 &&
|
||||||
|
confirmationCheck3
|
||||||
|
);
|
||||||
|
});
|
||||||
|
let preventFetchingConfig = $state(false);
|
||||||
|
|
||||||
|
function commit() {
|
||||||
|
setResult({
|
||||||
|
backup: backupType,
|
||||||
|
extra: {
|
||||||
|
preventFetchingConfig,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogHeader title="Final Confirmation: Overwrite Server Data with This Device's Files" />
|
||||||
|
<Guidance
|
||||||
|
>This procedure will first delete all existing synchronisation data from the server. Following this, the server data
|
||||||
|
will be completely rebuilt, using the current state of your Vault on this device (including its local database) as
|
||||||
|
<strong>the single, authoritative master copy</strong>.</Guidance
|
||||||
|
>
|
||||||
|
<InfoNote>
|
||||||
|
You should perform this operation only in exceptional circumstances, such as when the server data is completely
|
||||||
|
corrupted, when changes on all other devices are no longer needed, or when the database size has become unusually
|
||||||
|
large in comparison to the Vault size.
|
||||||
|
</InfoNote>
|
||||||
|
<Guidance important title="⚠️ Please Confirm the Following">
|
||||||
|
<Check
|
||||||
|
title="I understand that all changes made on other smartphones or computers possibly could be lost."
|
||||||
|
bind:value={confirmationCheck1}
|
||||||
|
>
|
||||||
|
<InfoNote>There is a way to resolve this on other devices.</InfoNote>
|
||||||
|
<InfoNote>Of course, we can back up the data before proceeding.</InfoNote>
|
||||||
|
</Check>
|
||||||
|
<Check
|
||||||
|
title="I understand that other devices will no longer be able to synchronise, and will need to be reset the synchronisation information."
|
||||||
|
bind:value={confirmationCheck2}
|
||||||
|
>
|
||||||
|
<InfoNote>by resetting the remote, you will be informed on other devices.</InfoNote>
|
||||||
|
</Check>
|
||||||
|
<Check title="I understand that this action is irreversible once performed." bind:value={confirmationCheck3} />
|
||||||
|
</Guidance>
|
||||||
|
<hr />
|
||||||
|
<Instruction>
|
||||||
|
<Question>Have you created a backup before proceeding?</Question>
|
||||||
|
<InfoNote warning>
|
||||||
|
This is an extremely powerful operation. We strongly recommend that you copy your Vault folder to a safe
|
||||||
|
location.
|
||||||
|
</InfoNote>
|
||||||
|
<Options>
|
||||||
|
<Option selectedValue={TYPE_BACKUP_DONE} title="I have created a backup of my Vault." bind:value={backupType} />
|
||||||
|
<Option
|
||||||
|
selectedValue={TYPE_BACKUP_SKIPPED}
|
||||||
|
title="I understand the risks and will proceed without a backup."
|
||||||
|
bind:value={backupType}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
selectedValue={TYPE_UNABLE_TO_BACKUP}
|
||||||
|
title="I am unable to create a backup of my Vaults."
|
||||||
|
bind:value={backupType}
|
||||||
|
>
|
||||||
|
<InfoNote error visible={backupType === TYPE_UNABLE_TO_BACKUP}>
|
||||||
|
<strong
|
||||||
|
>You should create a new synchronisation destination and rebuild your data there. <br /> After that,
|
||||||
|
synchronise to a brand new vault on each other device with the new remote one by one.</strong
|
||||||
|
>
|
||||||
|
</InfoNote>
|
||||||
|
</Option>
|
||||||
|
</Options>
|
||||||
|
</Instruction>
|
||||||
|
<Instruction>
|
||||||
|
<ExtraItems title="Advanced">
|
||||||
|
<Check title="Prevent fetching configuration from server" bind:value={preventFetchingConfig} />
|
||||||
|
</ExtraItems>
|
||||||
|
</Instruction>
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title="I Understand, Overwrite Server" important disabled={!canProceed} commit={() => commit()} />
|
||||||
|
<Decision title="Cancel" commit={() => setResult(TYPE_CANCEL)} />
|
||||||
|
</UserDecisions>
|
||||||
28
src/modules/features/SetupWizard/dialogs/ScanQRCode.svelte
Normal file
28
src/modules/features/SetupWizard/dialogs/ScanQRCode.svelte
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||||
|
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||||
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
|
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||||
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
const TYPE_CLOSE = "close";
|
||||||
|
type ResultType = typeof TYPE_CLOSE;
|
||||||
|
type Props = {
|
||||||
|
setResult: (result: ResultType) => void;
|
||||||
|
};
|
||||||
|
const { setResult }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogHeader title="Scan QR Code" />
|
||||||
|
<Guidance>Please follow the steps below to import settings from your existing device.</Guidance>
|
||||||
|
<Instruction>
|
||||||
|
<!-- <Question>How would you like to configure the connection to your server?</Question> -->
|
||||||
|
<ol>
|
||||||
|
<li>On this device, please keep this Vault open.</li>
|
||||||
|
<li>On the source device, open Obsidian.</li>
|
||||||
|
<li>On the source device, from the command palette, run the 'Show settings as a QR code' command.</li>
|
||||||
|
<li>On this device, switch to the camera app or use a QR code scanner to scan the displayed QR code.</li>
|
||||||
|
</ol>
|
||||||
|
</Instruction>
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title="Close this dialog" important={true} commit={() => setResult(TYPE_CLOSE)} />
|
||||||
|
</UserDecisions>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||||
|
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||||
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
|
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||||
|
import Option from "@/lib/src/UI/components/Option.svelte";
|
||||||
|
import Options from "@/lib/src/UI/components/Options.svelte";
|
||||||
|
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||||
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||||
|
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
|
||||||
|
import Check from "@/lib/src/UI/components/Check.svelte";
|
||||||
|
const TYPE_USE_SETUP_URI = "use-setup-uri";
|
||||||
|
const TYPE_SCAN_QR_CODE = "scan-qr-code";
|
||||||
|
const TYPE_CONFIGURE_MANUALLY = "configure-manually";
|
||||||
|
const TYPE_CANCELLED = "cancelled";
|
||||||
|
type ResultType = typeof TYPE_USE_SETUP_URI | typeof TYPE_SCAN_QR_CODE | typeof TYPE_CONFIGURE_MANUALLY | typeof TYPE_CANCELLED;
|
||||||
|
type Props = {
|
||||||
|
setResult: (result: ResultType) => void;
|
||||||
|
};
|
||||||
|
const { setResult }: Props = $props();
|
||||||
|
let userType = $state<ResultType>(TYPE_CANCELLED);
|
||||||
|
let proceedTitle = $derived.by(() => {
|
||||||
|
if (userType === TYPE_USE_SETUP_URI) {
|
||||||
|
return "Proceed with Setup URI";
|
||||||
|
} else if (userType === TYPE_CONFIGURE_MANUALLY) {
|
||||||
|
return "I know my server details, let me enter them";
|
||||||
|
} else if (userType === TYPE_SCAN_QR_CODE) {
|
||||||
|
return "Scan the QR code displayed on an active device using this device's camera.";
|
||||||
|
} else {
|
||||||
|
return "Please select an option to proceed";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const canProceed = $derived.by(() => {
|
||||||
|
return userType === TYPE_USE_SETUP_URI || userType === TYPE_CONFIGURE_MANUALLY || userType === TYPE_SCAN_QR_CODE;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogHeader title="Device Setup Method" />
|
||||||
|
<Guidance>You are adding this device to an existing synchronisation setup.</Guidance>
|
||||||
|
<Instruction>
|
||||||
|
<Question>Please select a method to import the settings from another device.</Question>
|
||||||
|
<Options>
|
||||||
|
<Option selectedValue={TYPE_USE_SETUP_URI} title="Use a Setup URI (Recommended)" bind:value={userType}>
|
||||||
|
Paste the Setup URI generated from one of your active devices.
|
||||||
|
</Option>
|
||||||
|
<Option selectedValue={TYPE_SCAN_QR_CODE} title="Scan a QR Code (Recommended for mobile)" bind:value={userType}>
|
||||||
|
Scan the QR code displayed on an active device using this device's camera.
|
||||||
|
</Option>
|
||||||
|
<Option
|
||||||
|
selectedValue={TYPE_CONFIGURE_MANUALLY}
|
||||||
|
title="Enter the server information manually"
|
||||||
|
bind:value={userType}
|
||||||
|
>
|
||||||
|
Configure the same server information as your other devices again, manually, very advanced users only.
|
||||||
|
</Option>
|
||||||
|
</Options>
|
||||||
|
</Instruction>
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title={proceedTitle} important={canProceed} disabled={!canProceed} commit={() => setResult(userType)} />
|
||||||
|
<Decision title="Cancel" commit={() => setResult(TYPE_CANCELLED)} />
|
||||||
|
</UserDecisions>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||||
|
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||||
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
|
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||||
|
import Option from "@/lib/src/UI/components/Option.svelte";
|
||||||
|
import Options from "@/lib/src/UI/components/Options.svelte";
|
||||||
|
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||||
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||||
|
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
|
||||||
|
import Check from "@/lib/src/UI/components/Check.svelte";
|
||||||
|
const TYPE_USE_SETUP_URI = "use-setup-uri";
|
||||||
|
const TYPE_CONFIGURE_MANUALLY = "configure-manually";
|
||||||
|
const TYPE_CANCELLED = "cancelled";
|
||||||
|
type ResultType = typeof TYPE_USE_SETUP_URI | typeof TYPE_CONFIGURE_MANUALLY | typeof TYPE_CANCELLED;
|
||||||
|
type Props = {
|
||||||
|
setResult: (result: ResultType) => void;
|
||||||
|
};
|
||||||
|
const { setResult }: Props = $props();
|
||||||
|
let userType = $state<ResultType>(TYPE_CANCELLED);
|
||||||
|
let proceedTitle = $derived.by(() => {
|
||||||
|
if (userType === TYPE_USE_SETUP_URI) {
|
||||||
|
return "Proceed with Setup URI";
|
||||||
|
} else if (userType === TYPE_CONFIGURE_MANUALLY) {
|
||||||
|
return "I know my server details, let me enter them";
|
||||||
|
} else {
|
||||||
|
return "Please select an option to proceed";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const canProceed = $derived.by(() => {
|
||||||
|
return userType === TYPE_USE_SETUP_URI || userType === TYPE_CONFIGURE_MANUALLY;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogHeader title="Connection Method" />
|
||||||
|
<Guidance>We will now proceed with the server configuration.</Guidance>
|
||||||
|
<Instruction>
|
||||||
|
<Question>How would you like to configure the connection to your server?</Question>
|
||||||
|
<Options>
|
||||||
|
<Option selectedValue={TYPE_USE_SETUP_URI} title="Use a Setup URI (Recommended)" bind:value={userType}>
|
||||||
|
A Setup URI is a single string of text containing your server address and authentication details. Using a
|
||||||
|
URI, if one was generated by your server installation script, provides a simple and secure configuration.
|
||||||
|
</Option>
|
||||||
|
<Option
|
||||||
|
selectedValue={TYPE_CONFIGURE_MANUALLY}
|
||||||
|
title="Enter the server information manually"
|
||||||
|
bind:value={userType}
|
||||||
|
>
|
||||||
|
This is an advanced option for users who do not have a URI or who wish to configure detailed settings.
|
||||||
|
</Option>
|
||||||
|
</Options>
|
||||||
|
</Instruction>
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title={proceedTitle} important={canProceed} disabled={!canProceed} commit={() => setResult(userType)} />
|
||||||
|
<Decision title="Cancel" commit={() => setResult(TYPE_CANCELLED)} />
|
||||||
|
</UserDecisions>
|
||||||
56
src/modules/features/SetupWizard/dialogs/SetupRemote.svelte
Normal file
56
src/modules/features/SetupWizard/dialogs/SetupRemote.svelte
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||||
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
|
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||||
|
import Option from "@/lib/src/UI/components/Option.svelte";
|
||||||
|
import Options from "@/lib/src/UI/components/Options.svelte";
|
||||||
|
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||||
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
const TYPE_COUCHDB = "couchdb";
|
||||||
|
const TYPE_BUCKET = "bucket";
|
||||||
|
const TYPE_P2P = "p2p";
|
||||||
|
const TYPE_CANCELLED = "cancelled";
|
||||||
|
type ResultType = typeof TYPE_COUCHDB | typeof TYPE_BUCKET | typeof TYPE_P2P | typeof TYPE_CANCELLED;
|
||||||
|
type Props = {
|
||||||
|
setResult: (result: ResultType) => void;
|
||||||
|
};
|
||||||
|
const { setResult }: Props = $props();
|
||||||
|
let userType = $state<ResultType>(TYPE_CANCELLED);
|
||||||
|
let proceedTitle = $derived.by(() => {
|
||||||
|
if (userType === TYPE_COUCHDB) {
|
||||||
|
return "Continue to CouchDB setup";
|
||||||
|
} else if (userType === TYPE_BUCKET) {
|
||||||
|
return "Continue to S3/MinIO/R2 setup";
|
||||||
|
} else if (userType === TYPE_P2P) {
|
||||||
|
return "Continue to Peer-to-Peer only setup";
|
||||||
|
} else {
|
||||||
|
return "Please select an option to proceed";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const canProceed = $derived.by(() => {
|
||||||
|
return userType === TYPE_COUCHDB || userType === TYPE_BUCKET || userType === TYPE_P2P;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogHeader title="Enter Server Information" />
|
||||||
|
<Instruction>
|
||||||
|
<Question>Please select the type of server to which you are connecting.</Question>
|
||||||
|
<Options>
|
||||||
|
<Option selectedValue={TYPE_COUCHDB} title="CouchDB" bind:value={userType}>
|
||||||
|
This is the most suitable synchronisation method for the design. All functions are available. You must have
|
||||||
|
set up a CouchDB instance.
|
||||||
|
</Option>
|
||||||
|
<Option selectedValue={TYPE_BUCKET} title="S3/MinIO/R2 Object Storage" bind:value={userType}>
|
||||||
|
Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.
|
||||||
|
</Option>
|
||||||
|
<Option selectedValue={TYPE_P2P} title="Peer-to-Peer only" bind:value={userType}>
|
||||||
|
This is an experimental feature enabling direct synchronisation between devices. No server is required, but
|
||||||
|
both devices must be online at the same time for synchronisation to occur, and some features may be limited.
|
||||||
|
Internet connection is only required to signalling (detecting peers) and not for data transfer.
|
||||||
|
</Option>
|
||||||
|
</Options>
|
||||||
|
</Instruction>
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title={proceedTitle} important={canProceed} disabled={!canProceed} commit={() => setResult(userType)} />
|
||||||
|
<Decision title="No, please take me back" commit={() => setResult(TYPE_CANCELLED)} />
|
||||||
|
</UserDecisions>
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||||
|
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||||
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||||
|
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
|
||||||
|
import InputRow from "@/lib/src/UI/components/InputRow.svelte";
|
||||||
|
import Password from "@/lib/src/UI/components/Password.svelte";
|
||||||
|
import {
|
||||||
|
type BucketSyncSetting,
|
||||||
|
type ObsidianLiveSyncSettings,
|
||||||
|
DEFAULT_SETTINGS,
|
||||||
|
PREFERRED_JOURNAL_SYNC,
|
||||||
|
RemoteTypes,
|
||||||
|
} from "../../../../lib/src/common/types";
|
||||||
|
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
|
||||||
|
import { getObsidianDialogContext } from "../ObsidianSvelteDialog";
|
||||||
|
import { copyTo, pickBucketSyncSettings } from "../../../../lib/src/common/utils";
|
||||||
|
|
||||||
|
const default_setting = pickBucketSyncSettings(DEFAULT_SETTINGS);
|
||||||
|
|
||||||
|
let syncSetting = $state<BucketSyncSetting>({ ...default_setting });
|
||||||
|
|
||||||
|
type ResultType = typeof TYPE_CANCELLED | BucketSyncSetting;
|
||||||
|
type Props = GuestDialogProps<ResultType, BucketSyncSetting>;
|
||||||
|
const TYPE_CANCELLED = "cancelled";
|
||||||
|
|
||||||
|
const { setResult, getInitialData }: Props = $props();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (getInitialData) {
|
||||||
|
const initialData = getInitialData();
|
||||||
|
if (initialData) {
|
||||||
|
copyTo(initialData, syncSetting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let error = $state("");
|
||||||
|
const context = getObsidianDialogContext();
|
||||||
|
const isEndpointSecure = $derived.by(() => {
|
||||||
|
return syncSetting.endpoint.trim().toLowerCase().startsWith("https://");
|
||||||
|
});
|
||||||
|
const isEndpointInsecure = $derived.by(() => {
|
||||||
|
return syncSetting.endpoint.trim().toLowerCase().startsWith("http://");
|
||||||
|
});
|
||||||
|
const isEndpointSupplied = $derived.by(() => {
|
||||||
|
return isEndpointInsecure || isEndpointSecure;
|
||||||
|
});
|
||||||
|
const canProceed = $derived.by(() => {
|
||||||
|
return (
|
||||||
|
syncSetting.accessKey.trim() !== "" &&
|
||||||
|
syncSetting.secretKey.trim() !== "" &&
|
||||||
|
syncSetting.bucket.trim() !== "" &&
|
||||||
|
syncSetting.endpoint.trim() !== "" &&
|
||||||
|
syncSetting.region.trim() !== "" &&
|
||||||
|
isEndpointSupplied
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function generateSetting() {
|
||||||
|
const connSetting: BucketSyncSetting = {
|
||||||
|
...syncSetting,
|
||||||
|
};
|
||||||
|
const trialSettings: BucketSyncSetting = {
|
||||||
|
...connSetting,
|
||||||
|
};
|
||||||
|
|
||||||
|
const trialRemoteSetting: ObsidianLiveSyncSettings = {
|
||||||
|
...DEFAULT_SETTINGS,
|
||||||
|
...PREFERRED_JOURNAL_SYNC,
|
||||||
|
remoteType: RemoteTypes.REMOTE_MINIO,
|
||||||
|
...trialSettings,
|
||||||
|
};
|
||||||
|
return trialRemoteSetting;
|
||||||
|
}
|
||||||
|
|
||||||
|
let processing = $state(false);
|
||||||
|
async function checkConnection() {
|
||||||
|
try {
|
||||||
|
processing = true;
|
||||||
|
const trialRemoteSetting = generateSetting();
|
||||||
|
const replicator = await context.services.replicator.getNewReplicator(trialRemoteSetting);
|
||||||
|
if (!replicator) {
|
||||||
|
return "Failed to create replicator instance.";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await replicator.tryConnectRemote(trialRemoteSetting, false);
|
||||||
|
if (result) {
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return "Failed to connect to the server. Please check your settings.";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return `Failed to connect to the server: ${e}`;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAndCommit() {
|
||||||
|
error = "";
|
||||||
|
try {
|
||||||
|
error = (await checkConnection()) || "";
|
||||||
|
if (!error) {
|
||||||
|
const setting = generateSetting();
|
||||||
|
setResult(pickBucketSyncSettings(setting));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error = `Error during connection test: ${e}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function commit() {
|
||||||
|
const setting = pickBucketSyncSettings(generateSetting());
|
||||||
|
setResult(setting);
|
||||||
|
}
|
||||||
|
function cancel() {
|
||||||
|
setResult(TYPE_CANCELLED);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogHeader title="S3/MinIO/R2 Configuration" />
|
||||||
|
<Guidance>Please enter the details required to connect to your S3/MinIO/R2 compatible object storage service.</Guidance>
|
||||||
|
<InputRow label="Endpoint URL">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="s3-endpoint"
|
||||||
|
placeholder="https://s3.amazonaws.com"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
required
|
||||||
|
pattern="^https?://.+"
|
||||||
|
bind:value={syncSetting.endpoint}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
<InfoNote warning visible={isEndpointInsecure}>We can use only Secure (HTTPS) connections on Obsidian Mobile.</InfoNote>
|
||||||
|
|
||||||
|
<InputRow label="Access Key ID">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="s3-access-key-id"
|
||||||
|
placeholder="Enter your Access Key ID"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
required
|
||||||
|
bind:value={syncSetting.accessKey}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
|
||||||
|
<InputRow label="Secret Access Key">
|
||||||
|
<Password
|
||||||
|
name="s3-secret-access-key"
|
||||||
|
placeholder="Enter your Secret Access Key"
|
||||||
|
required
|
||||||
|
bind:value={syncSetting.secretKey}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
<InputRow label="Bucket Name">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="s3-bucket-name"
|
||||||
|
placeholder="Enter your Bucket Name"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
required
|
||||||
|
bind:value={syncSetting.bucket}
|
||||||
|
/></InputRow
|
||||||
|
>
|
||||||
|
<InputRow label="Region">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="s3-region"
|
||||||
|
placeholder="Enter your Region (e.g., us-east-1, auto for R2)"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
bind:value={syncSetting.region}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
<InputRow label="Use Path-Style Access">
|
||||||
|
<input type="checkbox" name="s3-use-path-style" bind:checked={syncSetting.forcePathStyle} />
|
||||||
|
</InputRow>
|
||||||
|
|
||||||
|
<InputRow label="Folder Prefix">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="s3-folder-prefix"
|
||||||
|
placeholder="Enter a folder prefix (optional)"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
bind:value={syncSetting.bucketPrefix}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
<InfoNote>
|
||||||
|
If you want to store the data in a specific folder within the bucket, you can specify a folder prefix here.
|
||||||
|
Otherwise, leave it blank to store data at the root of the bucket.
|
||||||
|
</InfoNote>
|
||||||
|
<InputRow label="Use internal API">
|
||||||
|
<input type="checkbox" name="s3-use-internal-api" bind:checked={syncSetting.useCustomRequestHandler} />
|
||||||
|
</InputRow>
|
||||||
|
<InfoNote>
|
||||||
|
If you cannot avoid CORS issues, you might want to try this option. It uses Obsidian's internal API to communicate
|
||||||
|
with the S3 server. Not compliant with web standards, but works. Note that this might break in future Obsidian
|
||||||
|
versions.
|
||||||
|
</InfoNote>
|
||||||
|
|
||||||
|
<ExtraItems title="Advanced Settings">
|
||||||
|
<InputRow label="Custom Headers">
|
||||||
|
<textarea
|
||||||
|
name="bucket-custom-headers"
|
||||||
|
placeholder="e.g., x-example-header: value\n another-header: value2"
|
||||||
|
bind:value={syncSetting.bucketCustomHeaders}
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
rows="4"
|
||||||
|
></textarea>
|
||||||
|
</InputRow>
|
||||||
|
</ExtraItems>
|
||||||
|
|
||||||
|
<InfoNote error visible={error !== ""}>
|
||||||
|
{error}
|
||||||
|
</InfoNote>
|
||||||
|
|
||||||
|
{#if processing}
|
||||||
|
Checking connection... Please wait.
|
||||||
|
{:else}
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title="Test Settings and Continue" important disabled={!canProceed} commit={() => checkAndCommit()} />
|
||||||
|
<Decision title="Continue anyway" commit={() => commit()} />
|
||||||
|
<Decision title="Cancel" commit={() => cancel()} />
|
||||||
|
</UserDecisions>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||||
|
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||||
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||||
|
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
|
||||||
|
import InputRow from "@/lib/src/UI/components/InputRow.svelte";
|
||||||
|
import Password from "@/lib/src/UI/components/Password.svelte";
|
||||||
|
import {
|
||||||
|
DEFAULT_SETTINGS,
|
||||||
|
PREFERRED_SETTING_CLOUDANT,
|
||||||
|
PREFERRED_SETTING_SELF_HOSTED,
|
||||||
|
RemoteTypes,
|
||||||
|
type CouchDBConnection,
|
||||||
|
type ObsidianLiveSyncSettings,
|
||||||
|
} from "../../../../lib/src/common/types";
|
||||||
|
import { isCloudantURI } from "../../../../lib/src/pouchdb/utils_couchdb";
|
||||||
|
|
||||||
|
import { getObsidianDialogContext } from "../ObsidianSvelteDialog";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
|
||||||
|
import { copyTo, pickCouchDBSyncSettings } from "../../../../lib/src/common/utils";
|
||||||
|
import PanelCouchDBCheck from "./PanelCouchDBCheck.svelte";
|
||||||
|
|
||||||
|
const default_setting = pickCouchDBSyncSettings(DEFAULT_SETTINGS);
|
||||||
|
|
||||||
|
let syncSetting = $state<CouchDBConnection>({ ...default_setting });
|
||||||
|
type ResultType = typeof TYPE_CANCELLED | CouchDBConnection;
|
||||||
|
const TYPE_CANCELLED = "cancelled";
|
||||||
|
type Props = GuestDialogProps<ResultType, CouchDBConnection>;
|
||||||
|
const { setResult, getInitialData }: Props = $props();
|
||||||
|
onMount(() => {
|
||||||
|
if (getInitialData) {
|
||||||
|
const initialData = getInitialData();
|
||||||
|
if (initialData) {
|
||||||
|
copyTo(initialData, syncSetting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let error = $state("");
|
||||||
|
const context = getObsidianDialogContext();
|
||||||
|
|
||||||
|
function generateSetting() {
|
||||||
|
const connSetting: CouchDBConnection = {
|
||||||
|
...syncSetting,
|
||||||
|
};
|
||||||
|
const trialSettings: CouchDBConnection = {
|
||||||
|
...connSetting,
|
||||||
|
// ...encryptionSettings,
|
||||||
|
};
|
||||||
|
const preferredSetting = isCloudantURI(syncSetting.couchDB_URI)
|
||||||
|
? PREFERRED_SETTING_CLOUDANT
|
||||||
|
: PREFERRED_SETTING_SELF_HOSTED;
|
||||||
|
const trialRemoteSetting: ObsidianLiveSyncSettings = {
|
||||||
|
...DEFAULT_SETTINGS,
|
||||||
|
...preferredSetting,
|
||||||
|
remoteType: RemoteTypes.REMOTE_COUCHDB,
|
||||||
|
...trialSettings,
|
||||||
|
};
|
||||||
|
return trialRemoteSetting;
|
||||||
|
}
|
||||||
|
let processing = $state(false);
|
||||||
|
async function checkConnection() {
|
||||||
|
try {
|
||||||
|
processing = true;
|
||||||
|
const trialRemoteSetting = generateSetting();
|
||||||
|
const replicator = await context.services.replicator.getNewReplicator(trialRemoteSetting);
|
||||||
|
if (!replicator) {
|
||||||
|
return "Failed to create replicator instance.";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await replicator.tryConnectRemote(trialRemoteSetting, false);
|
||||||
|
if (result) {
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return "Failed to connect to the server. Please check your settings.";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return `Failed to connect to the server: ${e}`;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAndCommit() {
|
||||||
|
error = "";
|
||||||
|
try {
|
||||||
|
error = (await checkConnection()) || "";
|
||||||
|
if (!error) {
|
||||||
|
const setting = generateSetting();
|
||||||
|
setResult(pickCouchDBSyncSettings(setting));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error = `Error during connection test: ${e}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function commit() {
|
||||||
|
const setting = pickCouchDBSyncSettings(generateSetting());
|
||||||
|
setResult(setting);
|
||||||
|
}
|
||||||
|
function cancel() {
|
||||||
|
setResult(TYPE_CANCELLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// const isURICloudant = $derived.by(() => {
|
||||||
|
// return syncSetting.couchDB_URI && isCloudantURI(syncSetting.couchDB_URI);
|
||||||
|
// });
|
||||||
|
// const isURISelfHosted = $derived.by(() => {
|
||||||
|
// return syncSetting.couchDB_URI && !isCloudantURI(syncSetting.couchDB_URI);
|
||||||
|
// });
|
||||||
|
// const isURISecure = $derived.by(() => {
|
||||||
|
// return syncSetting.couchDB_URI && syncSetting.couchDB_URI.startsWith("https://");
|
||||||
|
// });
|
||||||
|
const isURIInsecure = $derived.by(() => {
|
||||||
|
return !!(syncSetting.couchDB_URI && syncSetting.couchDB_URI.startsWith("http://"));
|
||||||
|
});
|
||||||
|
const isUseJWT = $derived.by(() => {
|
||||||
|
return syncSetting.useJWT;
|
||||||
|
});
|
||||||
|
const canProceed = $derived.by(() => {
|
||||||
|
return (
|
||||||
|
syncSetting.couchDB_URI.trim().length > 0 &&
|
||||||
|
syncSetting.couchDB_USER.trim().length > 0 &&
|
||||||
|
syncSetting.couchDB_PASSWORD.trim().length > 0 &&
|
||||||
|
syncSetting.couchDB_DBNAME.trim().length > 0 &&
|
||||||
|
(isUseJWT ? syncSetting.jwtKey.trim().length > 0 : true)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const testSettings = $derived.by(() => {
|
||||||
|
return generateSetting();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogHeader title="CouchDB Configuration" />
|
||||||
|
<Guidance>Please enter the CouchDB server information below.</Guidance>
|
||||||
|
<InputRow label="URL">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="couchdb-url"
|
||||||
|
placeholder="https://example.com"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
bind:value={syncSetting.couchDB_URI}
|
||||||
|
required
|
||||||
|
pattern="^https?://.+"
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
<InfoNote warning visible={isURIInsecure}>We can use only Secure (HTTPS) connections on Obsidian Mobile.</InfoNote>
|
||||||
|
<InputRow label="Username">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="couchdb-username"
|
||||||
|
placeholder="Enter your username"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
required
|
||||||
|
bind:value={syncSetting.couchDB_USER}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
<InputRow label="Password">
|
||||||
|
<Password
|
||||||
|
name="couchdb-password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
bind:value={syncSetting.couchDB_PASSWORD}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
|
||||||
|
<InputRow label="Database Name">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="couchdb-database"
|
||||||
|
placeholder="Enter your database name"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
required
|
||||||
|
pattern="^[a-z0-9][a-z0-9_]*$"
|
||||||
|
bind:value={syncSetting.couchDB_DBNAME}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
<InfoNote>
|
||||||
|
You cannot use capital letters, spaces, or special characters in the database name. And not allowed to start with an
|
||||||
|
underscore (_).
|
||||||
|
</InfoNote>
|
||||||
|
<InputRow label="Use Internal API">
|
||||||
|
<input type="checkbox" name="couchdb-use-internal-api" bind:checked={syncSetting.useRequestAPI} />
|
||||||
|
</InputRow>
|
||||||
|
<InfoNote>
|
||||||
|
If you cannot avoid CORS issues, you might want to try this option. It uses Obsidian's internal API to communicate
|
||||||
|
with the CouchDB server. Not compliant with web standards, but works. Note that this might break in future Obsidian
|
||||||
|
versions.
|
||||||
|
</InfoNote>
|
||||||
|
|
||||||
|
<ExtraItems title="Advanced Settings">
|
||||||
|
<InputRow label="Custom Headers">
|
||||||
|
<textarea
|
||||||
|
name="couchdb-custom-headers"
|
||||||
|
placeholder="e.g., x-example-header: value\n another-header: value2"
|
||||||
|
bind:value={syncSetting.couchDB_CustomHeaders}
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
rows="4"
|
||||||
|
></textarea>
|
||||||
|
</InputRow>
|
||||||
|
</ExtraItems>
|
||||||
|
<ExtraItems title="Experimental Settings">
|
||||||
|
<InputRow label="Use JWT Authentication">
|
||||||
|
<input type="checkbox" name="couchdb-use-jwt" bind:checked={syncSetting.useJWT} />
|
||||||
|
</InputRow>
|
||||||
|
<InputRow label="JWT Algorithm">
|
||||||
|
<select bind:value={syncSetting.jwtAlgorithm} disabled={!isUseJWT}>
|
||||||
|
<option value="HS256">HS256</option>
|
||||||
|
<option value="HS512">HS512</option>
|
||||||
|
<option value="ES256">ES256</option>
|
||||||
|
<option value="ES512">ES512</option>
|
||||||
|
</select>
|
||||||
|
</InputRow>
|
||||||
|
<InputRow label="JWT Expiration Duration (seconds)">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="couchdb-jwt-exp-duration"
|
||||||
|
placeholder="0"
|
||||||
|
bind:value={() => `${syncSetting.jwtExpDuration}`, (v) => (syncSetting.jwtExpDuration = parseInt(v) || 0)}
|
||||||
|
disabled={!isUseJWT}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
<InputRow label="JWT Key">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="couchdb-jwt-key"
|
||||||
|
placeholder="Enter your JWT secret or private key"
|
||||||
|
bind:value={syncSetting.jwtKey}
|
||||||
|
disabled={!isUseJWT}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
<InputRow label="JWT Key ID (kid)">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="couchdb-jwt-kid"
|
||||||
|
placeholder="Enter your JWT Key ID (optional)"
|
||||||
|
bind:value={syncSetting.jwtKid}
|
||||||
|
disabled={!isUseJWT}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
<InputRow label="JWT Subject (sub)">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="couchdb-jwt-sub"
|
||||||
|
placeholder="Enter your JWT Subject (optional)"
|
||||||
|
bind:value={syncSetting.jwtSub}
|
||||||
|
disabled={!isUseJWT}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
<InfoNote warning>
|
||||||
|
JWT (JSON Web Token) authentication allows you to securely authenticate with the CouchDB server using tokens.
|
||||||
|
Ensure that your CouchDB server is configured to accept JWTs and that the provided key and settings match the
|
||||||
|
server's configuration. Incidentally, I have not verified it very thoroughly.
|
||||||
|
</InfoNote>
|
||||||
|
</ExtraItems>
|
||||||
|
|
||||||
|
<PanelCouchDBCheck trialRemoteSetting={testSettings}></PanelCouchDBCheck>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<InfoNote error visible={error !== ""}>
|
||||||
|
{error}
|
||||||
|
</InfoNote>
|
||||||
|
|
||||||
|
{#if processing}
|
||||||
|
Checking connection... Please wait.
|
||||||
|
{:else}
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title="Test Settings and Continue" important disabled={!canProceed} commit={() => checkAndCommit()} />
|
||||||
|
<Decision title="Continue anyway" commit={() => commit()} />
|
||||||
|
<Decision title="Cancel" commit={() => cancel()} />
|
||||||
|
</UserDecisions>
|
||||||
|
{/if}
|
||||||
123
src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte
Normal file
123
src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||||
|
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||||
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||||
|
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
|
||||||
|
import InputRow from "@/lib/src/UI/components/InputRow.svelte";
|
||||||
|
import Password from "@/lib/src/UI/components/Password.svelte";
|
||||||
|
import {
|
||||||
|
DEFAULT_SETTINGS,
|
||||||
|
E2EEAlgorithmNames,
|
||||||
|
E2EEAlgorithms,
|
||||||
|
type EncryptionSettings,
|
||||||
|
} from "../../../../lib/src/common/types";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
|
||||||
|
import { copyTo, pickEncryptionSettings } from "../../../../lib/src/common/utils";
|
||||||
|
const TYPE_CANCELLED = "cancelled";
|
||||||
|
type ResultType = typeof TYPE_CANCELLED | EncryptionSettings;
|
||||||
|
type Props = GuestDialogProps<ResultType, EncryptionSettings>;
|
||||||
|
const { setResult, getInitialData }: Props = $props();
|
||||||
|
let default_encryption: EncryptionSettings = {
|
||||||
|
encrypt: true,
|
||||||
|
passphrase: "",
|
||||||
|
E2EEAlgorithm: DEFAULT_SETTINGS.E2EEAlgorithm,
|
||||||
|
usePathObfuscation: true,
|
||||||
|
} as EncryptionSettings;
|
||||||
|
|
||||||
|
let encryptionSettings = $state<EncryptionSettings>({ ...default_encryption });
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (getInitialData) {
|
||||||
|
const initialData = getInitialData();
|
||||||
|
if (initialData) {
|
||||||
|
copyTo(initialData, encryptionSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let e2eeValid = $derived.by(() => {
|
||||||
|
if (!encryptionSettings.encrypt) return true;
|
||||||
|
return encryptionSettings.passphrase.trim().length >= 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
function commit() {
|
||||||
|
setResult(pickEncryptionSettings(encryptionSettings));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogHeader title="End-to-End Encryption" />
|
||||||
|
<Guidance>Please configure your end-to-end encryption settings.</Guidance>
|
||||||
|
<InputRow label="End-to-End Encryption">
|
||||||
|
<input type="checkbox" bind:checked={encryptionSettings.encrypt} />
|
||||||
|
<Password
|
||||||
|
name="e2ee-passphrase"
|
||||||
|
placeholder="Enter your passphrase"
|
||||||
|
bind:value={encryptionSettings.passphrase}
|
||||||
|
disabled={!encryptionSettings.encrypt}
|
||||||
|
required={encryptionSettings.encrypt}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
<InfoNote title="Strongly Recommended">
|
||||||
|
Enabling end-to-end encryption ensures that your data is encrypted on your device before being sent to the remote
|
||||||
|
server. This means that even if someone gains access to the server, they won't be able to read your data without the
|
||||||
|
passphrase. Make sure to remember your passphrase, as it will be required to decrypt your data on other devices.
|
||||||
|
<br />
|
||||||
|
Also, please note that if you are using Peer-to-Peer synchronization, this configuration will be used when you switch
|
||||||
|
to other methods and connect to a remote server in the future.
|
||||||
|
</InfoNote>
|
||||||
|
<InfoNote warning>
|
||||||
|
This setting must be the same even when connecting to multiple synchronisation destinations.
|
||||||
|
</InfoNote>
|
||||||
|
<InputRow label="Obfuscate Properties">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={encryptionSettings.usePathObfuscation}
|
||||||
|
disabled={!encryptionSettings.encrypt}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
|
||||||
|
<InfoNote>
|
||||||
|
Obfuscating properties (e.g., path of file, size, creation and modification dates) adds an additional layer of
|
||||||
|
security by making it harder to identify the structure and names of your files and folders on the remote server.
|
||||||
|
This helps protect your privacy and makes it more difficult for unauthorized users to infer information about your
|
||||||
|
data.
|
||||||
|
</InfoNote>
|
||||||
|
|
||||||
|
<ExtraItems title="Advanced">
|
||||||
|
<InputRow label="Encryption Algorithm">
|
||||||
|
<select bind:value={encryptionSettings.E2EEAlgorithm} disabled={!encryptionSettings.encrypt}>
|
||||||
|
{#each Object.values(E2EEAlgorithms) as alg}
|
||||||
|
<option value={alg}>{E2EEAlgorithmNames[alg] ?? alg}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</InputRow>
|
||||||
|
<InfoNote>
|
||||||
|
In most cases, you should stick with the default algorithm ({E2EEAlgorithmNames[
|
||||||
|
DEFAULT_SETTINGS.E2EEAlgorithm
|
||||||
|
]}), This setting is only required if you have an existing Vault encrypted in a different format.
|
||||||
|
</InfoNote>
|
||||||
|
<InfoNote warning>
|
||||||
|
Changing the encryption algorithm will prevent access to any data previously encrypted with a different
|
||||||
|
algorithm. Ensure that all your devices are configured to use the same algorithm to maintain access to your
|
||||||
|
data.
|
||||||
|
</InfoNote>
|
||||||
|
</ExtraItems>
|
||||||
|
|
||||||
|
<InfoNote warning>
|
||||||
|
<p>
|
||||||
|
Please be aware that the End-to-End Encryption passphrase is not validated until the synchronisation process
|
||||||
|
actually commences. This is a security measure designed to protect your data.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Therefore, we ask that you exercise extreme caution when configuring server information manually. If an
|
||||||
|
incorrect passphrase is entered, the data on the server will become corrupted. <br /><br />
|
||||||
|
Please understand that this is intended behaviour.
|
||||||
|
</p>
|
||||||
|
</InfoNote>
|
||||||
|
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title="Proceed" important disabled={!e2eeValid} commit={() => commit()} />
|
||||||
|
<Decision title="Cancel" commit={() => setResult(TYPE_CANCELLED)} />
|
||||||
|
</UserDecisions>
|
||||||
306
src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte
Normal file
306
src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// import { delay } from "octagonal-wheels/promises";
|
||||||
|
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||||
|
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||||
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
|
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||||
|
import InputRow from "@/lib/src/UI/components/InputRow.svelte";
|
||||||
|
import Password from "@/lib/src/UI/components/Password.svelte";
|
||||||
|
import { PouchDB } from "../../../../lib/src/pouchdb/pouchdb-browser";
|
||||||
|
import {
|
||||||
|
DEFAULT_SETTINGS,
|
||||||
|
P2P_DEFAULT_SETTINGS,
|
||||||
|
PREFERRED_BASE,
|
||||||
|
RemoteTypes,
|
||||||
|
type EntryDoc,
|
||||||
|
type ObsidianLiveSyncSettings,
|
||||||
|
type P2PConnectionInfo,
|
||||||
|
type P2PSyncSetting,
|
||||||
|
} from "../../../../lib/src/common/types";
|
||||||
|
|
||||||
|
import { TrysteroReplicator } from "../../../../lib/src/replication/trystero/TrysteroReplicator";
|
||||||
|
import type { ReplicatorHostEnv } from "../../../../lib/src/replication/trystero/types";
|
||||||
|
import { copyTo, pickP2PSyncSettings, type SimpleStore } from "../../../../lib/src/common/utils";
|
||||||
|
import { getObsidianDialogContext } from "../ObsidianSvelteDialog";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
|
||||||
|
import { SETTING_KEY_P2P_DEVICE_NAME } from "../../../../lib/src/common/types";
|
||||||
|
import ExtraItems from "../../../../lib/src/UI/components/ExtraItems.svelte";
|
||||||
|
|
||||||
|
const default_setting = pickP2PSyncSettings(DEFAULT_SETTINGS);
|
||||||
|
let syncSetting = $state<P2PConnectionInfo>({ ...default_setting });
|
||||||
|
|
||||||
|
const context = getObsidianDialogContext();
|
||||||
|
let error = $state("");
|
||||||
|
const TYPE_CANCELLED = "cancelled";
|
||||||
|
type SettingInfo = P2PConnectionInfo;
|
||||||
|
type ResultType = typeof TYPE_CANCELLED | SettingInfo;
|
||||||
|
type Props = GuestDialogProps<ResultType, P2PSyncSetting>;
|
||||||
|
|
||||||
|
const { setResult, getInitialData }: Props = $props();
|
||||||
|
onMount(() => {
|
||||||
|
if (getInitialData) {
|
||||||
|
const initialData = getInitialData();
|
||||||
|
if (initialData) {
|
||||||
|
copyTo(initialData, syncSetting);
|
||||||
|
}
|
||||||
|
if (context.services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME)) {
|
||||||
|
syncSetting.P2P_DevicePeerName = context.services.config.getSmallConfig(
|
||||||
|
SETTING_KEY_P2P_DEVICE_NAME
|
||||||
|
) as string;
|
||||||
|
} else {
|
||||||
|
syncSetting.P2P_DevicePeerName = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function generateSetting() {
|
||||||
|
const connSetting: P2PSyncSetting = {
|
||||||
|
// remoteType: ",
|
||||||
|
...P2P_DEFAULT_SETTINGS,
|
||||||
|
...syncSetting,
|
||||||
|
P2P_Enabled: true,
|
||||||
|
};
|
||||||
|
const trialSettings: P2PSyncSetting = {
|
||||||
|
...connSetting,
|
||||||
|
};
|
||||||
|
const trialRemoteSetting: ObsidianLiveSyncSettings = {
|
||||||
|
...DEFAULT_SETTINGS,
|
||||||
|
...PREFERRED_BASE,
|
||||||
|
remoteType: RemoteTypes.REMOTE_P2P,
|
||||||
|
...trialSettings,
|
||||||
|
};
|
||||||
|
return trialRemoteSetting;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkConnection() {
|
||||||
|
try {
|
||||||
|
processing = true;
|
||||||
|
const trialRemoteSetting = generateSetting();
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
const store = {
|
||||||
|
get: (key: string) => {
|
||||||
|
return Promise.resolve(map.get(key) || null);
|
||||||
|
},
|
||||||
|
set: (key: string, value: any) => {
|
||||||
|
map.set(key, value);
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
delete: (key: string) => {
|
||||||
|
map.delete(key);
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
keys: () => {
|
||||||
|
return Promise.resolve(Array.from(map.keys()));
|
||||||
|
},
|
||||||
|
} as SimpleStore<any>;
|
||||||
|
|
||||||
|
const dummyPouch = new PouchDB<EntryDoc>("dummy");
|
||||||
|
const env: ReplicatorHostEnv = {
|
||||||
|
settings: trialRemoteSetting,
|
||||||
|
processReplicatedDocs: async (docs: any[]) => {
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
confirm: context.plugin.confirm,
|
||||||
|
db: dummyPouch,
|
||||||
|
simpleStore: store,
|
||||||
|
deviceName: syncSetting.P2P_DevicePeerName || "unnamed-device",
|
||||||
|
platform: "setup-wizard",
|
||||||
|
};
|
||||||
|
const replicator = new TrysteroReplicator(env);
|
||||||
|
try {
|
||||||
|
await replicator.setOnSetup();
|
||||||
|
await replicator.allowReconnection();
|
||||||
|
await replicator.open();
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
// await delay(1000);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
// Logger(`Checking known advertisements... (${i})`, LOG_LEVEL_INFO);
|
||||||
|
if (replicator.knownAdvertisements.length > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// context.holdingSettings = trialRemoteSetting;
|
||||||
|
|
||||||
|
if (replicator.knownAdvertisements.length === 0) {
|
||||||
|
return "Your settings seem correct, but no other peers were found.";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
} catch (e) {
|
||||||
|
return `Failed to connect to other peers: ${e}`;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
replicator.close();
|
||||||
|
dummyPouch.destroy();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setDefaultRelay() {
|
||||||
|
syncSetting.P2P_relays = P2P_DEFAULT_SETTINGS.P2P_relays;
|
||||||
|
}
|
||||||
|
|
||||||
|
let processing = $state(false);
|
||||||
|
function generateDefaultGroupId() {
|
||||||
|
const randomValues = new Uint16Array(4);
|
||||||
|
crypto.getRandomValues(randomValues);
|
||||||
|
const MAX_UINT16 = 65536;
|
||||||
|
const a = Math.floor((randomValues[0] / MAX_UINT16) * 1000);
|
||||||
|
const b = Math.floor((randomValues[1] / MAX_UINT16) * 1000);
|
||||||
|
const c = Math.floor((randomValues[2] / MAX_UINT16) * 1000);
|
||||||
|
const d_range = 36 * 36 * 36;
|
||||||
|
const d = Math.floor((randomValues[3] / MAX_UINT16) * d_range);
|
||||||
|
syncSetting.P2P_roomID = `${a.toString().padStart(3, "0")}-${b
|
||||||
|
.toString()
|
||||||
|
.padStart(3, "0")}-${c.toString().padStart(3, "0")}-${d.toString(36).padStart(3, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAndCommit() {
|
||||||
|
error = "";
|
||||||
|
try {
|
||||||
|
error = (await checkConnection()) || "";
|
||||||
|
if (!error) {
|
||||||
|
const setting = generateSetting();
|
||||||
|
setResult(pickP2PSyncSettings(setting));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error = `Error during connection test: ${e}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function commit() {
|
||||||
|
const setting = pickP2PSyncSettings(generateSetting());
|
||||||
|
setResult(setting);
|
||||||
|
}
|
||||||
|
function cancel() {
|
||||||
|
setResult(TYPE_CANCELLED);
|
||||||
|
}
|
||||||
|
const canProceed = $derived.by(() => {
|
||||||
|
return (
|
||||||
|
syncSetting.P2P_relays.trim() !== "" &&
|
||||||
|
syncSetting.P2P_roomID.trim() !== "" &&
|
||||||
|
syncSetting.P2P_passphrase.trim() !== "" &&
|
||||||
|
(syncSetting.P2P_DevicePeerName ?? "").trim() !== ""
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogHeader title="P2P Configuration" />
|
||||||
|
<Guidance>Please enter the Peer-to-Peer Synchronisation information below.</Guidance>
|
||||||
|
<InputRow label="Enabled">
|
||||||
|
<input type="checkbox" name="p2p-enabled" bind:checked={syncSetting.P2P_Enabled} />
|
||||||
|
</InputRow>
|
||||||
|
<InputRow label="Relay URL">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="p2p-relay-url"
|
||||||
|
placeholder="Enter the Relay URL)"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
bind:value={syncSetting.P2P_relays}
|
||||||
|
/>
|
||||||
|
<button class="button" onclick={() => setDefaultRelay()}>Use vrtmrz's relay</button>
|
||||||
|
</InputRow>
|
||||||
|
<InputRow label="Group ID">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="p2p-room-id"
|
||||||
|
placeholder="123-456-789-abc"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
bind:value={syncSetting.P2P_roomID}
|
||||||
|
/>
|
||||||
|
<button class="button" onclick={() => generateDefaultGroupId()}>Generate Random ID</button>
|
||||||
|
</InputRow>
|
||||||
|
<InputRow label="Passphrase">
|
||||||
|
<Password name="p2p-password" placeholder="Enter your passphrase" bind:value={syncSetting.P2P_passphrase} />
|
||||||
|
</InputRow>
|
||||||
|
<InfoNote>
|
||||||
|
The Group ID and passphrase are used to identify your group of devices. Make sure to use the same Group ID and
|
||||||
|
passphrase on all devices you want to synchronise.<br />
|
||||||
|
Note that the Group ID is not limited to the generated format; you can use any string as the Group ID.
|
||||||
|
</InfoNote>
|
||||||
|
<InputRow label="Device Peer ID">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="p2p-device-peer-id"
|
||||||
|
placeholder="main-iphone16"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
bind:value={syncSetting.P2P_DevicePeerName}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
<InputRow label="Auto Start P2P Connection">
|
||||||
|
<input type="checkbox" name="p2p-auto-start" bind:checked={syncSetting.P2P_AutoStart} />
|
||||||
|
</InputRow>
|
||||||
|
<InfoNote>
|
||||||
|
If "Auto Start P2P Connection" is enabled, the P2P connection will be started automatically when the plug-in
|
||||||
|
launches.
|
||||||
|
</InfoNote>
|
||||||
|
<InputRow label="Auto Broadcast Changes">
|
||||||
|
<input type="checkbox" name="p2p-auto-broadcast" bind:checked={syncSetting.P2P_AutoBroadcast} />
|
||||||
|
</InputRow>
|
||||||
|
<InfoNote>
|
||||||
|
If "Auto Broadcast Changes" is enabled, changes will be automatically broadcasted to connected peers without
|
||||||
|
requiring manual intervention. This requests peers to fetch this device's changes.
|
||||||
|
</InfoNote>
|
||||||
|
<ExtraItems title="Advanced Settings">
|
||||||
|
<InfoNote>
|
||||||
|
TURN server settings are only necessary if you are behind a strict NAT or firewall that prevents direct P2P
|
||||||
|
connections. In most cases, you can leave these fields blank.
|
||||||
|
</InfoNote>
|
||||||
|
<InfoNote warning>
|
||||||
|
Using public TURN servers may have privacy implications, as your data will be relayed through third-party
|
||||||
|
servers. Even if your data are encrypted, your existence may be known to them. Please ensure you trust the TURN
|
||||||
|
server provider before using their services. Also your `network administrator` too. You should consider setting
|
||||||
|
up your own TURN server for your FQDN, if possible.
|
||||||
|
</InfoNote>
|
||||||
|
<InputRow label="TURN Server URLs (comma-separated)">
|
||||||
|
<textarea
|
||||||
|
name="p2p-turn-servers"
|
||||||
|
placeholder="turn:turn.example.com:3478,turn:turn.example.com:443"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
bind:value={syncSetting.P2P_turnServers}
|
||||||
|
rows="5"
|
||||||
|
></textarea>
|
||||||
|
</InputRow>
|
||||||
|
<InputRow label="TURN Username">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="p2p-turn-username"
|
||||||
|
placeholder="Enter TURN username"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
bind:value={syncSetting.P2P_turnUsername}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
<InputRow label="TURN Credential">
|
||||||
|
<Password
|
||||||
|
name="p2p-turn-credential"
|
||||||
|
placeholder="Enter TURN credential"
|
||||||
|
bind:value={syncSetting.P2P_turnCredential}
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
|
</ExtraItems>
|
||||||
|
<InfoNote error visible={error !== ""}>
|
||||||
|
{error}
|
||||||
|
</InfoNote>
|
||||||
|
{#if processing}
|
||||||
|
Checking connection... Please wait.
|
||||||
|
{:else}
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title="Test Settings and Continue" important disabled={!canProceed} commit={() => checkAndCommit()} />
|
||||||
|
<Decision title="Continue anyway" commit={() => commit()} />
|
||||||
|
<Decision title="Cancel" commit={() => cancel()} />
|
||||||
|
</UserDecisions>
|
||||||
|
{/if}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user